·12 min read

Docker Compose in Production: Best Practices for Multi-Service Deployments

Docker Compose simplifies multi-service deployments, but production use requires careful configuration. Learn security, performance, and reliability best practices.

DockerPodmanComposeProductionDevOps

Docker Compose is fantastic for development: define your services in a YAML file, run docker compose up, and everything starts. But using Compose in production requires more discipline. You need health checks, resource limits, proper networking, secret management, and restart policies.

This guide covers everything you need to run Docker Compose reliably in production environments.

Basic vs Production Compose Files

Development Compose (Simple)

version: '3'
services:
  web:
    build: .
    ports:
      - "80:80"
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: password123

This works for local development but is unsafe for production.

Production Compose (Hardened)

version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.prod
    ports:
      - "127.0.0.1:8080:80"
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    depends_on:
      db:
        condition: service_healthy
    networks:
      - frontend
      - backend
    env_file:
      - .env.production
    read_only: true
    tmpfs:
      - /tmp:size=100M
    user: "1000:1000"

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G

volumes:
  pgdata:
    driver: local

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

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

Let's break down each production improvement.

Health Checks: Detect Failures Automatically

Health checks let Docker determine if a service is actually working, not just running.

Basic Health Check

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Fields:

  • test: Command to run (must exit 0 for healthy, 1 for unhealthy)
  • interval: How often to check (default: 30s)
  • timeout: Max time for check to complete (default: 30s)
  • retries: Failed checks before marking unhealthy (default: 3)
  • start_period: Grace period for container startup (default: 0s)

Common Health Check Patterns

HTTP endpoint:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/health"]

Database connection:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]

Custom script:

healthcheck:
  test: ["CMD", "/app/healthcheck.sh"]

Process check:

healthcheck:
  test: ["CMD-SHELL", "pidof nginx || exit 1"]

Using Health Checks

Depend on healthy services:

services:
  api:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

Monitor health status:

docker compose ps
# Shows health status for each service

docker inspect --format='{{.State.Health.Status}}' <container>

Resource Limits: Prevent Resource Exhaustion

Without limits, a single container can consume all host resources.

CPU and Memory Limits

deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 512M
    reservations:
      cpus: '0.25'
      memory: 128M

Limits: Maximum resources the container can use
Reservations: Minimum resources guaranteed to the container

Find the Right Limits

  1. Monitor actual usage:

    docker stats
    
  2. Set limits slightly above peak usage:

    • If app uses 400MB RAM normally, set limit to 512MB
    • Add headroom for traffic spikes
  3. Test under load:

    # Simulate high traffic
    ab -n 10000 -c 100 http://localhost/
    
    # Monitor resource usage
    docker stats
    

OOM Kill Prevention

If a container exceeds its memory limit, it's killed (OOM killed). Prevent this:

deploy:
  resources:
    limits:
      memory: 512M

Monitor for OOM kills:

dmesg | grep -i "out of memory"
docker inspect <container> | grep OOMKilled

Restart Policies: Automatic Recovery

When containers crash, restart them automatically.

Available Policies

restart: no              # Never restart (default)
restart: on-failure      # Restart only if exit code != 0
restart: unless-stopped  # Always restart unless manually stopped
restart: always          # Always restart, even after daemon restart

Recommendation

restart: unless-stopped

This restarts containers after crashes and host reboots, but respects manual stops.

Exponential Backoff

Docker uses exponential backoff for restart delays:

  • First restart: immediate
  • Second restart: 100ms delay
  • Third restart: 200ms delay
  • Fourth restart: 400ms delay
  • Maximum delay: 1 minute

Secret Management: Secure Credentials

Never hardcode passwords in compose files.

Docker Secrets

Create a secret file:

mkdir -p secrets
echo "supersecretpassword" > secrets/db_password.txt
chmod 600 secrets/db_password.txt

Reference in compose:

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

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

The secret is mounted at /run/secrets/db_password inside the container.

Environment Files

For non-sensitive config, use .env files:

services:
  web:
    env_file:
      - .env.production
# .env.production
NODE_ENV=production
LOG_LEVEL=info
API_URL=https://api.example.com

Add to .gitignore:

# .gitignore
.env.production
secrets/

External Secret Managers

