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) for main pushes and same-repository pull requests after the prerequisite checks pass. The required CI check waits for all three builds and fails if any build fails. Fork pull requests skip publishing but must still pass every prerequisite check.
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.
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.
Workspace installs
Every install stage copies all workspace manifests so frozen installs can validate the complete lockfile. Filters select the service and its dependencies; build stages also install root build tools. The API production stage installs only its runtime dependency graph. When adding a workspace, add its manifest to all three Dockerfiles, including the API production stage.
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:
| Variable | Purpose |
|---|---|
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 |
|---|---|
VITE_API_URL | API endpoint for frontend (baked at build time) |
TIPTAP_PRO_TOKEN | Optional — 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/fixturesThe seed script uses these at runtime when Railway's preDeployCommand runs pnpm db:seed.
Pre-built Images
CI pushes to ghcr.io for main pushes and same-repository pull requests:
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" — If the error names a missing workspace, check the manifest copies in the Dockerfile. If a manifest has changed, run pnpm install locally and commit the updated lockfile.
"Connect Timeout Error" — Transient network failure in CI. Retry the deployment.
Updating pnpm version — Docker uses Corepack to install the version declared in the root package.json automatically.