Self-Hosting
Deploy Toast with Railway, Docker, or manually.
Toast runs as three services — an API server, an admin panel, and a documentation site — backed by PostgreSQL. You can deploy all three together or individually.
Runtime Topology
┌──────────────┐
Browser ──────► │ Admin (nginx)│ :80
└──────┬───────┘
│ fetch
▼
┌──────────────┐ ┌────────────┐
Browser ──────► │ API (Hono) │ ───► │ PostgreSQL │
└──────────────┘ └────────────┘
:3000 :5432
┌──────────────┐
Browser ──────► │ Docs (Next.js)│ :3000
└──────────────┘- API — Hono server handling authentication, content management, uploads, and the public content API
- Admin — Static React build served by nginx, communicates with the API via fetch
- Docs — Next.js standalone server, no database connection required
- PostgreSQL — Single database shared by the API
Railway (Recommended)
Railway provides the simplest deployment path with automatic PR preview environments, managed PostgreSQL, and zero Docker knowledge required.
See the full Railway Setup Guide for step-by-step instructions covering:
- Provisioning all three services from the GitHub repo
- Adding a managed PostgreSQL database
- Configuring environment variables with Railway's interpolation syntax
- Setting up PR preview environments
- Custom domains and cross-site cookie configuration
Estimated setup time: 15-20 minutes.
Docker
Toast provides pre-built Docker images and Dockerfiles for all three services.
Pre-Built Images
CI publishes images to GitHub Container Registry on every commit:
# Authenticate with GHCR (images are currently private)
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Pull latest images
docker pull ghcr.io/tryghost/toast-api:main
docker pull ghcr.io/tryghost/toast-admin:main
docker pull ghcr.io/tryghost/toast-docs:main| Tag | Description |
|---|---|
main | Latest from main branch (mutable) |
sha-<commit> | Specific commit (immutable) |
pr-<number> | PR preview build |
Running with Docker
# API server (provide your env vars via --env-file)
docker run -p 3000:3000 --env-file .env ghcr.io/tryghost/toast-api:main
# Admin panel (pre-built images use relative API URLs)
docker run -p 8080:80 ghcr.io/tryghost/toast-admin:main
# Docs site (no env vars needed at runtime)
docker run -p 3300:3000 ghcr.io/tryghost/toast-docs:mainPass secrets via
--env-filerather than-eflags. The-eflag exposes values inpsoutput and shell history. Usechmod 600 .envto restrict file permissions.
Building Locally
If you need custom configuration (e.g., a different VITE_API_URL for the admin panel):
export TIPTAP_PRO_TOKEN=<your-token>
# API
docker build \
--build-arg TIPTAP_PRO_TOKEN="$TIPTAP_PRO_TOKEN" \
-t toast-api .
# Admin (VITE_API_URL is baked into the JS bundle at build time)
docker build \
--build-arg TIPTAP_PRO_TOKEN="$TIPTAP_PRO_TOKEN" \
--build-arg VITE_API_URL="https://api.example.com" \
-f apps/admin/Dockerfile.admin -t toast-admin .
# Docs
docker build \
--build-arg TIPTAP_PRO_TOKEN="$TIPTAP_PRO_TOKEN" \
-f apps/docs/Dockerfile.docs -t toast-docs .Docker Compose
For local self-hosting, the repository includes a docker-compose.yml that runs the API and PostgreSQL:
# Start API + PostgreSQL
docker compose up -d
# Check logs
docker compose logs -f apiFor the full Docker reference, including troubleshooting and CI integration, see Docker Patterns.
Manual Deployment
If you're not using Docker, you can build and run the services directly.
Prerequisites
- Node.js 24.x
- pnpm 10.x
- PostgreSQL 15+
Build the API
git clone https://github.com/TryGhost/Toast.git
cd Toast
pnpm install
# Build the API and its workspace dependencies
pnpm build --filter @toast/api...
# Run database migrations
pnpm db:migrate
# Start the API server
node apps/api/dist/index.jsBuild the Admin Panel
# Set the API URL (baked into the static bundle)
export VITE_API_URL=https://api.example.com
pnpm build --filter @toast/admin
# Serve the static files from apps/admin/dist/
# Use any static file server (nginx, caddy, etc.)Build the Docs Site
pnpm build --filter @toast/docs
# Start the Next.js standalone server
node apps/docs/.next/standalone/apps/docs/server.jsEnvironment Variables
Required for All Deployments
| Variable | Purpose | Example |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | postgresql://user:pass@host:5432/toast |
BETTER_AUTH_SECRET | Auth token signing (min 32 chars) | openssl rand -base64 32 |
BETTER_AUTH_URL | Base URL for auth callbacks | https://api.example.com |
ADMIN_URL | Admin panel URL (CORS, trusted origin) | https://admin.example.com |
TIPTAP_PRO_TOKEN | NPM registry auth (build time only) | Get from TipTap Cloud |
Email (Required in Production)
| Variable | Purpose | Example |
|---|---|---|
EMAIL_DRIVER | Email provider package name | toast-driver-email-mailgun |
EMAIL_FROM | Default sender address | noreply@example.com |
MAILGUN_API_KEY | Mailgun API key | key-xxx |
MAILGUN_DOMAIN | Mailgun sending domain | mg.example.com |
MAILGUN_REGION | Mailgun region (us or eu) | us |
See Driver Configuration for all available providers.
Storage (Optional)
| Variable | Purpose | Example |
|---|---|---|
STORAGE_DRIVER | Storage provider package name | toast-driver-storage-s3 |
S3_ENDPOINT | S3-compatible endpoint | https://s3.amazonaws.com |
S3_ACCESS_KEY_ID | Access key | AKIA... |
S3_SECRET_ACCESS_KEY | Secret key | wJal... |
S3_BUCKET | Bucket name | toast-uploads |
S3_REGION | Region | us-east-1 |
S3_PUBLIC_URL | Public URL for uploaded files | https://cdn.example.com |
TipTap Cloud (Optional)
Real-time collaboration and AI features require a TipTap Cloud account.
| Variable | Purpose |
|---|---|
VITE_TIPTAP_COLLAB_APP_ID | Collaboration app ID (client) |
TIPTAP_COLLAB_SECRET | Collaboration JWT secret (server) |
VITE_TIPTAP_AI_APP_ID | AI app ID (client) |
TIPTAP_AI_SECRET | AI JWT secret (server) |
Admin Panel (Build Time)
| Variable | Purpose | Default |
|---|---|---|
VITE_API_URL | API endpoint baked into the JS bundle | (empty/relative) |
Changing
VITE_API_URLrequires rebuilding the admin image. A restart won't pick up the change because it's embedded in the static JavaScript.
Production Checklist
Before going live, verify:
-
BETTER_AUTH_SECRETis a unique, random 32+ character string -
EMAIL_DRIVERis set to a real provider (notconsole) -
EMAIL_FROMis set to a verified sender address -
ADMIN_URLmatches the actual admin panel domain -
BETTER_AUTH_URLmatches the actual API domain - Database has SSL enabled for the connection
- TipTap tokens are set if you want collaboration/AI features
- Storage driver is configured if you want image uploads