You’ve built a slick Next.js app and now you want predictable builds, fast deploys, and identical behavior across dev and prod. Docker delivers that, when you set it up right. In this guide, you’ll containerize a Next.js 13/14 app with a production-grade Dockerfile, manage environment variables safely, wire up Docker Compose for local and cloud runs, integrate CI/CD for deterministic builds (including multi-arch), and sidestep the potholes that trip teams up.
Understand The Deployment Goals And Prerequisites
When To Use Docker For Next.js
Use Docker when you need:
- Consistent runtime across machines and clouds, especially when your team spans macOS (arm64) and Linux (amd64).
- Immutable, versioned images for quick rollbacks and blue/green or canary releases.
- Sidecar services (Postgres, Redis, Playwright) co-located via Compose or Kubernetes.
If you’re deploying to Vercel and don’t need custom OS dependencies or self-hosting, Docker may be overkill. But for AWS ECS, Kubernetes, Fly.io, Render, or on-prem, Dockerizing your Next.js app pays off.
Required Tools And Versions
- Docker Engine 24+ and BuildKit enabled (DOCKER_BUILDKIT=1 is default on recent Docker Desktop).
- docker buildx for multi-arch builds.
- Node.js 18 or 20 (Next.js 13/14 supports both: Node 20 is a great default).
- pnpm, npm, or Yarn, use one consistently. pnpm is fastest and cache-friendly.
- Git for deterministic versioning (commit SHA tags).
Project Structure Assumptions
We’ll assume a Next.js 13/14 project using the app/ directory:
- app/ for routes and server components
- next.config.js (or .mjs)
- package.json + lockfile (pnpm-lock.yaml or package-lock.json)
- public/ for static assets
- .env files for local dev only (never baked into images)
This guide works for pages/ apps too: only the build command and output handling differ slightly.
Write A Production-Ready Dockerfile
Choose Base Images And A Multi-Stage Strategy
You want small, secure images and reproducible builds. Use multi-stage builds:
- Builder stage: Node official image (node:20-alpine or node:20-bookworm-slim). Do installs and next build here.
- Runner stage: Minimal base (e.g., gcr.io/distroless/nodejs20 or node:20-alpine for simplicity) to serve the built app.
Next.js supports standalone output (output: ‘standalone’) which packages production server code and node_modules needed to run, perfect for tiny runtime images.
Example Dockerfile For Next.js 13/14
Below is a broadly compatible example using pnpm. Swap pnpm for npm/yarn if needed.
# syntax=docker/dockerfile:1.6
ARG NODE_VERSION=20
ARG PNPM_VERSION=9
FROM node:${NODE_VERSION}-alpine AS base
WORKDIR /app
FROM base AS deps
RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
FROM base AS builder
ENV NODE_ENV=production
RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Enable standalone output for a smaller runtime image
# next.config.js should set: module.exports = { output: 'standalone' }
RUN --mount=type=cache,target=.next/cache \
pnpm build
# Minimal runtime image (alpine for simplicity: distroless is smaller and more secure)
FROM node:${NODE_VERSION}-alpine AS runner
ENV NODE_ENV=production
WORKDIR /app
# Create non-root user
RUN addgroup -S nextjs && adduser -S nextjs -G nextjs
# Copy the standalone server and public assets
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
# Use Next.js standalone server
CMD ["node", "server.js"]
If you use npm, replace pnpm steps with npm ci and npm run build. For Yarn, use corepack with yarn set version stable and yarn install –immutable.
Optimize Layers, Caching, And .dockerignore
- Cache dependencies: copy only package.json + lockfile before running install: copy the rest later.
- Use BuildKit caches: –mount=type=cache for pnpm/yarn caches and .next/cache to speed up rebuilds.
- Maintain a tight .dockerignore: node_modules, .next (except when copying standalone output from builder), .git, .env*, tests, coverage, and local configs. This reduces context size and speeds builds.
A minimal .dockerignore might include:
.git
.next
node_modules
.env*
coverage
**/*.log
.DS_Store
Run As Non-Root With A Minimal Runtime Image
Don’t ship root. Create a user and chown only what’s necessary. For extra hardening, consider distroless (gcr.io/distroless/nodejs20) and add a small init (tini) if you need signal handling. If you must shell into the container, alpine is friendlier: distroless is more secure but shell-less.
Manage Environment Variables And Runtime Config
Build-Time Vs Runtime Variables
Next.js inlines variables prefixed with NEXT_PUBLIC_ at build time. Everything else is read at runtime by the Node server. Key rules:
- Secrets should never be baked at build time. Keep them runtime-only.
- If you need dynamic configuration without rebuilding images, avoid exposing values via NEXT_PUBLIC_ unless they’re safe to leak.
- For static site export, build-time variables are fixed: for server-rendered routes, you can read from process.env at runtime.
Injecting Secrets Securely
- Local: use docker compose and an .env file that isn’t committed. In production, rely on your orchestrator’s secret manager.
- Docker secrets: mount at runtime and read them in your entrypoint, exporting them as env vars for the Next.js server.
- Cloud managers: AWS SSM/Secrets Manager, GCP Secret Manager, or Kubernetes Secrets (backed by KMS). Rotate periodically and scope narrowly.
Never COPY .env into your image: it will leak in the layer history.
Configuring Next.js Features And Image Optimization
- next/image: If you run behind a reverse proxy or CDN, set images.domains or images.remotePatterns and, if offloading image resizing to the CDN, configure images.loader and images.path appropriately.
- basePath and assetPrefix: Behind a subpath or CDN, set basePath and assetPrefix so CSS/JS resolve correctly. Mismatches cause 404s.
- Standalone output: Ensure next.config sets output: ‘standalone’. It packages only the modules you need at runtime, shrinking your final image and cold starts.
Run Locally And In Production With Docker Compose
Example docker-compose.yml
A simple setup for local dev and prod-like runs:
version: "3.9"
services:
web:
build:
context: .
dockerfile: Dockerfile
image: my-next-app:local
ports:
- "3000:3000"
env_file:
- .env.local
environment:
NODE_ENV: production
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', r=>process.exit(r.statusCode===200?0:1))"]
interval: 10s
timeout: 3s
retries: 5
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
Add databases or caches as separate services and connect them on the default network.
Development Vs Production Profiles
- Dev: Mount source code and run next dev for HMR. That’s a different Dockerfile target or command, often a separate compose.override.yml. For pure dev images, skip standalone and keep dependencies editable.
- Prod: Use the built image with NODE_ENV=production and the standalone server. No bind mounts: just environment and networking.
Profiles in Compose (x-profiles or –profile) let you toggle services and commands cleanly.
Healthchecks, Ports, And Networking
Expose port 3000 by default, or set PORT in the container to match your reverse proxy. Add a healthcheck so orchestrators can restart unhealthy containers. For reverse proxies (Nginx, Traefik, AWS ALB):
- Pass X-Forwarded-* headers so Next.js can infer protocol and host.
- Increase timeouts for SSR routes that do heavy work.
- If using websockets or Next.js middleware, ensure proxy upgrades are allowed.
CI/CD: Build, Tag, And Deploy Images
Deterministic Builds And Caching In CI
Enable BuildKit in CI and cache aggressively:
- Use docker/build-push-action with cache-from/cache-to=type=gha for GitHub Actions.
- Keep lockfiles pinned: avoid npm install without a lock.
- Consider RUN –mount=type=cache for package managers and .next/cache to speed builds.
Tag images with: app:1.4.0, app:1.4, app:1, app:latest, and app:git-. That gives you stable channels and traceability.
Multi-Arch Images For amd64 And arm64
Your team likely uses M1/M2 Macs, but servers are amd64. Build multi-arch so the same tag works everywhere:
- docker buildx create –use
- docker buildx build –platform linux/amd64,linux/arm64 -t repo/app:TAG –push .
Beware native dependencies (sharp, bcrypt). Next.js often compiles these during install. Ensure they’re built for each platform in the builder stage or use prebuilt binaries.
Pushing To Registries And Release Strategy
- Registries: GHCR, Docker Hub, AWS ECR, GCP Artifact Registry. Authenticate via CI OIDC or short-lived tokens.
- Least privilege: write-only tokens in CI: read-only in runtime.
- Promotion: build once, promote by tag, don’t rebuild for staging vs prod. Use immutable git-SHA tags and mutable env tags (staging, prod) that point to the same digest. That’s how you keep builds deterministic and audits simple.
Troubleshooting Common Issues
Next Build Fails Or Runs Out Of Memory
- Increase memory in Docker Desktop or your CI runner. Next.js can spike during image optimization and route compilation.
- Add NODE_OPTIONS=–max-old-space-size=4096 in the builder stage for large repos.
- Ensure you’re using Node 18/20. Older runtimes can break swc.
- If sharp fails to build, install build deps in the builder stage (e.g., apk add –no-cache python3 make g++ on alpine) or switch the builder image to -bookworm-slim for better glibc compatibility.
404s, Asset Paths, And Reverse Proxy Pitfalls
- basePath and assetPrefix must align with the proxy path. If you serve under /app, set basePath: ‘/app’.
- If assets 404 in production but not locally, confirm that .next/static was copied and served by the standalone server, and that your proxy forwards that path.
- Set trust proxy headers. In Nginx, forward X-Forwarded-Proto and X-Forwarded-Host. In ALB, enable target group health checks on the correct path.
File Permissions And Running As A Non-Root User
- If the app tries to write to /app but you’re running as a non-root user, use a writable path like /tmp or chown the necessary directories during build.
- Avoid writing to the container filesystem in prod: prefer external storage (S3) or a mounted volume. Stateless containers roll easier.
Frequently Asked Questions
What’s a production-grade Next.js Dockerfile for Next.js 13/14?
Use a multi-stage build: install deps and run next build in a builder stage, then copy the .next/standalone output and static assets into a minimal runner (Alpine or distroless). Enable output: ‘standalone’ in next.config, run as a non-root user, and expose PORT 3000 with node server.js.
How should I manage environment variables when I Dockerize a Next.js app?
Treat NEXT_PUBLIC_* as build-time only and safe to expose; all secrets should be injected at runtime. Don’t COPY .env into the image. Use Compose env_file locally and secret managers (AWS SSM/Secrets Manager, GCP, or Kubernetes Secrets) in production. Avoid rebuilding images just to change config.
How do I use Docker Compose for Next.js in dev vs. production?
Use a dev profile that mounts source code and runs next dev for HMR. For production-like runs, build the image, set NODE_ENV=production, and run the standalone server without bind mounts. Add a healthcheck, expose port 3000, and attach databases or caches as separate services on the default network.
How can I build multi-arch images (arm64 and amd64) for a Next.js Dockerfile?
Enable docker buildx and BuildKit, then build with –platform linux/amd64,linux/arm64 and push a single tag. Watch for native modules (e.g., sharp, bcrypt): build them in the builder stage or use Debian-based images for glibc compatibility. Cache with cache-from/cache-to in CI to speed builds.
Should I use Alpine or Debian for Next.js Docker images?
Alpine yields smaller images but uses musl, which can trip native deps like sharp. Debian slim (bookworm-slim) is slightly larger but glibc-compatible and often builds native modules more reliably. A common pattern: Debian for builder, Alpine or distroless for the runtime, depending on your need for a shell.
What’s the best way to reduce image size and cold starts when Dockerizing a Next.js app?
Enable standalone output, use a minimal runner (distroless or Alpine), and copy only the runtime artifacts (.next/standalone, .next/static, public). Keep a strict .dockerignore, run installs with a lockfile, and cache dependencies and .next/cache. Avoid dev dependencies in runtime and run as a non-root user.

No responses yet