Skip to main content
Docker Compose A single docker run with a pile of flags is hard to maintain, and multiple containers more so. Docker Compose declares the whole app (services, networks, volumes) in one compose.yaml and brings it all up with one command.
The current version is docker compose (v2, a space, no hyphen), built into the Docker CLI. The old docker-compose (the Python v1) is EOL, no longer maintained, do not use it. A compose.yaml also does not need and should not include a version: top-level field; it is obsolete in the Compose Specification and only produces an obsolescence warning.
Click the keys in compose.yaml to see what each block does:

Top-level structure

name: myapp          # project name (optional)

services:            # required, the containers
  web:
    image: nginx
networks:            # optional, custom networks
volumes:             # optional, named volumes
configs:             # optional, non-sensitive config
secrets:             # optional, sensitive data

Common service keys

KeyPurpose
imagePull a prebuilt image (pin a tag, not latest)
buildBuild from a Dockerfile (context / dockerfile / args / target)
portsPublish ports, HOST:CONTAINER
environment / env_fileEnv vars (inline / from file)
volumesbind mounts and named volumes
depends_onStartup order (with condition: service_healthy)
healthcheckHealth-check command
restartRestart policy (no / always / on-failure / unless-stopped)
networksWhich networks to join
command / entrypointOverride the image’s default command / entry point
profilesGroup a service into a profile, off by default
deployResource limits and GPUs

Creating networks

docker compose up automatically creates a project-specific bridge network (named <project>_default) and joins every service. A service name is its DNS hostname on that network, so web reaches the database at db:5432, no IP needed. To isolate tiers, declare your own networks:
services:
  proxy:
    networks: [frontend]
  app:
    networks: [frontend, backend]
  db:
    networks: [backend]          # backend only, proxy cannot reach it

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
proxy and db are on different networks and cannot talk directly; only app touches both.

Container group (project) name

Compose ties the set together with a “project name” and uses it as a naming prefix:
ResourceNaming formatExample (project myapp, service web)
Container<project>-<service>-<index>myapp-web-1
Network<project>_<network>myapp_default
Volume<project>_<volume>myapp_db-data
Precedence (high to low): docker compose -p <name> > COMPOSE_PROJECT_NAME env var > top-level name: > the directory containing compose.yaml.

Environment configuration

FormUse for
environment:A few visible variables written inline, supports ${VAR} interpolation
env_file:Many variables, or secrets you keep out of the YAML and gitignore
The project .envInterpolation in compose.yaml itself (it does not auto-inject into the container)
Interpolation: ${VAR}, ${VAR:-default} (fall back when unset or empty), ${VAR:?msg} (error out when unset). In-container env var precedence (high to low): docker compose run -e > environment: > env_file: > host shell inheritance > Dockerfile ENV.

Using GPUs

The official way is to declare it under a service’s deploy.resources.reservations.devices (the host needs the NVIDIA Container Toolkit first):
services:
  trainer:
    image: nvidia/cuda:12.9.0-base-ubuntu22.04
    command: nvidia-smi
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all              # or count: 1 / device_ids: ['0','3']
              capabilities: [gpu]     # required, deployment errors without it

Performance and startup order

  • depends_on with healthcheck: use condition: service_healthy to wait until a dependency is actually ready, so the app does not race a database that is not up yet.
  • Resource limits: deploy.resources.limits cpus / memory also take effect in standalone (non-Swarm) mode.
  • pull_policy and build cache: control whether to pull every time and reuse build cache.
  • Startup: up -d (background), --build (rebuild first), --scale web=3 (run 3 copies), --wait (wait until everything is healthy).
services:
  app:
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:18
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

Volume and bind mount mapping

Compose’s volumes: handles both bind mounts and named volumes, distinguished by how the source is written (see Where data lives):
services:
  app:
    volumes:
      - ./src:/app/src              # bind mount: relative host path
      - ./config.yaml:/app/config.yaml:ro   # read-only
      - app-data:/var/lib/data      # named volume (declared at the top level)

volumes:
  app-data:
Use named volumes for data that must persist and bind mounts for live-syncing source. A path with no mount declared only writes to the container writable layer and is gone when the container is removed.

A complete compose.yaml

# compose.yaml — no version field needed or wanted (obsolete)
name: myapp

services:
  web:
    build:
      context: .
      target: production
    ports:
      - "8080:80"
    environment:
      DATABASE_URL: "postgresql://db:5432/${DB_NAME:-mydb}"
    env_file:
      - path: .env
        required: false
    volumes:
      - ./static:/app/static:ro
      - upload-data:/app/uploads
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    networks: [frontend, backend]

  db:
    image: postgres:18
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME:-mydb}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped
    networks: [backend]

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

volumes:
  db-data:
  upload-data:

Common docker compose commands

docker compose up -d            # bring the whole set up in the background
docker compose up -d --build    # rebuild then start
docker compose ps               # containers in this project
docker compose logs -f web      # follow the web service logs
docker compose exec web sh      # shell into the web service
docker compose config           # expand and validate the compose file (exit 0 on success)
docker compose build --no-cache # rebuild without cache
docker compose down             # stop and remove containers and networks
Bringing a compose.yaml up looks like this:
docker compose down -v deletes named volumes (the ones declared in the volumes: section), and database data often lives there: once gone, it is gone. To only stop services and keep data, use docker compose down without -v.

Next

Reference: docs.docker.com/reference/compose-file, Compose GPU support