Skip to main content
โšก Calmops

Docker Compose: Orchestrating Multi-Container Applications

Introduction

Docker Compose lets you define and run multi-container applications with a single YAML file. Instead of running docker run commands with many flags, you declare your entire stack โ€” app, database, cache, reverse proxy โ€” and start everything with docker compose up.

Prerequisites: Docker and Docker Compose installed. Basic Docker knowledge (see Docker Fundamentals).

Basic docker-compose.yml

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:password@db:5432/myapp
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data

volumes:
  postgres-data:
  redis-data:
# Start all services
docker compose up

# Start in background
docker compose up -d

# Stop all services
docker compose down

# Stop and remove volumes (careful โ€” deletes data)
docker compose down -v

Essential Commands

# Lifecycle
docker compose up -d              # start in background
docker compose down               # stop and remove containers
docker compose restart app        # restart specific service
docker compose stop               # stop without removing

# Logs
docker compose logs               # all services
docker compose logs app           # specific service
docker compose logs -f app        # follow logs
docker compose logs --tail=50 app # last 50 lines

# Exec
docker compose exec app sh        # shell in running container
docker compose exec db psql -U postgres myapp  # run command

# Build
docker compose build              # build all images
docker compose build app          # build specific service
docker compose build --no-cache   # ignore cache

# Status
docker compose ps                 # list containers
docker compose top                # running processes
docker compose port app 3000      # show mapped port

Networking

Services in the same Compose file can reach each other by service name:

services:
  app:
    # Can reach 'db' at hostname 'db', port 5432
    environment:
      DATABASE_URL: postgres://postgres:pass@db:5432/myapp
      # NOT localhost:5432 โ€” that's inside the app container

  db:
    image: postgres:16-alpine

Custom Networks

services:
  frontend:
    networks:
      - public
      - internal

  backend:
    networks:
      - internal

  db:
    networks:
      - internal
    # db is NOT accessible from frontend directly

networks:
  public:
    driver: bridge
  internal:
    driver: bridge
    internal: true  # no external access

Volumes

services:
  app:
    volumes:
      # Named volume (managed by Docker, persists across restarts)
      - app-data:/app/data

      # Bind mount (maps host path to container path)
      - ./config:/app/config:ro  # :ro = read-only

      # Anonymous volume (temporary, removed with container)
      - /app/tmp

  db:
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  app-data:
  postgres-data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /data/postgres  # store on specific host path

Environment Variables

services:
  app:
    # Inline values (avoid for secrets)
    environment:
      NODE_ENV: production
      PORT: 3000

    # Load from file
    env_file:
      - .env
      - .env.production  # overrides .env

    # Pass through from host environment
    environment:
      - AWS_ACCESS_KEY_ID    # value comes from host shell
      - AWS_SECRET_ACCESS_KEY
# .env file
DATABASE_URL=postgres://user:pass@db:5432/myapp
REDIS_URL=redis://redis:6379
SECRET_KEY=your-secret-key

Health Checks

Health checks prevent dependent services from starting before dependencies are ready:

services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
      interval: 10s      # check every 10s
      timeout: 5s        # fail if no response in 5s
      retries: 5         # mark unhealthy after 5 failures
      start_period: 30s  # grace period before checks start

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

  app:
    depends_on:
      db:
        condition: service_healthy   # wait for db to be healthy
      redis:
        condition: service_healthy

Development vs Production Configs

Use multiple Compose files to override settings per environment:

# docker-compose.yml (base)
services:
  app:
    build: .
    environment:
      NODE_ENV: production

  db:
    image: postgres:16-alpine
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:
# docker-compose.override.yml (development โ€” auto-loaded)
services:
  app:
    build:
      target: development
    volumes:
      - .:/app              # live code reload
      - /app/node_modules   # don't override node_modules
    environment:
      NODE_ENV: development
    ports:
      - "9229:9229"         # Node.js debugger port

  db:
    ports:
      - "5432:5432"         # expose DB port for local tools
# docker-compose.prod.yml (production)
services:
  app:
    image: registry.example.com/myapp:${VERSION}
    restart: unless-stopped
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
# Development (uses docker-compose.yml + docker-compose.override.yml automatically)
docker compose up

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

Complete Full-Stack Example

# docker-compose.yml โ€” Node.js + PostgreSQL + Redis + Nginx
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app
    restart: unless-stopped

  app:
    build:
      context: .
      target: production
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/myapp
      REDIS_URL: redis://redis:6379
      SECRET_KEY: ${SECRET_KEY}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped

volumes:
  postgres-data:
  redis-data:

Scaling Services

# Scale app to 3 instances (requires a load balancer like nginx)
docker compose up -d --scale app=3

# Check which ports are mapped
docker compose port app 3000

Troubleshooting

# Container won't start โ€” check logs
docker compose logs app

# Service can't reach another service
docker compose exec app ping db          # test DNS resolution
docker compose exec app nc -zv db 5432   # test TCP connection

# Database connection refused
docker compose exec db pg_isready -U postgres  # is postgres ready?
docker compose ps db                           # is it running?

# Volume permissions
docker compose exec app ls -la /app/data       # check ownership

# Rebuild after Dockerfile changes
docker compose build --no-cache app
docker compose up -d app

Resources

Comments