Docker Compose for Production: A Practical Configuration Guide

Beyond docker-compose up

Docker Compose is not just a development tool. With the right configuration it handles production workloads cleanly — but the defaults are not production-ready. This guide covers the settings that matter.

---

Service Dependencies Done Right

depends_on alone does not wait for a service to be ready — only for it to have started. Use healthchecks to wait for readiness:

services:
  postgres:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U devops -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    image: myapp-backend
    depends_on:
      postgres:
        condition: service_healthy    # waits for healthcheck to pass

---

Environment Variables

services:
  backend:
    env_file:
      - .env            # load from file (gitignored)
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://user:${POSTGRES_PASSWORD}@postgres:5432/myapp
      # ${VAR} is substituted from .env or shell environment at compose-up time

Never commit secrets to docker-compose.yml. Keep them in .env and load via env_file.

---

Resource Limits

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

Without limits, a misbehaving container can consume all host memory and OOM-kill other services.

---

Logging

services:
  backend:
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"    # 100 MB max total per service

Default logging has no size limit — logs grow unbounded and fill the disk.

---

Restart Policy

services:
  backend:
    restart: unless-stopped   # restart on crash, stop on explicit docker stop

Use unless-stopped in production. always restarts even after a deliberate docker stop, which makes maintenance harder.

---

Multi-Environment Override Files

# Base config shared by all environments
docker-compose.yml

# Production overrides
docker-compose.prod.yml

# Run with both files merged
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Override file only needs to contain the differences:

# docker-compose.prod.yml
services:
  frontend:
    build:
      target: prod          # use the prod build stage
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

---

Volumes for Persistence

services:
  postgres:
    volumes:
      - postgres_data:/var/lib/postgresql/data   # named volume (managed by Docker)
      - ./backup:/backup:ro                       # bind mount (host path)

volumes:
  postgres_data:    # declare named volume

Named volumes survive docker compose down. Use docker compose down -v to also remove them (wipes data).

---

Networking

services:
  backend:
    networks:
      - app_net
      - infra_net

  frontend:
    networks:
      - app_net    # can reach backend, cannot reach infra_net

networks:
  app_net: {}
  infra_net:
    external: true   # created outside this compose file

Put each service only on the networks it needs. Avoid putting everything on the same network.

Practice Docker Compose in the ShellGenius Docker Labs — the compose-stack and network-microservices challenges use real multi-service scenarios.