Docker has become the standard for containerization, but running containers in production requires following best practices for security, performance, and reliability. This guide covers essential practices for production Docker deployments.
Image optimization
Use multi-stage builds
Reduce final image size by using multi-stage builds:
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs package*.json ./
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
Use minimal base images
Prefer Alpine or distroless images:
# Bad: Full OS image
FROM ubuntu:22.04
# Good: Minimal image
FROM node:18-alpine
# Better: Distroless (no shell, minimal attack surface)
FROM gcr.io/distroless/nodejs18-debian11
Layer optimization
Order Dockerfile instructions from least to most frequently changing:
# Bad: Dependencies copied after code
COPY . .
RUN npm install
# Good: Dependencies first (better caching)
COPY package*.json ./
RUN npm ci --only=production
COPY . .
Remove unnecessary files
Use .dockerignore to exclude files:
node_modules
npm-debug.log
.git
.gitignore
.env
.nyc_output
coverage
.DS_Store
*.md
Security best practices
Run as non-root user
Never run containers as root:
# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
# Switch to non-root user
USER appuser
Scan images for vulnerabilities
# Use Trivy
trivy image myapp:latest
# Use Docker Scout
docker scout quickview myapp:latest
# In CI/CD
- name: Scan image
run: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myapp:latest
Use specific image tags
Avoid latest tag in production:
# Bad
FROM node:latest
# Good
FROM node:18.17.0-alpine
Keep images updated
Regularly update base images and dependencies:
# Use specific versions
FROM node:18.17.0-alpine
# Update regularly
# Check for security updates monthly
Limit capabilities
Drop unnecessary Linux capabilities:
# docker-compose.yml
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only what's needed
Use secrets management
Never hardcode secrets:
# Bad
ENV DB_PASSWORD=secret123
# Good: Use Docker secrets or environment variables
# Pass at runtime
# Runtime
docker run -e DB_PASSWORD=$(cat /path/to/secret) myapp
Resource management
Set resource limits
Always set memory and CPU limits:
# docker-compose.yml
services:
app:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
Health checks
Implement proper health checks:
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
CMD curl -f http://localhost:3000/health || exit 1
Or in docker-compose:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Logging and monitoring
Use structured logging
# Configure logging driver
# In docker-compose.yml
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Centralize logs
# Use logging driver for centralized logging
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "app.logs"
Networking
Use custom networks
Isolate containers with custom networks:
# Create network
docker network create app-network
# Connect containers
docker run --network=app-network myapp
Avoid host network mode
# Bad: Exposes container to host network
network_mode: "host"
# Good: Use bridge network
networks:
- app-network
Data persistence
Use volumes for persistent data
volumes:
- app-data:/var/lib/app/data
volumes:
app-data:
driver: local
Avoid bind mounts in production
# Development: OK
volumes:
- ./data:/app/data
# Production: Use named volumes
volumes:
- app-data:/app/data
Build optimization
Leverage build cache
# Order matters for cache efficiency
# 1. Install dependencies (changes less frequently)
COPY package*.json ./
RUN npm ci
# 2. Copy source code (changes frequently)
COPY . .
RUN npm run build
Use BuildKit
Enable BuildKit for faster builds:
export DOCKER_BUILDKIT=1
docker build -t myapp .
Or in Dockerfile:
# syntax=docker/dockerfile:1.4
FROM node:18-alpine
# ... rest of Dockerfile
Production deployment
Use orchestration
For production, use orchestration tools:
- Docker Swarm: Built-in orchestration
- Kubernetes: Industry standard
- Nomad: HashiCorp’s orchestrator
Implement restart policies
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
Use init containers
For cleanup and initialization:
# docker-compose.yml
services:
app:
init: true # Uses tini as init system
Monitoring and observability
Expose metrics
# Expose metrics endpoint
EXPOSE 9090
# Application should expose /metrics endpoint
Use labels
Add metadata with labels:
LABEL maintainer="[email protected]"
LABEL version="1.0.0"
LABEL description="Production application"
Best practices checklist
Image optimization
- Use multi-stage builds
- Use minimal base images (Alpine/distroless)
- Optimize layer ordering
- Use .dockerignore
- Remove unnecessary files
Security
- Run as non-root user
- Scan images for vulnerabilities
- Use specific image tags
- Keep images updated
- Limit capabilities
- Use secrets management
Resource management
- Set memory limits
- Set CPU limits
- Implement health checks
- Configure restart policies
Operations
- Use structured logging
- Centralize logs
- Use custom networks
- Use volumes for persistence
- Implement monitoring
Conclusion
Following Docker best practices is essential for production deployments. Focus on security, performance, and reliability. Regularly review and update your Docker configurations, scan for vulnerabilities, and monitor container health.
Remember: Security and reliability should never be compromised for convenience. Take the time to implement these practices from the start.
