Toolmingo
Guides8 min read

Docker Compose Cheat Sheet: Every Command and Config You Need

A complete Docker Compose cheat sheet — docker-compose.yml anatomy, CLI commands, volumes, networks, environment variables, health checks, and production patterns. Copy-ready configs.

Docker Compose turns a handful of docker run commands into a single docker compose up. This reference covers everything from the compose.yml structure to networking, volumes, health checks, and production-ready patterns.

Compose V2 note: docker compose (space, no hyphen) is the current plugin. The old docker-compose (hyphen) standalone binary is deprecated. Both use the same YAML format.

Quick reference

The 22 commands that cover 95% of daily Compose work.

Command What it does
docker compose up Start all services (foreground)
docker compose up -d Start all services (detached/background)
docker compose up --build Rebuild images then start
docker compose down Stop and remove containers + networks
docker compose down -v Also remove named volumes
docker compose stop Stop containers (keep them)
docker compose start Start stopped containers
docker compose restart Restart all services
docker compose restart web Restart one service
docker compose ps List running services
docker compose logs View all logs
docker compose logs -f web Follow logs for one service
docker compose exec web bash Shell into running container
docker compose run web npm test Run one-off command in new container
docker compose build Build all images
docker compose build web Build one image
docker compose pull Pull latest images
docker compose config Validate and print resolved config
docker compose top Show running processes
docker compose events Stream lifecycle events
docker compose scale web=3 Scale a service (or use --scale)
docker compose cp web:/app/log.txt . Copy file from container

compose.yml anatomy

A complete example: web app + Postgres + Redis.

# compose.yml  (or docker-compose.yml — both work)
name: myapp                        # optional project name

services:
  web:
    build:
      context: .                   # Dockerfile location
      dockerfile: Dockerfile       # default; omit if using "Dockerfile"
      args:
        NODE_ENV: production
    image: myapp-web:latest        # tag the built image
    ports:
      - "3000:3000"                # host:container
    environment:
      DATABASE_URL: postgres://postgres:secret@db:5432/mydb
      REDIS_URL: redis://cache:6379
    env_file:
      - .env                       # loaded on top of environment:
    depends_on:
      db:
        condition: service_healthy # wait for health check
      cache:
        condition: service_started # just wait for start
    volumes:
      - ./src:/app/src             # bind mount for dev hot-reload
      - node_modules:/app/node_modules  # named volume (don't overwrite)
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

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

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    networks:
      - backend
    command: redis-server --appendonly yes  # override default CMD

volumes:
  pgdata:                          # named volume, managed by Docker
  redisdata:
  node_modules:

networks:
  backend:
    driver: bridge                 # default; explicit is clearer

CLI commands in depth

Starting services

# Start everything in background
docker compose up -d

# Start specific services only
docker compose up -d web cache

# Build before starting (when Dockerfile changed)
docker compose up -d --build

# Remove orphan containers (services removed from compose.yml)
docker compose up -d --remove-orphans

# Force recreate all containers
docker compose up -d --force-recreate

Stopping and cleaning up

# Stop and remove containers + default network
docker compose down

# Also remove named volumes (deletes DB data!)
docker compose down -v

# Also remove built images
docker compose down --rmi all

# Just stop, keep containers for inspection
docker compose stop

# Remove stopped containers
docker compose rm -f

Logs and debugging

# Tail logs for all services
docker compose logs -f

# Tail logs for one service, last 100 lines
docker compose logs -f --tail=100 web

# Shell into running service
docker compose exec web bash
docker compose exec db psql -U postgres mydb

# Run a one-off command (new container, exits after)
docker compose run --rm web node scripts/migrate.js

# Show all running processes inside containers
docker compose top

Scaling

# Scale web service to 3 replicas
docker compose up -d --scale web=3

# Check all replicas
docker compose ps

Scaling works best with ports using only the container port (e.g., - "3000") so Docker auto-assigns host ports. A load balancer (Nginx, Traefik) in front handles routing.

Environment variables

Three ways to pass env vars

services:
  web:
    # 1. Inline — visible in compose.yml (ok for non-secrets)
    environment:
      NODE_ENV: production
      PORT: "3000"

    # 2. From .env file (gitignored, per service)
    env_file:
      - .env
      - .env.local   # merged in order, later overrides earlier

    # 3. Pass through from host shell (value from your terminal)
    environment:
      - API_KEY       # no value = inherit from host

Variable substitution

Compose reads .env in the project root automatically for variable substitution:

# .env
POSTGRES_VERSION=16
APP_PORT=3000
# compose.yml
services:
  db:
    image: postgres:${POSTGRES_VERSION:-15}   # fallback to 15 if unset
  web:
    ports:
      - "${APP_PORT}:3000"
# Override at runtime
APP_PORT=8080 docker compose up -d

# Or with --env-file
docker compose --env-file .env.staging up -d

Volumes

