Vercel is the path of least resistance, but many teams run Next.js on their own infrastructure: a VPS, Kubernetes, a company cloud account. Next.js supports this fully through next start and the standalone output mode. After this lesson you will be able to build a small production Docker image, run it with a database via Compose, and handle the details that differ from a managed platform: environment variables, image optimisation and the ISR cache.
Set output: "standalone" in next.config.ts. During next build, Next.js traces every file the server actually imports and copies them into .next/standalone, including a minimal server.js and only the required parts of node_modules. The result is typically a fraction of the size of a full node_modules folder. Two folders are not included and must be copied alongside: public/ and .next/static/.
The sample at the top of this lesson uses three stages:
npm ci so the layer is cached until the lockfile changes.next build.Add a .dockerignore so the build context stays small:
node_modules
.next
.git
.env*.localBuild and run:
docker build -t my-next-app .
docker run -p 3000:3000 --env-file .env.production my-next-appHOSTNAME=0.0.0.0 is required so the server listens on all interfaces inside the container rather than only on localhost.
Server-side variables are read when the container starts, so --env-file or the orchestrator's secrets work as expected. NEXT_PUBLIC_ variables are different: they are inlined during npm run build, which happens inside the image. Either pass them as build arguments (ARG NEXT_PUBLIC_SITE_URL before RUN npm run build) or avoid them for values that change between environments and read them on the server instead.
A Compose file wires the app to PostgreSQL for staging or small deployments:
# compose.yaml
services:
web:
build: .
ports: ["3000:3000"]
environment:
DATABASE_URL: postgresql://app:secret@db:5432/app
depends_on: [db]
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
pgdata:Run migrations as a separate one-off command (docker compose run web npx prisma migrate deploy) rather than at container start, so several replicas never race to migrate.
| Concern | What to do |
|---|---|
| Image optimisation | sharp is bundled with Next.js; nothing extra is needed. For very high traffic, set images.loader to a CDN. |
| ISR and Data Cache with several replicas | Each instance has its own file cache by default; configure cacheHandler in next.config.ts with a shared store such as Redis, and set cacheMaxMemorySize: 0. |
| TLS and compression | Terminate TLS at a reverse proxy (nginx, Caddy, a load balancer) in front of port 3000. |
| Process management without Docker | Run next start under PM2 or a systemd unit so it restarts on failure. |
| Health checks | Add app/api/health/route.ts returning 200 and point the orchestrator at it. |
.next folder instead of .next/standalone, which defeats the size savings..next/static and public, resulting in a running server with missing CSS and images.What does `output: "standalone"` copy into `.next/standalone`?
output: "standalone" produces a minimal server folder; copy public and .next/static next to it.HOSTNAME=0.0.0.0.NEXT_PUBLIC_ values must be present at build time.cacheHandler and put a reverse proxy in front.Next lesson: Migrating from the Pages Router to the App Router ā move an existing project route by route without a rewrite.