# Next.js + Docker Setup Guide ## Principle Run Next.js 16 in Docker using multi-stage builds with pnpm standalone output for minimal production images, and volume-mounted source code with filesystem polling for fast development hot reload. ## Architecture ### Multi-Stage Build Flow ``` +------------------+ +------------------+ +------------------+ | Stage: deps | | Stage: build | | Stage: runner | | | | | | | | node:20-slim | | node:20-slim | | node:20-slim | | | | | | | | - corepack | --> | - Copy deps from | --> | - Copy standalone| | enable pnpm | | deps stage | | from build | | - pnpm install | | - Copy src | | - Copy static | | --frozen- | | - next build | | assets | | lockfile | | (standalone) | | - Run server.js | +------------------+ +------------------+ +------------------+ ~600 MB ~900 MB ~200 MB (cached between (discarded after (final image) builds) build completes) ``` ### Docker Compose Topology (Development) ``` +---------------------------------------------------+ | docker-compose.yml | | | | +-------------+ +-------------------+ | | | app | | db | | | | :3000 | -------> | :5432 | | | | | depends | | | | | Next.js dev | on | PostgreSQL 17 | | | | (webpack) | healthy | pgdata volume | | | +------+------+ +-------------------+ | | | | | bind mount | | ./src -> /app/src | | ./public -> /app/public | +---------------------------------------------------+ ``` ## Patterns | Do | Don't | When | | -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | | Use `output: "standalone"` in next.config.ts | Ship full `node_modules` in production image | Always in production | | Use `corepack enable pnpm` in Dockerfile | Install pnpm globally with `npm i -g pnpm` | pnpm projects (respects version pinning in package.json) | | Use `pnpm install --frozen-lockfile` | Use `pnpm install` without lockfile flag | CI and Docker builds (ensures reproducibility) | | Use `WATCHPACK_POLLING=true` for dev hot reload | Rely on native filesystem events in Docker | Development on macOS/Windows hosts | | Use `--turbopack` flag with `next dev` only when not in Docker | Use Turbopack in Docker dev containers | Docker dev (Turbopack ignores polled fs events) | | Mount only `./src` and `./public` as bind mounts | Mount the entire project root as a bind mount | Docker dev (avoids overwriting container node_modules) | | Use named volumes for `node_modules` and `.next` | Let bind mounts shadow container-built directories | Docker dev (prevents host/container version mismatches) | | Run as non-root user (`nextjs:nodejs`) in runner stage | Run as root in production containers | Always in production (limits blast radius) | | Copy only `standalone`, `static`, and `public` to runner | Copy the entire `.next` folder to runner | Production builds (minimizes image size) | | Set `HOSTNAME=0.0.0.0` in runner stage | Omit hostname binding | Docker networking (allows external access to container) | | Use `depends_on` with `condition: service_healthy` | Start app before database is ready | When app needs database at startup (e.g., Prisma migrations) | | Use `.dockerignore` to exclude `node_modules`, `.next`, `.git` | Let Docker context include everything | Always (speeds up builds, prevents leaking secrets) | ## Configuration ### next.config.ts | Option | Value | Why | | -------- | -------------- | ---------------------------------------------------------------------------------------------------------- | | `output` | `"standalone"` | Produces self-contained build with only required `node_modules` files; reduces image from ~1 GB to ~200 MB | ### Environment Variables | Variable | Value | Stage | Why | | ------------------------- | ------------------ | ------------- | ---------------------------------------------------------- | | `PNPM_HOME` | `/pnpm` | deps, build | Ensures pnpm store is in a known location for caching | | `PATH` | `$PNPM_HOME:$PATH` | deps, build | Makes pnpm binary available on PATH | | `NEXT_TELEMETRY_DISABLED` | `1` | build, runner | Disables Vercel telemetry in CI/production | | `NODE_ENV` | `production` | build, runner | Enables production optimizations in Next.js and React | | `HOSTNAME` | `0.0.0.0` | runner | Binds server to all interfaces so Docker can route traffic | | `PORT` | `3000` | runner | Explicit port for `server.js` (matches EXPOSE) | | `WATCHPACK_POLLING` | `true` | dev only | Forces webpack to poll for file changes in Docker volumes | ### .dockerignore | Entry | Why | | --------------------- | ----------------------------------------------------------- | | `node_modules` | Prevent host dependencies from overriding container install | | `.next` | Prevent stale build artifacts from entering build context | | `.git` | Reduce context size; not needed for builds | | `*.md` | Documentation not needed in image | | `.env*.local` | Prevent secrets from leaking into image layers | | `docker-compose*.yml` | Not needed inside the image | ## Dockerfile Stages ### Production (Multi-Stage) | Stage | Base Image | Purpose | Key Actions | | -------- | -------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deps` | `node:20-slim` | Install dependencies | `corepack enable pnpm`, copy `package.json` + `pnpm-lock.yaml`, run `pnpm install --frozen-lockfile` | | `build` | `node:20-slim` | Compile application | Copy `node_modules` from deps, copy source code, run `pnpm build` (triggers `next build` with standalone output) | | `runner` | `node:20-slim` | Run production server | Create `nextjs` user/group, copy `.next/standalone`, copy `.next/static` to `.next/standalone/.next/static`, copy `public` to `.next/standalone/public`, run `node server.js` | ### Stage Details | Concern | Implementation | Notes | | ------------- | ------------------------------------------------------------ | ------------------------------------------------------ | | Layer caching | Copy lockfile before source code | Dependency layer is rebuilt only when lockfile changes | | Security | `RUN addgroup --system nodejs && adduser --system nextjs` | Non-root user in runner stage | | Permissions | `RUN chown -R nextjs:nodejs /app/.next` | Ensures Next.js can write cache at runtime | | Final size | Only `standalone` + `static` + `public` in runner | No `node_modules` directory, no build tools | | Health check | `HEALTHCHECK CMD curl -f http://localhost:3000/ \|\| exit 1` | Optional; useful for orchestrators | ### Base Image Comparison | Image | Size | Pros | Cons | Recommendation | | ------------------------------------- | ------ | ----------------------------------------- | -------------------------------------------------------- | --------------------------- | | `node:20-alpine` | ~50 MB | Smallest, popular | Uses musl libc; can break native modules (bcrypt, sharp) | Use if no native deps | | `node:20-slim` | ~80 MB | Debian-based (glibc), broad compatibility | Slightly larger than Alpine | Default choice for Next.js | | `node:20-bookworm-slim` | ~80 MB | Same as slim, pinned Debian release | None significant | Equivalent to slim | | `gcr.io/distroless/nodejs20-debian12` | ~40 MB | Minimal attack surface, no shell | No shell for debugging, no package manager | Advanced: runner stage only | ## Docker Compose Services ### Development | Service | Image / Build | Ports | Volumes | Environment | Purpose | | ------- | ------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------- | | `app` | `build: .` (dev Dockerfile or target) | `3000:3000` | `./src:/app/src`, `./public:/app/public`, `node_modules:/app/node_modules`, `next-cache:/app/.next` | `NODE_ENV=development`, `WATCHPACK_POLLING=true`, `DATABASE_URL=postgres://...` | Dev server with hot reload | | `db` | `postgres:17-alpine` | `5432:5432` | `pgdata:/var/lib/postgresql/data` | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Development database | ### Production | Service | Image / Build | Ports | Volumes | Environment | Purpose | | ------- | --------------------------------------- | -------------------------- | --------------------------------- | ---------------------------------------------------- | ------------------- | | `app` | `build: .` (multi-stage, runner target) | `3000:3000` | None (stateless) | `NODE_ENV=production`, `DATABASE_URL=postgres://...` | Production server | | `db` | `postgres:17-alpine` | `5432:5432` (or unexposed) | `pgdata:/var/lib/postgresql/data` | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Production database | ### PostgreSQL Health Check | Parameter | Value | Why | | ---------- | ----------------------------------------- | --------------------------------------------------- | | `test` | `["CMD-SHELL", "pg_isready -U postgres"]` | Lightweight check that Postgres accepts connections | | `interval` | `5s` | Check every 5 seconds | | `timeout` | `3s` | Fail check if no response in 3 seconds | | `retries` | `10` | Mark unhealthy after 10 consecutive failures | ### Named Volumes | Volume | Mount Point | Why | | -------------- | -------------------------- | --------------------------------------------------------------- | | `pgdata` | `/var/lib/postgresql/data` | Persist database between container restarts | | `node_modules` | `/app/node_modules` | Prevent host bind mount from shadowing container-installed deps | | `next-cache` | `/app/.next` | Preserve build cache between dev restarts | ## Hot Reload Checklist (Development) | Step | Detail | | --------------------------------------- | ---------------------------------------------------------------- | | 1. Set `WATCHPACK_POLLING=true` | Forces webpack to poll for changes instead of relying on inotify | | 2. Bind mount only source directories | `./src:/app/src` and `./public:/app/public` (not entire project) | | 3. Use named volume for `node_modules` | Prevents host `node_modules` from overwriting container deps | | 4. Use named volume for `.next` | Prevents stale build cache conflicts | | 5. Use `next dev` without `--turbopack` | Turbopack does not detect polled filesystem events in Docker | | 6. Expose WebSocket port if needed | HMR uses WebSocket; ensure port 3000 is forwarded | ## Common Issues | Problem | Cause | Fix | | ---------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------- | | Hot reload not working in Docker | Missing `WATCHPACK_POLLING=true` or using Turbopack | Set env var; use webpack for dev in Docker | | `pnpm: not found` in Dockerfile | Corepack not enabled | Add `RUN corepack enable pnpm` before pnpm commands | | Image is 1+ GB | Not using standalone output | Set `output: "standalone"` in next.config.ts | | App crashes with EACCES | Running as non-root without proper ownership | `chown nextjs:nodejs /app/.next` before switching user | | Container can't reach database | Using `localhost` instead of service name | Use `db` (service name) as PostgreSQL host in `DATABASE_URL` | | `sharp` module fails on Alpine | musl libc incompatibility | Use `node:20-slim` (Debian) or install `sharp` platform-specific build | | Corepack signature verification error | Outdated corepack in base image | Add `RUN corepack prepare pnpm@ --activate` | | Static assets (CSS/images) missing after build | Forgot to copy `public` and `.next/static` to runner | Copy both directories alongside `standalone` in final stage | ## Sources - [Next.js Official Deployment Docs](https://nextjs.org/docs/app/getting-started/deploying) - 2025 - [Next.js output: standalone Configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/output) - 2025 - [Next.js 16 Release Blog Post](https://nextjs.org/blog/next-16) - October 2025 - [Next.js 16.1 Release Blog Post](https://nextjs.org/blog/next-16-1) - December 2025 - [Optimizing Next.js Docker Builds with PNPM, Corepack, and Standalone Mode](https://htalbot.dev/posts/build-nextjs-standalone-docker) - 2025 - [Dockerizing Next.js 15 with pnpm for Production](https://medium.com/@she11fish/dockerizing-next-js-15-application-with-pnpm-for-production-39c841ce8323) - 2025 - [Best Next.js Docker Compose Hot-Reload Setup](https://medium.com/@elifront/best-next-js-docker-compose-hot-reload-production-ready-docker-setup-28a9125ba1dc) - 2025 - [How to Configure Next.js with Docker](https://oneuptime.com/blog/post/2026-01-24-nextjs-docker-configuration/view) - January 2026 - [pnpm Docker Documentation](https://pnpm.io/next/docker) - 2025 - [How to use Prisma in Docker](https://www.prisma.io/docs/guides/docker) - 2025 - [Security Advice for Self-Hosting Next.js in Docker](https://blog.arcjet.com/security-advice-for-self-hosting-next-js-in-docker/) - 2025 - [Node.js Docker Optimization 2025: Multi-Stage Builds](https://markaicode.com/nodejs-docker-optimization-2025/) - 2025 - [Container Images Guide: Alpine, Slim, Distroless](https://www.ykira.com/blog/container-images-guide) - 2025 - [Choosing the Best Node.js Docker Image (Snyk)](https://snyk.io/blog/choosing-the-best-node-js-docker-image/) - 2025 - [Turbopack Polling Feature Issue #80665](https://github.com/vercel/next.js/issues/80665) - 2025 - [Docker Compose Watch + Turbopack Issue #12827](https://github.com/docker/compose/issues/12827) - 2025