Axil_Accountants/docs/guides/01-nextjs-docker-setup.md
Vadym Samoilenko e373c2b46c feat: project setup & repository (Feature 1)
- 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>
2026-02-21 22:43:53 +00:00

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