Devops

Docker Best Practices for Production

Essential Docker practices for building secure, efficient, and maintainable containerized applications

February 5, 2024By Vikash Kumar7 min read
dockercontainersdevopsproductionsecurity

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

  1. Use multi-stage builds to reduce size
  2. Optimize layer caching
  3. Use specific, minimal base images
  4. Implement proper .dockerignore
  5. Scan for vulnerabilities regularly

Security

  1. Run as non-root user
  2. Use secrets management
  3. Keep base images updated
  4. Implement security scanning
  5. Follow principle of least privilege

Performance

  1. Set resource limits
  2. Implement health checks
  3. Use proper logging
  4. Monitor container metrics
  5. Optimize startup time

Operations

  1. Use container orchestration
  2. Implement proper CI/CD
  3. Monitor and log effectively
  4. Plan for scaling
  5. 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

Click any platform to share • Preview shows how your content will appear