Skip to content
Codeloom
Docker

Docker Compose Patterns for Production

Production-ready Docker Compose patterns: override files, healthchecks, resource limits, logging, and secrets management.

·5 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • How to structure Compose files for dev, staging, and production
  • Resource limits, logging, and restart policies for production
  • Secrets management and environment variable patterns

Prerequisites

Docker Compose is often seen as a development tool, but with the right patterns it works well for single-host production deployments, staging environments, and CI pipelines. This article covers the patterns that separate a toy docker-compose.yml from a production-grade one.

Override Files for Environment Separation

The foundation of production Compose is the override file pattern. Keep a base file with shared configuration and layer environment-specific settings on top.

docker-compose.yml — the base:

services:
  api:
    build:
      context: .
      target: production
    environment:
      - NODE_ENV=production
    networks:
      - backend
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

networks:
  backend:

docker-compose.override.yml — development overrides (loaded automatically):

services:
  api:
    build:
      target: development
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - DEBUG=api:*

  db:
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_PASSWORD=devpassword

docker-compose.prod.yml — production overrides:

services:
  api:
    image: registry.example.com/myapp:${APP_VERSION}
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.5"
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    restart: unless-stopped
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Run with explicit file selection:

# Development (uses override automatically)
docker compose up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Resource Limits

Always set resource limits in production. Without them, a single container can consume all host memory and bring down everything else.

services:
  api:
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 256M

The limits cap what the container can use. The reservations guarantee minimum resources. If a container exceeds its memory limit, Docker kills it with an OOM error.

For Java applications, align JVM heap settings with container limits:

services:
  java-api:
    image: myapp:latest
    environment:
      - JAVA_OPTS=-Xmx768m -Xms256m
    deploy:
      resources:
        limits:
          memory: 1G

Healthchecks and Dependency Ordering

Production containers must have healthchecks. Without them, Docker considers a container healthy the moment it starts, even if the application inside is still initializing.

services:
  api:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

The start_period gives the container time to initialize before Docker starts counting failed checks. The depends_on with condition: service_healthy ensures services start in the right order.

Logging Configuration

Docker’s default logging driver stores logs as JSON files with no size limit. In production, logs will fill your disk.

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
        tag: "{{.Name}}/{{.ID}}"

For centralized logging, forward to a log aggregator:

services:
  api:
    logging:
      driver: syslog
      options:
        syslog-address: "tcp://logserver:514"
        tag: "api"

Or run a sidecar log collector:

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

  log-forwarder:
    image: fluent/fluent-bit:latest
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
    depends_on:
      - api

Secrets Management

Never put passwords in environment variables in your Compose file. Use Docker secrets or external files.

File-based secrets:

services:
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Environment files for non-sensitive configuration:

services:
  api:
    env_file:
      - .env.common
      - .env.production

Keep .env files out of version control. Add them to .gitignore and .dockerignore.

Restart Policies

Production containers should restart automatically after failures:

services:
  api:
    restart: unless-stopped

The options are:

  • no — never restart (default)
  • on-failure — restart only on non-zero exit codes
  • always — restart no matter what
  • unless-stopped — restart unless explicitly stopped by the user

Use unless-stopped for most production services. Use on-failure for one-shot tasks that should retry on failure but not loop forever.

Networking Patterns

Isolate services into separate networks. Only expose what needs to be exposed:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    networks:
      - frontend

  api:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend

  redis:
    networks:
      - backend

networks:
  frontend:
  backend:
    internal: true

The internal: true flag on the backend network prevents containers on that network from reaching the internet. The database and Redis are only accessible to the API, never directly from the outside.

Named Volumes with Backup Patterns

Use named volumes for persistent data and back them up:

volumes:
  pgdata:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/postgres

Add a backup service:

services:
  db-backup:
    image: postgres:16-alpine
    volumes:
      - ./backups:/backups
    environment:
      - PGHOST=db
      - PGUSER=app
      - PGPASSWORD_FILE=/run/secrets/db_password
    entrypoint: >
      sh -c 'pg_dump -Fc app > /backups/backup_$$(date +%Y%m%d_%H%M%S).dump'
    depends_on:
      db:
        condition: service_healthy
    profiles:
      - backup
    secrets:
      - db_password

Run backups on demand with:

docker compose --profile backup run --rm db-backup

Production Checklist

Before deploying with Compose in production, verify:

  • All services have restart: unless-stopped
  • All services have healthchecks
  • Resource limits are set for CPU and memory
  • Logging has size limits configured
  • Secrets use files or external secret stores, not inline environment variables
  • Internal services are on isolated networks
  • Volumes use named volumes, not bind mounts for data
  • Images are pinned to specific tags or digests, not latest
  • A .env file is excluded from version control

Summary

Docker Compose works for production when you apply the right patterns: override files for environment separation, resource limits to prevent runaway containers, healthchecks for proper dependency ordering, structured logging, and secrets management. These patterns give you a reproducible, maintainable deployment that is easy for any team member to understand and operate.