Axil_website/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

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