docker compose cheatsheet.

Basic compose.yml

services:
  web:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgres://postgres:x@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
  
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: x
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

volumes:
  postgres_data:

Commands

docker compose up                    # foreground
docker compose up -d                 # detached
docker compose up --build            # rebuild
docker compose up web                # specific service
docker compose down                  # stop + remove
docker compose down -v               # also remove volumes (DANGER)
docker compose logs -f web
docker compose exec web bash
docker compose ps
docker compose restart web
docker compose pull
docker compose config                # validate + render

build

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
      args:
        VERSION: "1.0"
      target: dev                    # multi-stage stop

environment

services:
  web:
    environment:
      - DATABASE_URL=postgres://...
      - DEBUG=1
    env_file: .env
    env_file:
      - .env
      - .env.local

ports

ports:
  - "8000:8000"                      # host:container
  - "127.0.0.1:8000:8000"            # bind specific
  - "8000-8010:8000-8010"            # range

volumes

services:
  web:
    volumes:
      - .:/app                       # bind mount
      - /app/node_modules            # anonymous (preserve from image)
      - data:/var/lib/data           # named volume

volumes:
  data:
  data2:
    driver: local
    driver_opts:
      type: nfs
      o: addr=192.168.1.10
      device: ":/path"

networks

services:
  web:
    networks: [backend, frontend]
  db:
    networks: [backend]
  proxy:
    networks: [frontend]

networks:
  backend:
  frontend:

Services on same network resolve each other by service name. By default, all in one network.

depends_on

services:
  web:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

Conditions: service_started, service_healthy, service_completed_successfully.

restart

services:
  web:
    restart: unless-stopped

Options: no, always, on-failure, unless-stopped.

healthcheck

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

resource limits

services:
  web:
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 512M
        reservations:
          memory: 256M

profiles (optional services)

services:
  web:
    image: web:latest
  
  worker:
    image: worker:latest
    profiles: ["full"]
  
  monitoring:
    image: prom/prometheus
    profiles: ["monitoring"]
docker compose up                    # just web
docker compose --profile full up     # web + worker
docker compose --profile full --profile monitoring up

override files

# compose.yml + compose.override.yml auto-merged
docker compose up

# Specific files
docker compose -f compose.yml -f compose.prod.yml up
# compose.override.yml (dev defaults)
services:
  web:
    volumes: [".:/app"]              # hot-reload code
    environment:
      DEBUG: "1"

scale

docker compose up --scale web=3
services:
  web:
    deploy:
      replicas: 3

exec / run

docker compose exec web bash         # in running container
docker compose run --rm web pytest   # new ephemeral container
docker compose run --rm web sh -c "python manage.py migrate"

ENV substitution

services:
  web:
    image: myapp:${TAG:-latest}
    ports:
      - "${PORT:-8000}:8000"
TAG=v1.0 PORT=9000 docker compose up
# Or in .env (auto-loaded)

extends (reuse)

services:
  base:
    image: python:3.13
    environment:
      TZ: UTC
  
  web:
    extends:
      service: base
    command: gunicorn config.wsgi

logging

services:
  web:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

secrets (Swarm or Compose)

services:
  web:
    secrets:
      - db_password

secrets:
  db_password:
    file: ./db_password.txt

In container: /run/secrets/db_password.

Common patterns

Web + worker + db

services:
  web:
    build: .
    command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
    depends_on: [db, redis]
  
  worker:
    build: .
    command: celery -A config worker -l info
    depends_on: [db, redis]
  
  beat:
    build: .
    command: celery -A config beat -l info
    depends_on: [db, redis]
  
  db:
    image: postgres:16
  redis:
    image: redis:7-alpine

Reverse proxy

services:
  proxy:
    image: nginx:alpine
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./certs:/etc/nginx/certs
    depends_on: [web]

Common mistakes

  • depends_on without condition: service_healthy → app starts before DB.
  • Sharing volumes across services without coordination.
  • Hard-coding ports for multi-instance → conflict.
  • Compose v1 (docker-compose) syntax vs v2 (docker compose) — minor differences.
  • Forgetting --build after Dockerfile change.

Read this next

If you want my compose templates (web/worker/db/redis/nginx), they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .