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:
| File | Service | Purpose |
|---|---|---|
Dockerfile | API | Node.js Hono server with database access |
apps/admin/Dockerfile.admin | Admin | Vite static build served via nginx |
apps/docs/Dockerfile.docs | Docs | Next.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.jsonengines) - CI, Docker images, and the devcontainer run Node.js 24
.nvmrcand.node-versionare pinned to24
Why Docker Builds Break
The most common cause is missing files or environment variables that pnpm needs during install.
The .npmrc requirement
Toast uses TipTap Pro packages from a private registry. .npmrc configures authentication:
@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=${TIPTAP_PRO_TOKEN}Both Dockerfiles pass the token as a build arg:
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
ARG TIPTAP_PRO_TOKEN
RUN pnpm install --frozen-lockfileAPI 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
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 static bundle at build time)
docker build --build-arg TIPTAP_PRO_TOKEN="$TIPTAP_PRO_TOKEN" \
--build-arg VITE_API_URL="https://your-api-url.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 .Required Environment Variables
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 |
STORAGE_DRIVER | Storage backend driver package name |
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_PUBLIC_URL | Public URL for uploaded files |
Admin Service:
| Variable | Purpose |
|---|---|
TIPTAP_PRO_TOKEN | NPM registry auth (build time) |
VITE_API_URL | API endpoint for frontend (baked at build time) |
Docs Service:
| Variable | Purpose |
|---|---|
TIPTAP_PRO_TOKEN | NPM registry auth (build time) |
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/fixturesThe 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| Tag | Description |
|---|---|
main | Latest 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.