- 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>
199 lines
18 KiB
Markdown
199 lines
18 KiB
Markdown
# 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](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
|