The API Dockerfile intentionally builds only the API workspace and its dependency graph:
RUN pnpm build --filter @toast/api...
Why this is scoped:
Reduces Docker build time by skipping unrelated workspace builds
Improves cache reuse by avoiding invalidation from unrelated package changes
Keeps CI and local image builds aligned with API runtime needs
If the API image needs artifacts from packages outside this graph, either make those packages real dependencies of @toast/api or widen the filter in Dockerfile.
The docs app uses symlinks from apps/docs/content/code/ to the repo-root docs/ directory to include contributor documentation (patterns, decisions, drivers, reference). Docker COPY doesn't follow symlinks outside the build context, so the Dockerfile:
Copies the repo-root docs/ directory into the container
Replaces symlinks with real copies during the build stage
Runs pnpm build --filter @toast/docs to produce Next.js standalone output
The .dockerignore re-includes apps/docs/**/*.md, apps/docs/**/*.mdx, and docs/**/*.md to ensure content files reach the build context despite the global *.md exclusion.
Additionally, fumadocs-mdx runs a postinstall script that compiles source.config.ts via esbuild. This file (and tsconfig.json) must be copied into the deps stage beforepnpm install, or the postinstall fails with "The entry point source.config.ts cannot be marked as external".
Note: ARG values can appear in docker history but are NOT persisted in the final image metadata (unlike ENV). This is acceptable because Railway and ghcr.io images are private
Both services need these variables set in Railway:
API Service:
Variable
Purpose
TIPTAP_PRO_TOKEN
NPM registry auth (build time)
TIPTAP_COLLAB_SECRET
JWT signing for collaboration tokens
TIPTAP_AI_SECRET
JWT signing for AI tokens
VITE_TIPTAP_COLLAB_APP_ID
Returned in collaboration token response
VITE_TIPTAP_AI_APP_ID
Returned in AI token response
EMAIL_DRIVER
Email provider (required in production)
EMAIL_FROM
Default sender address (e.g., noreply@...)
MAILGUN_API_KEY
Mailgun API key (if using Mailgun)
MAILGUN_DOMAIN
Mailgun sending domain (if using Mailgun)
MAILGUN_REGION
Mailgun region: us or eu (optional)
STORAGE_DRIVER
Storage backend (toast-driver-storage-s3)
S3_ENDPOINT
S3-compatible endpoint URL
S3_ACCESS_KEY_ID
S3 access key ID
S3_SECRET_ACCESS_KEY
S3 secret access key
S3_BUCKET
S3 bucket name
S3_REGION
S3 region (default: auto)
S3_PUBLIC_URL
Public URL for accessing uploaded files
Admin Service:
Variable
Purpose
TIPTAP_PRO_TOKEN
NPM registry auth (build time)
VITE_API_URL
API endpoint for frontend (build time)
Important:VITE_API_URL is baked into the static JavaScript bundle at build time. If you change it, you must rebuild the admin image - a restart won't pick up the change.
Docs Service:
Variable
Purpose
TIPTAP_PRO_TOKEN
NPM registry auth (build time)
The docs service has no runtime environment variables. Content is statically built into the Next.js standalone output.
JSON content files for each profile (profiles/default/content.json, profiles/development/content.json)
The seed script uses these at runtime when preDeployCommand runs pnpm db:seed on Railway. The fixtures are loaded relative to the compiled seed script's location via import.meta.url.
Note: The fixture files are copied during pnpm build in the @toast/db package via a copy-fixtures script that moves JSON profiles from src/fixtures/profiles to dist/fixtures/profiles.
This is a transient network failure - Railway's build VM couldn't reach the npm registry. The Dockerfiles pre-install pnpm via corepack prepare to mitigate this, but if the error occurs during pnpm install (fetching actual packages), it's a temporary infrastructure issue. Retry the deployment.
When you update packageManager in package.json, you must also update all three Dockerfiles:
# In Dockerfile, apps/admin/Dockerfile.admin, and apps/docs/Dockerfile.docsRUN corepack enable && corepack prepare pnpm@10.28.2 --activate # ← Update version here
This ensures pnpm is pre-installed in the base image layer, which:
Allows Docker to cache the layer (faster subsequent builds)
Avoids on-demand downloads that can timeout during npm registry issues
Note: Pre-built admin images have VITE_API_URL baked in at CI build time:
VITE_API_URL - defaults to empty (uses relative URLs)
For custom values, build your own admin image (see Building Locally).
Security: Passing secrets via -e flags exposes them in ps output and shell history. For production, use:
Env file: docker run --env-file .env ... (secure file permissions with chmod 600)
Docker secrets: For Docker Swarm/Kubernetes deployments
Orchestrator secrets: Railway, Fly.io, etc. handle this automatically
# Run API (example using env file)docker run -p 3000:3000 --env-file .env ghcr.io/tryghost/toast-api:main# Required in .env:# DATABASE_URL=postgresql://...# BETTER_AUTH_SECRET=...# TIPTAP_COLLAB_SECRET=...# TIPTAP_AI_SECRET=...# VITE_TIPTAP_COLLAB_APP_ID=...# VITE_TIPTAP_AI_APP_ID=...# Run Admin (pre-built images use CI's baked-in VITE_API_URL value)docker run -p 8080:80 ghcr.io/tryghost/toast-admin:main# For custom API URL, build your own admin image:docker build -f apps/admin/Dockerfile.admin \ --build-arg VITE_API_URL=https://your-api.example.com \ --build-arg TIPTAP_PRO_TOKEN=$TIPTAP_PRO_TOKEN \ -t my-toast-admin .
Railway can be configured to pull pre-built images instead of building:
Change service source from "GitHub Repo" to "Docker Image"
Set image URL: ghcr.io/tryghost/toast-api:main (or toast-admin, toast-docs)
Add registry credentials if images are private
This eliminates build time and avoids TipTap registry rate limiting.
Admin image note: Pre-built admin images have VITE_API_URL baked at build time. If your Railway environment needs a different API URL, keep the admin service building from "GitHub Repo" or set up a custom build pipeline.