- Next.js 16.1.6 with App Router, TypeScript strict, Tailwind CSS v4 - ESLint 9, Prettier with Tailwind class sorting plugin - Husky + lint-staged pre-commit hooks - Multi-stage Dockerfile (dev/build/production) with pnpm + Node 20 - docker-compose.yml with hot reload + PostgreSQL 17 - src/ directory structure (components, lib, hooks, types, payload) - .env.example template with all required variables - standalone output mode for production Docker builds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
18 KiB
18 KiB
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@<version> --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 - 2025
- Next.js output: standalone Configuration - 2025
- Next.js 16 Release Blog Post - October 2025
- Next.js 16.1 Release Blog Post - December 2025
- Optimizing Next.js Docker Builds with PNPM, Corepack, and Standalone Mode - 2025
- Dockerizing Next.js 15 with pnpm for Production - 2025
- Best Next.js Docker Compose Hot-Reload Setup - 2025
- How to Configure Next.js with Docker - January 2026
- pnpm Docker Documentation - 2025
- How to use Prisma in Docker - 2025
- Security Advice for Self-Hosting Next.js in Docker - 2025
- Node.js Docker Optimization 2025: Multi-Stage Builds - 2025
- Container Images Guide: Alpine, Slim, Distroless - 2025
- Choosing the Best Node.js Docker Image (Snyk) - 2025
- Turbopack Polling Feature Issue #80665 - 2025
- Docker Compose Watch + Turbopack Issue #12827 - 2025