Overview
Docker production patterns for Node.js and Next.js applications. The gold standard is multi-stage builds with a non-root user, minimal image, and no secrets in layers.
Multi-Stage Build (Gold Standard)
# Stage 1: Dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Stage 3: Runner (minimal production image)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy only what's needed
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Security Hardening
# Non-root user (always)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Read-only filesystem (where possible)
# In docker-compose:
# read_only: true
# tmpfs:
# - /tmp
# - /var/run
# No new privileges
# docker run --security-opt no-new-privileges:true
# Specific capabilities only
# docker run --cap-drop ALL --cap-add NET_BIND_SERVICE
Secrets Management
Never put secrets in ENV or ARG build instructions — they appear in image history:
# BAD — secret in image history forever
ENV API_KEY=sk-abc123
# GOOD — runtime env var (not in image)
# Pass at runtime: docker run -e API_KEY=... my-image
# GOOD — Docker secrets (Swarm/Compose v3.1+)
# docker-compose.yml with secrets
services:
app:
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Docker Compose: Production
# docker-compose.prod.yml
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
target: runner
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
cpus: "1"
memory: 512M
reservations:
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: myapp
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
Docker Compose: Development Override
# docker-compose.override.yml (auto-merged with docker-compose.yml)
services:
app:
build:
target: builder
volumes:
- .:/app
- /app/node_modules
command: npm run dev
environment:
NODE_ENV: development
# Dev: uses docker-compose.yml + docker-compose.override.yml
docker compose up
# Prod: uses only docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d
.dockerignore (Essential)
node_modules
.next
.git
.env*
*.log
coverage
.DS_Store
README.md
docker-compose*.yml
Dockerfile*
.github
tests
Missing .dockerignore is the #1 cause of bloated images (node_modules copying into context).
Image Size Optimization
# Use alpine base
FROM node:20-alpine
# Combine RUN commands (fewer layers)
RUN apk add --no-cache curl && \
rm -rf /var/cache/apk/*
# Only install production deps in final stage
RUN npm ci --omit=dev && npm cache clean --force
# Use .dockerignore
# Remove dev files
RUN find . -name "*.test.*" -delete
BuildKit (Enable for All Builds)
# Enable BuildKit
DOCKER_BUILDKIT=1 docker build .
# Or set in Docker config
echo '{"features": {"buildkit": true}}' > /etc/docker/daemon.json
# Mount cache in builds (speeds up npm install across builds)
RUN --mount=type=cache,target=/root/.npm npm ci
Health Check Patterns
# HTTP health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1
# Process check (for services without HTTP)
HEALTHCHECK CMD pgrep -x node || exit 1
Common Commands
# Build
docker build -t my-app:latest .
docker build -t my-app:v1.2.3 --build-arg VERSION=1.2.3 .
# Run
docker run -d --name my-app -p 3000:3000 --env-file .env my-app:latest
# Inspect
docker logs my-app --follow
docker stats my-app
docker exec -it my-app sh
# Cleanup
docker system prune -af # remove all unused images/containers/networks
docker image prune -f # remove dangling images only
Cross-Platform Builds (M1/ARM)
# Build for linux/amd64 from Apple Silicon
docker buildx build --platform linux/amd64 -t my-app:latest --push .
# Multi-platform
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t my-app:latest \
--push .