For enterprise deployments, integrate with:

  • HashiCorp Vault: Dynamic secrets, audit logging
  • AWS Secrets Manager: Cloud-native secret storage
  • Azure Key Vault: Microsoft cloud secrets
  • Google Secret Manager: GCP secret storage

Networking: Isolation and Security

Separate services into different networks for security.

Frontend and Backend Networks

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

Assign services:

services:
  web:
    networks:
      - frontend  # Accessible from outside
      - backend   # Can reach backend services
  
  api:
    networks:
      - backend   # Only accessible via web
  
  db:
    networks:
      - backend   # Only accessible via api

Traffic flow:

Internet → web (frontend+backend) → api (backend) → db (backend)

The internal: true flag prevents backend network from accessing the internet.

Port Binding

Bind to localhost unless external access is needed:

ports:
  - "127.0.0.1:8080:80"  # Localhost only

For public services, use a reverse proxy (Nginx, Traefik) instead of direct port exposure.

Volume Management: Persistent Data

Use named volumes for data that must persist.

Named Volumes

volumes:
  pgdata:
    driver: local

services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data

Named volumes are managed by Docker, stored in /var/lib/docker/volumes/.

Bind Mounts (Development Only)

volumes:
  - ./config:/etc/app/config:ro  # Read-only mount

Avoid bind mounts in production (path dependencies, permission issues).

Volume Backups

Backup named volumes:

docker run --rm \
  -v pgdata:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/pgdata-backup.tar.gz -C /source .

Restore:

docker run --rm \
  -v pgdata:/dest \
  -v $(pwd):/backup \
  alpine tar xzf /backup/pgdata-backup.tar.gz -C /dest

Logging: Capture and Rotate Logs

Prevent logs from filling up disk space.

Log Driver Configuration

services:
  web:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
  • max-size: Maximum size per log file (10 MB)
  • max-file: Maximum number of log files to keep (3)

Total disk usage: 30 MB per service.

View Logs

# All services
docker compose logs

# Specific service
docker compose logs web

# Follow logs
docker compose logs -f web

# Last 100 lines
docker compose logs --tail=100 web

Centralized Logging

Forward logs to external systems:

logging:
  driver: syslog
  options:
    syslog-address: "tcp://logs.company.com:514"
    tag: "myapp-{{.Name}}"

Deployment Workflow

Step 1: Validate Compose File

docker compose config
# Shows resolved configuration

Step 2: Build Images

docker compose build --no-cache

Step 3: Test Locally

docker compose up -d
docker compose ps
docker compose logs

Step 4: Deploy to Server

Upload compose file and secrets:

scp docker-compose.prod.yml user@server:/opt/myapp/
scp -r secrets/ user@server:/opt/myapp/

On server:

cd /opt/myapp
docker compose -f docker-compose.prod.yml up -d

Step 5: Verify Deployment

# Check all services running
docker compose ps

# Check health
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"

# Test endpoints
curl http://localhost:8080/health

Step 6: Update Strategy

Zero-downtime updates:

# Pull new images
docker compose pull

# Recreate services one at a time
docker compose up -d --no-deps --build web
docker compose up -d --no-deps --build api

# Remove old images
docker image prune -f

FoxTerm Compose Integration

FoxTerm simplifies Compose deployments.

Visual Stack Management

View all Compose stacks:

  • Service status and health
  • Resource usage per container
  • Port mappings
  • Volume mounts

One-Click Deploy

  1. Paste or upload docker-compose.yml
  2. Configure environment variables via UI
  3. Click "Deploy"
  4. FoxTerm uploads files, runs docker compose up -d, and shows real-time logs

Monitoring Dashboard

Monitor all services:

  • CPU/memory usage graphs
  • Log viewer with filtering
  • Health check status
  • Restart history

Stack Templates

Save common stacks as templates:

  • LAMP stack (Linux, Apache, MySQL, PHP)
  • MEAN stack (MongoDB, Express, Angular, Node)
  • Monitoring stack (Prometheus, Grafana, Alertmanager)

Deploy with one click.

Conclusion

Docker Compose in production requires attention to health checks, resource limits, secrets, networking, and logging. Apply these best practices, and you'll have reliable, secure multi-service deployments.

FoxTerm makes managing Compose stacks visual and straightforward, whether you're running one service or twenty.


FoxTerm Pro includes Docker Compose management. Download at foxterm.app.