Named volumes vs bind mounts

volumes:
  # Named volume — Docker manages location, persists across down/up
  pgdata:/var/lib/postgresql/data

  # Bind mount — maps host path to container path
  ./src:/app/src                   # relative to compose.yml

  # Read-only bind mount
  ./config/nginx.conf:/etc/nginx/nginx.conf:ro

  # tmpfs — in-memory, lost on restart (fast, good for tests)
  /tmp/cache:
    type: tmpfs
    tmpfs:
      size: 100m

Exclude node_modules from bind mount

services:
  web:
    volumes:
      - .:/app                     # bind mount entire project
      - /app/node_modules          # anonymous volume hides host's node_modules

Networks

Default behaviour

Compose creates one default network per project. Every service joins it and can reach other services by service name as hostname:

# Inside web container:
curl http://db:5432        # "db" resolves to the db service container
redis-cli -h cache         # "cache" resolves to the redis container

Custom networks

services:
  web:
    networks:
      - frontend
      - backend
  db:
    networks:
      - backend            # db is not reachable from the "frontend" network

networks:
  frontend:
  backend:
    internal: true         # no external internet access

Host network (Linux only)

services:
  web:
    network_mode: host     # container uses host's network stack directly

Health checks

Health checks let depends_on: condition: service_healthy work properly.

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s      # how often to check
      timeout: 5s        # max time for one check
      retries: 5         # fail after this many consecutive failures
      start_period: 30s  # grace period before failures count
  web:
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Profiles

Profiles let you start subsets of services.

services:
  web:
    # no profile = always started
  db:
    # no profile = always started
  mailhog:
    profiles: [dev]        # only starts when "dev" profile active
  swagger-ui:
    profiles: [dev, docs]

  worker:
    profiles: [worker]
# Start default services + dev profile
docker compose --profile dev up -d

# Multiple profiles
docker compose --profile dev --profile worker up -d

Production patterns

Pattern 1: override files for dev vs prod

# compose.yml           — shared base config
# compose.override.yml  — auto-merged in dev (bind mounts, ports)
# compose.prod.yml      — production overrides

# Dev (auto-merges compose.yml + compose.override.yml)
docker compose up -d

# Production
docker compose -f compose.yml -f compose.prod.yml up -d
# compose.prod.yml
services:
  web:
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"

Pattern 2: wait-for-it with depends_on

For services without a proper health check, use depends_on: condition: service_started and a startup probe inside the app, or use a sidecar script:

services:
  web:
    depends_on:
      db:
        condition: service_healthy
    command: ["./wait-for-it.sh", "db:5432", "--", "node", "server.js"]

Pattern 3: secrets (Docker Swarm / Compose v2)

services:
  web:
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt   # dev only
    # external: true                  # prod: managed by Swarm/Kubernetes

Common mistakes

Mistake What happens Fix
Using depends_on: without condition: service_healthy App starts before DB is ready — connection errors Add health check to dependency, use condition: service_healthy
Hardcoding secrets in compose.yml Secrets leak into version control Use .env (gitignored) or Docker secrets
Bind-mounting entire project over node_modules Container uses host's node_modules (wrong OS/arch) Add anonymous volume for node_modules: - /app/node_modules
Using version: top-level key Deprecated warning in Compose V2 Remove version: — it's ignored but noisy
docker compose down -v in production Deletes database volumes Never run -v on production; remove named volumes manually
Port conflicts with docker compose up Service fails to bind port Use docker compose ps and docker ps -a to find conflicting container
docker compose run without --rm Leaves stopped containers behind Always use docker compose run --rm for one-off commands

FAQ

docker compose up vs docker compose up --build — when to rebuild?
up uses the cached image. up --build rebuilds images from Dockerfile. Rebuild when you change Dockerfile, package.json (for npm install layer), or any COPYed file that affects the image (not bind-mounted source).

How do services find each other by name?
Compose creates a DNS entry per service on the shared network. web can reach db at db:5432 because Docker's internal DNS resolves service names. This only works on the same Compose-managed network.

docker compose exec vs docker compose run?
exec runs a command inside an already running container. run starts a new container from the service image, runs the command, then exits. Use exec for debugging running services; use run --rm for migrations or one-off scripts.

How do I view the final resolved config (variables substituted)?
docker compose config prints the merged, fully-resolved YAML. Useful to verify .env substitution and override file merges before deploying.

Can I use Compose for production?
Single-server production: yes, with restart: unless-stopped or restart: always. For multi-host or high availability you need Docker Swarm (docker stack deploy) or Kubernetes. Compose lacks built-in load balancing, rolling updates, and auto-healing across hosts.

How do I pass --build-arg in Compose?
Use build.args in compose.yml:

build:
  context: .
  args:
    NODE_VERSION: 20
    BUILD_DATE: "2026-07-13"

Or override at CLI: docker compose build --build-arg NODE_VERSION=22 web.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools