This recipe runs a Next.js Node server on port 3000. It keeps server rendering and a dynamic health route; it is not a static export. The sample makes its release ID visible so you can distinguish a successful update from a response served by the previous app.
Verification scope
The downloadable sample was checked on September 24, 2026 using both its Dockerfile and Nixpacks 1.39.0 build configuration. Checks covered HTTPS through a local Caddy proxy, static assets, runtime release IDs and configuration recovery. A local certificate authority was used for those tests; public certificate issuance and a licensed QuickDeploy SSH deployment still need verification on your own test server.
Before you start
Use a Linux VPS with Docker Engine and Docker Compose, an SSH user allowed to run Docker, and a domain pointing to that server. Ports 80 and 443 must reach your reverse proxy. Follow the Ubuntu server checklist first.
Start with a separate test domain and project. If a reverse proxy already owns ports 80/443, extend that proxy rather than starting another. The manual Docker path does not need a QuickDeploy license. The optional QuickDeploy path requires a license and a compatible server; check requirements before buying.
Get the sample
On your computer, download the archive using the button above or run:
curl -fL https://quickdeploy.dev/examples/nextjs.tar.gz -o nextjs.tar.gz
tar -xzf nextjs.tar.gz
cd nextjs
The archive includes the application, exact dependency lockfile, Dockerfile, .dockerignore, .env.example, and nixpacks.toml. Archive checksums identify the downloadable versions. Use Node.js 24 if building outside Docker. Do not replace your existing app with the sample; use it to verify the deployment path first.
Configure the production server
The sample pins Next.js 16.3.6 and React 19.3.0. Its package scripts build once and start the production server:
{
"build": "next build --webpack",
"start": "next start --hostname 0.0.0.0 --port 3000"
}
Use npm ci with the included lockfile. The Dockerfile builds in a Node.js 24 stage, copies the application into a runtime stage and runs as the unprivileged node user. This example deliberately uses next start. If your existing project uses output: 'standalone', run its generated server.js instead and copy public and .next/static into the standalone output; do not mix the two startup approaches. See Next.js self-hosting.
The sample's /api/health route is dynamic and returns { "ok": true, "release": "first-deploy" } plus server time. RELEASE_ID is read at runtime. Public NEXT_PUBLIC_* variables are embedded during the build; changing container variables later will not replace those browser values. Never use that prefix for secrets.
Build and run locally with Docker
On your computer, inside the extracted directory:
docker build -t nextjs-recipe:1 .
docker run --rm --name nextjs-recipe-local \
-p 127.0.0.1:3100:3000 \
-e RELEASE_ID=first-deploy nextjs-recipe:1
Use an available host port if 3100 is occupied. Open http://localhost:3100, /api/health and /verification.txt. The page should show the release ID, the JSON should report ok: true, and the static file should contain quickdeploy-static-asset. Stop this sample with Ctrl+C when finished.
When your laptop and VPS use different CPU architectures, build for the VPS, for example docker buildx build --platform linux/amd64 --load -t nextjs-recipe:1 .. The build needs enough RAM for your application, even when serving it later uses less.
Put the image behind HTTPS
Use the Docker and Caddy walkthrough to transfer the image and configure the shared proxy network. For this sample use image nextjs-recipe:1, an internal app port of 3000, and a unique service/container name such as nextjs-recipe.
On the VPS, the app's Compose service can be:
services:
app:
image: nextjs-recipe:1
container_name: nextjs-recipe
environment:
RELEASE_ID: first-deploy
expose:
- "3000"
networks:
- web
restart: unless-stopped
networks:
web:
external: true
After creating the web network if necessary, connect Caddy to it and add this site to its existing Caddyfile:
next.example.com {
reverse_proxy nextjs-recipe:3000
}
Replace the domain with yours. Run the app with docker compose up -d in its own directory, validate Caddy's configuration, then reload your proxy using the walkthrough. The app does not publish port 3000 on the host. Caddy handles external HTTPS traffic.
Use QuickDeploy instead of the manual transfer
QuickDeploy builds the source with Nixpacks, not this Dockerfile. The sample's nixpacks.toml pins a Nix package archive, selects Node.js 24 and OpenSSL explicitly, then runs npm ci --include=dev, npm run build and npm start. This avoids the older builder's automatic Node version selection. Both build paths were checked locally. A full QuickDeploy SSH deployment is still a separate check.
After installing and configuring QuickDeploy, create .env.production in the sample directory:
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1
RELEASE_ID=first-deploy
On your computer, run:
quickdeploy push --domain next.example.com --project nextjs-recipe --port 3000
Use an unused project name on a suitable test server. QuickDeploy manages a shared Caddy deployment; its first deployment can recreate that proxy. Do not use this path on a host whose existing containers must stay untouched. Keep the command attached so a server-side failure can reach your terminal.
Verify and deploy an update
From your computer:
curl --fail --show-error https://next.example.com/api/health
curl --fail --show-error https://next.example.com/verification.txt
Open the homepage too. Check for loaded assets and browser errors. For your own application, exercise a real server action or API route: a working homepage alone does not verify every Next.js feature.
Change RELEASE_ID to second-deploy and redeploy using the same project name. On the manual path, change the Compose environment and run docker compose up -d; rebuild and tag a new image when source changes. On the QuickDeploy path, update .env.production and repeat the same push command. Check that /api/health and the page show the new release.
Keep the previous image and source revision until the check passes. For a manual recovery, restore the previous image tag and environment, run Compose again, and repeat the checks. With QuickDeploy, redeploy the last known working source and configuration; this is not a promise of an automatic HTTP-based rollback. QuickDeploy's built-in check tests whether the process stays running.
Troubleshooting
- Caddy returns 502: check the app logs, internal port 3000, shared Docker network and listener on
0.0.0.0. - Missing JavaScript or CSS: check requests under
/_next/static/. A standalone output requires the separately copied static assets; the included normalnext startimage keeps.nextandpublictogether. - A changed variable has no effect: determine whether it is a public build-time value or a server runtime value, then rebuild or recreate the app as appropriate.
- An old release still responds: compare
/api/health, Caddy's upstream and the running container image. Do not declare success from HTTP 200 alone.
For a database-backed app, use the Next.js/Prisma PostgreSQL example. Keep database migrations and persistence checks separate from static page checks. After a manual release works, use the GitHub Actions deployment guide.