Docker Best Practices for Next.js Applications
This document outlines best practices for Docker containerization of Next.js applications.
Multi-Stage Builds
Why Use Multi-Stage Builds?
Multi-stage builds create smaller, more secure images by:
- Separating build dependencies from runtime dependencies
- Reducing final image size (often 50-70% smaller)
- Improving security by excluding build tools from production
- Faster deployments due to smaller images
Stage Structure
Stage 1: Dependencies
- Install production dependencies only
- Use
npm cifor reproducible builds - Cache dependencies for faster rebuilds
Stage 2: Builder
- Copy dependencies from Stage 1
- Build the application
- Generate optimized production assets
Stage 3: Runner
- Copy only necessary files (public, .next)
- Run as non-root user
- Minimal runtime dependencies
Image Optimization
1. Base Image Selection
# ✅ Good: Alpine images (smallest)
FROM node:18-alpine
# ⚠️ Okay: Slim images (small)
FROM node:18-slim
# ❌ Avoid: Full images (large)
FROM node:18Comparison:
- Alpine: ~170MB
- Slim: ~250MB
- Full: ~900MB
2. Layer Caching
Order Dockerfile instructions from least to most frequently changing:
# 1. System dependencies (rarely changes)
RUN apk add --no-cache libc6-compat
# 2. Package files (changes occasionally)
COPY package.json package-lock.json ./
# 3. Dependencies (changes when package files change)
RUN npm ci
# 4. Source code (changes frequently)
COPY . .
# 5. Build (changes with source)
RUN npm run build3. .dockerignore
Always include a .dockerignore file to exclude:
node_modules/.next/.git/*.log- Development files
- Documentation
Benefits:
- Faster builds (less data to copy)
- Smaller images
- Better security (no secrets)
Security Best Practices
1. Run as Non-Root User
# Create user and group
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Change ownership
RUN chown -R nextjs:nodejs /app
# Switch to non-root user
USER nextjsWhy: Root users can compromise the host system if container is breached.
2. Use Specific Image Tags
# ✅ Good: Specific version
FROM node:18.17.0-alpine
# ❌ Bad: Latest (unpredictable)
FROM node:latest3. Scan for Vulnerabilities
# Scan image for vulnerabilities
docker scan nextjs-app:latest
# Use Trivy
trivy image nextjs-app:latest4. Minimize Attack Surface
- Use minimal base images (Alpine)
- Include only necessary dependencies
- Remove package managers if not needed
- Disable unnecessary services
Performance Optimization
1. Build Cache
Use BuildKit for better caching:
# Enable BuildKit
export DOCKER_BUILDKIT=1
# Build with cache
docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t app:latest .2. Parallel Builds
# Use experimental syntax for parallel operations
# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS deps
RUN --mount=type=cache,target=/root/.npm \
npm ci3. Compression
Enable compression in Next.js:
// next.config.js
module.exports = {
compress: true,
}Environment Variables
Build-time vs Runtime
Build-time (--build-arg):
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENVRuntime (docker run -e):
docker run -e API_URL=https://api.example.com app:latestSecrets Management
# ❌ Never do this
ENV API_SECRET=my-secret-key
# ✅ Use secrets
docker run --env-file .env.production app:latest
# ✅ Or use Docker secrets (Swarm)
docker secret create api_key ./api_key.txtHealth Checks
Application Health Check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"Health Check Endpoint
// app/api/health/route.ts
export async function GET() {
return Response.json({ status: 'healthy' }, { status: 200 });
}Logging
Container Logs
# View logs
docker logs -f container-name
# Follow logs from multiple containers
docker-compose logs -f
# Last 100 lines
docker logs --tail 100 container-nameLogging Best Practices
- Log to STDOUT/STDERR: Docker captures these automatically
- Structured Logging: Use JSON format for easy parsing
- Log Levels: Use appropriate levels (error, warn, info, debug)
- Avoid Sensitive Data: Never log secrets or PII
Networking
Container Communication
# docker-compose.yml
networks:
app-network:
driver: bridge
services:
app:
networks:
- app-network
db:
networks:
- app-networkPort Mapping
# Single port
docker run -p 3000:3000 app:latest
# Multiple ports
docker run -p 3000:3000 -p 9229:9229 app:latest
# Random host port
docker run -p 3000 app:latestPersistent Data
Volumes
# Named volume (managed by Docker)
docker run -v app-data:/app/data app:latest
# Bind mount (host directory)
docker run -v $(pwd)/data:/app/data app:latest
# Anonymous volume
docker run -v /app/data app:latestVolume Best Practices
- Use named volumes for production data
- Use bind mounts for development (hot reload)
- Backup volumes regularly
- Set permissions correctly
Development vs Production
Development Configuration
# Dockerfile.development
FROM node:18-alpine
# Install all dependencies (including dev)
RUN npm install
# Enable hot reload
CMD ["npm", "run", "dev"]Production Configuration
# Dockerfile.production
FROM node:18-alpine AS runner
# Install production dependencies only
RUN npm ci --only=production
# Build and optimize
RUN npm run build
# Start production server
CMD ["node", "server.js"]CI/CD Integration
GitHub Actions
- name: Build Docker image
run: docker build -t app:${{ github.sha }} .
- name: Push to registry
run: docker push app:${{ github.sha }}Automated Testing
- name: Run tests in container
run: |
docker build -t app:test -f Dockerfile.test .
docker run app:test npm testMonitoring
Resource Limits
# Limit memory
docker run -m 512m app:latest
# Limit CPU
docker run --cpus=".5" app:latest
# Both
docker run -m 512m --cpus=".5" app:latestStats
# Real-time stats
docker stats
# Specific container
docker stats container-name
# No streaming
docker stats --no-streamTroubleshooting
Common Issues
Image too large
- Use multi-stage builds
- Use Alpine base images
- Add .dockerignore file
Slow builds
- Optimize layer caching
- Use BuildKit
- Parallelize operations
Container exits immediately
- Check logs:
docker logs container-name - Run interactively:
docker run -it app:latest sh - Check CMD/ENTRYPOINT
- Check logs:
Port already in use
- Find process:
lsof -i :3000 - Use different port:
-p 3001:3000 - Stop conflicting container
- Find process:
Size Comparison
Before Optimization
Repository Tag Size
nextjs-app latest 1.2GBAfter Optimization
Repository Tag Size
nextjs-app latest 180MBSavings: 85% reduction in image size
Checklist
Before deploying to production, ensure:
- ✅ Using multi-stage builds
- ✅ Running as non-root user
- ✅ Using specific image tags (not latest)
- ✅ .dockerignore file present
- ✅ Health checks configured
- ✅ Resource limits set
- ✅ Logging to STDOUT/STDERR
- ✅ Secrets not hardcoded
- ✅ Image scanned for vulnerabilities
- ✅ Tested in staging environment
Last Updated: November 2025 Version: 1.0.0