Toast
Contributor

Docker

How Docker builds work in Toast and how to avoid breaking them.

Overview

Toast has three Dockerfiles, each producing a multi-stage production image:

FileServicePurpose
DockerfileAPINode.js Hono server with database access
apps/admin/Dockerfile.adminAdminVite static build served via nginx
apps/docs/Dockerfile.docsDocsNext.js standalone build for documentation

CI builds and pushes all three to GitHub Container Registry (ghcr.io) on every commit.

Node.js Version Policy

  • Toast requires Node.js 24.x (package.json engines)
  • CI, Docker images, and the devcontainer run Node.js 24
  • .nvmrc and .node-version are pinned to 24

Why Docker Builds Break

The most common cause is missing files or environment variables that pnpm needs during install.

No private registry (token-free builds)

Toast has no private-registry dependencies — Docker builds need no token or registry auth. The committed .npmrc is token-free and pnpm install --frozen-lockfile works as-is. The ARG TIPTAP_PRO_TOKEN in the Dockerfiles is now unused and may be left empty.

The editor's paid TipTap Pro extensions are decoupled and off by default (the editor degrades gracefully without them — see Local Dev Setup). To bake them into an image, your build must run ./scripts/enable-tiptap-pro.sh with a valid TIPTAP_PRO_TOKEN before pnpm install, so the @tiptap-pro/* overlay is installed.

API build scope

The API Dockerfile builds only the API workspace and its dependency graph:

RUN pnpm build --filter @toast/api...

If the API image needs artifacts from packages outside this graph, make those packages real dependencies of @toast/api or widen the filter.

Docs build: postinstall order

fumadocs-mdx runs a postinstall that compiles source.config.ts via esbuild. This file and tsconfig.json must be copied into the deps stage before pnpm install, or the postinstall fails.

Building Locally

# API
docker build -t toast-api .

# Admin (VITE_API_URL is baked into the static bundle at build time)
docker build --build-arg VITE_API_URL="https://your-api-url.com" \
  -f apps/admin/Dockerfile.admin -t toast-admin .

# Docs
docker build -f apps/docs/Dockerfile.docs -t toast-docs .

No TIPTAP_PRO_TOKEN build arg is needed. To include the optional Pro extensions, run ./scripts/enable-tiptap-pro.sh (with a valid token) before building so the overlay is present in the build context.

Required Environment Variables

API Service:

VariablePurpose
TIPTAP_COLLAB_SECRETJWT signing for collaboration tokens
TIPTAP_AI_SECRETJWT signing for AI tokens
VITE_TIPTAP_COLLAB_APP_IDReturned in collaboration token response
VITE_TIPTAP_AI_APP_IDReturned in AI token response
EMAIL_DRIVEREmail provider (required in production)
EMAIL_FROMDefault sender address
STORAGE_DRIVERStorage backend driver package name
S3_ENDPOINTS3-compatible endpoint URL
S3_ACCESS_KEY_IDS3 access key ID
S3_SECRET_ACCESS_KEYS3 secret access key
S3_BUCKETS3 bucket name
S3_PUBLIC_URLPublic URL for uploaded files

Admin Service:

VariablePurpose
VITE_API_URLAPI endpoint for frontend (baked at build time)
TIPTAP_PRO_TOKENOptional — only if enabling the TipTap Pro overlay (see above)

Docs Service:

The docs image needs no build-time secrets (it has no private dependencies).

Seed Fixtures

The API Dockerfile copies seed fixtures into the final image for preview/staging environments:

COPY --from=build /app/shared/db/dist/fixtures ./shared/db/dist/fixtures

The seed script uses these at runtime when Railway's preDeployCommand runs pnpm db:seed.

Pre-built Images

CI pushes to ghcr.io on every commit:

docker pull ghcr.io/tryghost/toast-api:main
docker pull ghcr.io/tryghost/toast-admin:main
docker pull ghcr.io/tryghost/toast-docs:main
TagDescription
mainLatest from main branch (mutable)
sha-<commit>Specific commit (immutable)
pr-<number>PR preview build

Troubleshooting

"ERR_PNPM_FETCH_404" or "No authorization header" — Token isn't reaching pnpm. Check: is .npmrc copied? Is the ARG defined before COPY? Is the build-arg being passed?

"Lockfile is not up to date" — Run pnpm install locally and commit the updated lockfile.

"Connect Timeout Error" — Transient network failure in CI. Retry the deployment.

Updating pnpm version — When you update packageManager in package.json, also update all three Dockerfiles' corepack prepare pnpm@X.Y.Z lines.

On this page