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.