Docker Best Practices for Production
Essential Docker practices for building secure, efficient, and maintainable containerized applications
Docker Best Practices for Production
Docker has revolutionized application deployment, but building production-ready containers requires following established best practices. This guide covers essential techniques for creating secure, efficient, and maintainable Docker images.
Dockerfile Optimization
Multi-Stage Builds
Use multi-stage builds to reduce image size and improve security:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# Copy only necessary files
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Layer Optimization
Optimize Docker layers for better caching:
# Bad: Changes to code invalidate package installation
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
# Good: Separate dependency installation from code
FROM node:18-alpine
WORKDIR /app
# Install dependencies first (cached unless package.json changes)
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy application code
COPY . .
# Build application
RUN npm run build
Minimize Image Size
# Use specific, minimal base images
FROM node:18-alpine # Instead of node:18
# Remove unnecessary packages
RUN apk add --no-cache \
python3 \
make \
g++ \
&& npm install \
&& apk del python3 make g++
# Use .dockerignore to exclude unnecessary files
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
coverage
.nyc_output
Security Best Practices
Non-Root User
Always run containers as non-root users:
# Create and use non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Or for Alpine
RUN addgroup -S appuser && adduser -S appuser -G appuser
USER appuser
Secrets Management
Never include secrets in Docker images:
# Bad: Secrets in environment variables
ENV DATABASE_PASSWORD=secret123
# Good: Use Docker secrets or external secret management
# Runtime: docker run -e DATABASE_PASSWORD_FILE=/run/secrets/db_password
Security Scanning
# Scan images for vulnerabilities
docker scout cves my-app:latest
# Use Trivy for comprehensive scanning
trivy image my-app:latest
# Integrate into CI/CD pipeline
docker build -t my-app:latest .
trivy image --exit-code 1 --severity HIGH,CRITICAL my-app:latest
Performance Optimization
Resource Limits
Set appropriate resource limits:
# docker-compose.yml
version: '3.8'
services:
app:
image: my-app:latest
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Health Checks
Implement proper health checks:
# Application health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
// Express.js health check endpoint
app.get('/health', (req, res) => {
// Check database connection
const dbStatus = database.isConnected() ? 'healthy' : 'unhealthy';
// Check external dependencies
const externalServices = {
redis: redis.ping() ? 'healthy' : 'unhealthy',
database: dbStatus
};
const isHealthy = Object.values(externalServices).every(status => status === 'healthy');
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
services: externalServices
});
});
Container Orchestration
Docker Compose for Development
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
depends_on:
- db
- redis
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Production Deployment
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: my-app:${VERSION:-latest}
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL_FILE=/run/secrets/database_url
secrets:
- database_url
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
secrets:
database_url:
external: true
Logging and Monitoring
Structured Logging
// Structured logging for containers
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
// Log with context
logger.info('User login', {
userId: user.id,
ip: req.ip,
userAgent: req.get('User-Agent')
});
Container Monitoring
# Monitoring stack with Prometheus and Grafana
version: '3.8'
services:
app:
image: my-app:latest
ports:
- "3000:3000"
- "9090:9090" # Metrics endpoint
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
grafana:
image: grafana/grafana:latest
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
volumes:
prometheus_data:
grafana_data:
CI/CD Integration
GitHub Actions
# .github/workflows/docker.yml
name: Docker Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
myapp:latest
myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Security scan
run: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latest
Build Optimization
# Dockerfile with build optimization
FROM node:18-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Dependencies stage
FROM base AS deps
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Build stage
FROM base AS builder
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM base AS runner
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Debugging and Troubleshooting
Container Debugging
# Debug running container
docker exec -it container_name /bin/sh
# Check container logs
docker logs -f container_name
# Inspect container configuration
docker inspect container_name
# Monitor container resources
docker stats container_name
# Debug networking
docker network ls
docker network inspect network_name
Development Tools
# Development Dockerfile with debugging tools
FROM node:18-alpine AS development
# Install debugging tools
RUN apk add --no-cache \
curl \
wget \
vim \
htop \
strace
WORKDIR /app
# Install nodemon for development
RUN npm install -g nodemon
COPY package*.json ./
RUN npm install
COPY . .
# Enable debugging
EXPOSE 3000 9229
CMD ["nodemon", "--inspect=0.0.0.0:9229", "app.js"]
Best Practices Summary
Image Building
- Use multi-stage builds to reduce size
- Optimize layer caching
- Use specific, minimal base images
- Implement proper .dockerignore
- Scan for vulnerabilities regularly
Security
- Run as non-root user
- Use secrets management
- Keep base images updated
- Implement security scanning
- Follow principle of least privilege
Performance
- Set resource limits
- Implement health checks
- Use proper logging
- Monitor container metrics
- Optimize startup time
Operations
- Use container orchestration
- Implement proper CI/CD
- Monitor and log effectively
- Plan for scaling
- Have debugging strategies
Conclusion
Following Docker best practices is crucial for building production-ready containerized applications. Focus on security, performance, and maintainability from the start, and continuously monitor and improve your containerization strategy.
Key takeaways:
- Use multi-stage builds for optimal image size
- Always run containers as non-root users
- Implement comprehensive health checks and monitoring
- Integrate security scanning into your CI/CD pipeline
- Design for scalability and debugging from day one
Share this article