Deploy to Node / Docker
Self-managing a Node server gives you full control and a fixed cost: a single VPS runs the
whole site - builder, CMS, and rendering - with the ISR cache on local disk. The starter
ships a production Dockerfile, and a plain pnpm build && pnpm start works anywhere
Node 20+ runs.
Option A: plain Node server
On the server (or in CI, shipping the build output to the server):
pnpm install
pnpm run build
pnpm run start # serves on port 3000
Set the environment variables from
Environment & License in the process
environment (systemd Environment=, a .env loaded by your process manager, or your
platform's secret store).
Keep the process alive with a process manager:
# pm2
pm2 start pnpm --name chaibuilder -- start
# or a systemd unit with ExecStart=pnpm run start, Restart=always
Single instance, local SQLite is fine. A long-running server is the one target where a local SQLite file works in production - it persists on disk. Move to Turso or Postgres before scaling to multiple instances.
Option B: Docker
The starter includes a multi-stage Dockerfile based on Next.js standalone output - it
builds the app and produces a small runtime image that starts node server.js as a
non-root user on port 3000.
One requirement: standalone output must be enabled in next.config.ts:
const nextConfig: NextConfig = {
output: 'standalone',
// …rest of your config
}
Then:
docker build -t my-site .
docker run -d -p 3000:3000 --env-file .env.production my-site
Notes on the shipped image:
- Based on
node:22-alpinewithlibc6-compatinstalled (needed by sharp). - Detects your package manager from the lockfile - pnpm works out of the box.
- The ISR cache lives in
.nextinside the container. It is a cache, not data - losing it on redeploy just means pages rebuild on first visit.
A docker-compose.yml is included for local development with a containerized database;
for production compose setups, add your app service plus Postgres and point DATABASE_URL
at the database service name.
Reverse proxy and TLS
Put the app behind a reverse proxy that terminates TLS:
server {
listen 443 ssl;
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
- Forwarding
HostandX-Forwarded-Protomatters - the app derives the site origin from the request, and licensing validates against your domain. - Raise
client_max_body_size(nginx defaults to 1 MB) so media uploads are not rejected by the proxy before reaching the app. - Caddy and Traefik work equally well and handle certificates automatically.
Deploy updates
A standard zero-fuss update flow:
- Build the new image (or run
pnpm buildin a release directory). - Stop the old process/container and start the new one - pending database migrations run automatically on boot (see Database Setup).
- First visits after the deploy rebuild pages into the fresh ISR cache.
For zero-downtime, run the new container next to the old one and switch the proxy upstream once it is healthy.
Checklist
- <input disabled="" type="checkbox"> Node 20.18.1+ (plain Node) or Docker on the host
- <input disabled="" type="checkbox">
output: 'standalone'set when using the Dockerfile - <input disabled="" type="checkbox"> Env vars in the process/container environment
- <input disabled="" type="checkbox"> Reverse proxy forwarding
HostandX-Forwarded-Proto, TLS configured - <input disabled="" type="checkbox"> Upload size limit raised on the proxy
- <input disabled="" type="checkbox"> Object storage configured - the container filesystem is not a media store
- <input disabled="" type="checkbox"> Database backups scheduled
Related
- Requirements - runtime versions and native deps.
- Database Setup - SQLite vs Postgres on a VPS.
- Caching & Revalidation - how the on-disk ISR cache behaves.

