🐙 Docker Compose Quick Reference

Docker Compose cheat sheet / survival guide

Video: Docker Compose Quick Reference (DevOps video)
Prefer to watch? This short video runs this page's main commands in a real lab, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.
docker compose [options] <command> [service...]
docker compose up -d

Docker Compose runs a whole application made of several containers (a web app, a database, a cache) from one
YAML file. You describe the services, networks and volumes once, then start, stop and rebuild everything with a
single command. It is the easiest way to run multi-container setups on one machine: development, testing, home
labs and small servers. For a cluster of machines, look at Kubernetes instead.
This sheet uses the current Compose (v2, the "docker compose" command with a space). The old standalone
"docker-compose" command (with a hyphen) is Compose v1. It is retired, and most commands are the same.

Install and check

docker compose version                      # Compose v2 ships with Docker Desktop and current Docker Engine packages
sudo apt install docker-compose-plugin      # Debian/Ubuntu (with Docker's apt repository set up)
sudo dnf install docker-compose-plugin      # Fedora/RHEL family (with Docker's repository)
docker version                              # is the Docker daemon running and are you allowed to use it?
sudo usermod -aG docker $USER               # run docker without sudo (log out and back in). Members of the docker group are effectively root
If "docker compose" is not found but "docker-compose" is, you have the old v1. Upgrade to the plugin.

The compose file

Name it compose.yaml (preferred) or docker-compose.yml in your project folder. Compose finds it automatically.
The old top-level "version:" line is obsolete: leave it out. A minimal file:
services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    restart: unless-stopped
docker compose up -d                        # start it, then open http://localhost:8080
docker compose down                         # stop and remove it

Everyday commands

docker compose up                           # create and start everything (logs in the foreground, Ctrl+C stops)
docker compose up -d                        # detached: run in the background
docker compose up -d --build                # rebuild images first
docker compose up -d web                    # start only one service (and what it depends on)
docker compose up -d --force-recreate       # recreate containers even if nothing changed
docker compose up -d --remove-orphans       # also remove containers for services no longer in the file
docker compose up -d --scale worker=3       # run three copies of a service
docker compose up -d --wait                 # wait until services are running/healthy before returning
docker compose down                         # stop and remove containers and networks (volumes are KEPT)
docker compose down -v                      # ALSO delete named volumes: your database data is gone
docker compose down --rmi local             # also remove images built by Compose
docker compose stop                         # stop containers, keep them        docker compose start    start them again
docker compose restart                      # restart everything          docker compose restart web    one service
docker compose pause / unpause              # freeze and unfreeze
docker compose kill                         # force stop
docker compose rm                           # remove stopped containers

See what is happening

docker compose ps                           # containers of this project and their state
docker compose ps -a                        # include stopped ones
docker compose ls                           # every Compose project on this machine
docker compose logs                         # all logs
docker compose logs -f web                  # follow one service's logs
docker compose logs --tail 100 -t web       # last 100 lines, with timestamps
docker compose top                          # processes inside each container
docker compose images                       # images used
docker compose port web 80                  # which host port maps to container port 80
docker compose events                       # live events
docker compose config                       # validate and print the final merged file (great for debugging)
docker compose config --services            # list service names
docker compose config -q                    # quiet: only report errors

Run commands in containers

docker compose exec web sh                  # a shell inside a RUNNING container (bash if the image has it)
docker compose exec -u root web sh          # as root
docker compose exec db psql -U postgres     # run a client tool
docker compose run --rm web npm test        # start a NEW one-off container for a command, then remove it
docker compose run --rm --no-deps web sh    # without starting dependencies
docker compose cp web:/etc/nginx/nginx.conf ./nginx.conf     # copy files out (or in)
exec runs inside an existing container. run creates a new temporary container from the service's definition.

Build and images

docker compose build                        # build all services that have a "build:" section
docker compose build --no-cache web         # ignore the layer cache
docker compose build --pull                 # pull newer base images first
docker compose pull                         # download newer images for image: services
docker compose push                         # push built images to a registry
Update a stack to the latest images:  docker compose pull && docker compose up -d && docker image prune -f

Options that go before the command

-f compose.prod.yaml                        # use this file (repeat -f to merge several files)
-p myproject                                # project name (default: the folder name). Sets container, network and volume prefixes
--env-file .env.prod                        # use another environment file
--profile debug                             # also start services in that profile
--project-directory ./app                   # where to find the file and relative paths
--dry-run                                   # show what would happen without doing it (newer versions)
Example:  docker compose -f compose.yaml -f compose.prod.yaml -p shop up -d

A realistic multi-service example

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        APP_ENV: production
    image: myorg/web:1.0
    ports:
      - "127.0.0.1:8080:8000"              # only reachable from this machine
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy         # wait until the database reports healthy
      cache:
        condition: service_started
    restart: unless-stopped
    networks:
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - dbdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped
    networks:
      - backend                            # no "ports:" so the database is NOT exposed to the host

  cache:
    image: redis:7-alpine
    restart: unless-stopped
    networks:
      - backend

networks:
  backend:

volumes:
  dbdata:

Service options you will use most

image: nginx:1.27                           Image to run (pin a version, do not use latest)
build: ./app                                Build from a folder     (or the long form: context, dockerfile, args, target)
container_name: web                         Fixed name (prevents scaling: usually leave it out)
command: ["npm", "start"]                   Override the image's default command
entrypoint: ["/entrypoint.sh"]              Override the entrypoint
working_dir: /app                           Working directory
user: "1000:1000"                           Run as a non-root user
ports:                                      "HOST:CONTAINER". "8080:80", "127.0.0.1:8080:80", "8080:80/udp"
expose: ["3000"]                            Document a port for other containers only (does not publish it)
environment:                                A map (KEY: value) or a list (- KEY=value)
env_file: [.env, web.env]                   Load variables from files
volumes:                                    "name:/path" (named volume), "./dir:/path" (bind mount), add :ro for read-only
tmpfs: [/tmp]                               In-memory folder
networks: [frontend, backend]               Networks this service joins
depends_on:                                 Start order (see below)
restart: unless-stopped                     no | always | on-failure | unless-stopped
healthcheck:                                Command Docker runs to decide if the service is healthy
labels: { app: shop }                       Metadata (also used by reverse proxies like Traefik)
profiles: ["debug"]                         Only start when that profile is enabled
extra_hosts: ["host.docker.internal:host-gateway"]      Let the container reach the host machine
dns: [1.1.1.1]                              Custom DNS servers
logging:                                    driver: json-file, options: { max-size: "10m", max-file: "3" }
init: true                                  Use a tiny init process that reaps zombies
stop_grace_period: 30s                      Time to wait after SIGTERM before killing
pull_policy: always                         always | missing | never | build
platform: linux/amd64                       Force a CPU architecture

Networking (how containers find each other)

- Compose creates one default network per project. Every service joins it automatically
- Containers reach each other by SERVICE NAME as the hostname:  db:5432,  cache:6379,  web:8000
- Use the container's own port, not the published one:  db:5432 (not localhost:5432)
- "ports:" publishes to the host. Services that only other containers use need NO ports section
- "127.0.0.1:8080:80" keeps a published port private to the machine. "8080:80" opens it on all interfaces
- Extra networks isolate groups: put the database on a backend network that the reverse proxy cannot reach
- External network shared with other projects:  networks: { proxy: { external: true } }  (create it with: docker network create proxy)
docker compose exec web ping db             # test name resolution and connectivity from inside a container

Start order and health (depends_on does NOT wait by itself)

depends_on:
  db:
    condition: service_started              # only wait for the container to start (default)
  cache:
    condition: service_healthy              # wait until its healthcheck passes
  migrate:
    condition: service_completed_successfully    # wait for a one-shot job to finish OK
A container being "started" does not mean the database accepts connections. Add a healthcheck and use service_healthy.
Healthcheck examples:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]        (write $$ to keep a literal $)
  test: ["CMD", "redis-cli", "ping"]
  test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$${MYSQL_ROOT_PASSWORD}"]
Also give apps retry logic. Databases can restart at any time.

Volumes and data

volumes:
  - dbdata:/var/lib/postgresql/data         # named volume: managed by Docker, survives "down", best for databases
  - ./config:/etc/app:ro                    # bind mount: a folder from your machine (great for code and config in development)
  - /var/run/docker.sock:/var/run/docker.sock    # DANGER: gives the container control of Docker (root on the host). Avoid
docker volume ls                            docker volume inspect myproject_dbdata          docker volume rm myproject_dbdata
Named volumes get the project name as a prefix (myproject_dbdata). "docker compose down -v" deletes them.
Bind mount permissions: files created in a container may belong to root or another user on the host. Set user: "1000:1000".
Back up a named volume:
docker run --rm -v myproject_dbdata:/data -v "$PWD":/backup alpine tar czf /backup/dbdata.tgz -C /data .

Variables and secrets

Compose reads a file named .env in the project folder automatically and uses it for ${VARIABLE} substitution:
# .env
DB_PASSWORD=change-me
TAG=1.0
# in compose.yaml
image: myorg/web:${TAG}
${VAR}              Value of VAR (empty and a warning if unset)
${VAR:-default}     Use "default" if VAR is unset or empty
${VAR-default}      Use "default" only if VAR is unset
${VAR:?message}     Stop with an error message if VAR is unset or empty (great for required settings)
$$                  A literal dollar sign
Variables set in your shell override the .env file. Check the result with: docker compose config
env_file: sends the variables INTO the container. .env feeds ${...} substitution in the file itself (they are different things)
Secrets (better than putting passwords in environment variables):
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt         # keep this file out of git
Add .env and secrets/ to .gitignore. Commit an .env.example with fake values instead.

Multiple environments (override files)

compose.yaml                     The shared base
compose.override.yaml            Merged AUTOMATICALLY on top of it (good for local development)
compose.prod.yaml                Used only when you ask:  docker compose -f compose.yaml -f compose.prod.yaml up -d
# compose.override.yaml (development)
services:
  web:
    build: .
    volumes:
      - ./src:/app/src                      # live code
    environment:
      DEBUG: "1"
    ports:
      - "8000:8000"
Merge rules: later files override single values, and lists (ports, volumes) are combined. Use "docker compose config" to see the result.
Include other files:   include: [ ./db/compose.yaml ]      (Compose 2.20+)
Reuse blocks with extensions and anchors:
x-common: &common
  restart: unless-stopped
  logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }
services:
  web:
    <<: *common
    image: nginx:1.27

Profiles (optional services)

services:
  web: { image: nginx:1.27 }
  adminer:
    image: adminer
    profiles: ["debug"]                     # not started by default
docker compose up -d                        # starts web only
docker compose --profile debug up -d        # starts web and adminer
COMPOSE_PROFILES=debug docker compose up -d # same, with an environment variable

Resource limits and hardening

services:
  web:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          memory: 128M
    read_only: true                         # read-only root filesystem
    tmpfs: [/tmp]
    cap_drop: [ALL]                         # remove Linux capabilities
    security_opt:
      - no-new-privileges:true
    user: "1000:1000"
    pids_limit: 200
Limits stop one service from taking the whole machine. Check them with: docker stats

Development helpers

docker compose up --watch                   # sync or rebuild automatically when files change (needs a develop: watch: section)
services:
  web:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json
Or use a bind mount plus a dev server with hot reload, as in the override file above.
docker compose run --rm web sh             # a throwaway shell with all the service's settings
docker compose logs -f --tail 50           # follow everything

Update, back up and clean up

docker compose pull && docker compose up -d        # update to newer images (pinned tags only move when YOU change them)
docker compose down && docker compose up -d        # a clean restart
docker compose config > full.yaml                  # save the fully resolved configuration
docker system df                                   # what is using disk space
docker image prune -f                              # remove dangling images
docker compose down -v --rmi local --remove-orphans     # remove EVERYTHING from this project, including data. Be sure

Practical one-liners

# What is running in this project, and is it healthy?
docker compose ps

# Restart one service and watch it come back
docker compose restart web && docker compose logs -f web

# Rebuild and restart only the service you changed
docker compose up -d --build web

# Open a shell in the database and run a query
docker compose exec db psql -U app -d app -c "SELECT count(*) FROM users;"

# Dump a Postgres or MySQL database from a container
docker compose exec -T db pg_dump -U app app > backup.sql
docker compose exec -T db mysqldump -u root -p"$DB_ROOT_PASSWORD" app > backup.sql

# Restore it
docker compose exec -T db psql -U app app < backup.sql

# Which ports are published?
docker compose ps --format "table {{.Name}}\t{{.Ports}}"

# Scale a worker up and down
docker compose up -d --scale worker=5
docker compose up -d --scale worker=1

# Start fresh (data preserved)
docker compose down && docker compose up -d --build

# Wipe the database and start over (data deleted)
docker compose down -v && docker compose up -d

# Validate a file in CI
docker compose -f compose.yaml config -q && echo OK

# Start on boot: restart: unless-stopped in the file, and make sure Docker itself starts at boot
sudo systemctl enable --now docker

# Follow one line of output from every service, with names
docker compose logs -f --no-log-prefix web | grep -i error

Move from docker-compose (v1) to docker compose (v2)

- Replace "docker-compose" with "docker compose" (a space, not a hyphen). The file format is the same
- Delete the old "version:" line. v2 ignores it and prints a warning
- v2 names containers with hyphens (project-web-1) where v1 used underscores (project_web_1). Update scripts that use the old names
- Compose v1 has reached end of life and no longer receives fixes
- Add this alias if you like typing it the old way:  alias docker-compose='docker compose'

Common problems

"no configuration file provided: not found"        You are in the wrong folder, or the file is not named compose.yaml. Use -f, or cd to the project
"yaml: line N: mapping values are not allowed"     Indentation or a missing space after a colon. Use spaces (never tabs). Check with docker compose config
"services.web refers to undefined network/volume"  Declare it under the top-level networks: or volumes: section
"port is already allocated" / "address already in use"   Another program or container uses that host port. Change the left side of "8080:80", or stop the other one (ss -tulpn | grep 8080)
"Conflict. The container name is already in use"   A leftover container. docker compose down --remove-orphans, or docker rm -f NAME. Avoid container_name
Changes to the compose file have no effect         Run docker compose up -d again (it recreates changed services). Config in a bind-mounted file may need a restart
Code changes are not in the container              You changed code but the image was built earlier: docker compose up -d --build, or use a bind mount for development
"depends_on" does not wait for the database        It only waits for the container to START. Add a healthcheck and condition: service_healthy, and retry in the app
Old database password still works or new one fails The volume kept the old data. The env vars only apply the FIRST time a database is created. down -v resets it (deletes data)
Container cannot reach another by name             Different networks, or wrong hostname. Both must share a network. Use the service name and the container port
"connection refused" to localhost from a container localhost inside a container means the container itself. Use the service name (db) or host.docker.internal
Permission denied on a bind mount                  User ID mismatch. Set user:, fix ownership with chown, or use a named volume. On SELinux systems add :z or :Z to the volume
Variable is empty / "variable is not set" warning  Not in .env or your shell. Use ${VAR:?msg} for required ones. Check docker compose config
"$" in a value gets eaten                          Write $$ for a literal dollar sign
Service keeps restarting                           docker compose logs web. Often a crash, a bad command, or a missing setting. Temporarily remove restart: to see it fail
Out of disk space                                  docker system df, then docker image prune and docker builder prune. Old volumes and build cache add up
"the attribute version is obsolete"                Harmless warning. Delete the version: line
"docker: permission denied ... docker.sock"        You are not in the docker group (or need sudo), or the daemon is not running

Tips

    Use "docker compose config" whenever something looks wrong. It shows exactly what Compose will run.

    Pin image versions (postgres:16, not latest) so an update never surprises you.

    Do not publish database and cache ports. Let containers talk to them over the internal network, and bind anything you must publish to 127.0.0.1.

    Use healthchecks with depends_on condition: service_healthy, and give your app retry logic.

    Put data in named volumes, and remember that "down -v" deletes it. Back up volumes before you experiment.

    Keep secrets out of git: use .env (in .gitignore), an .env.example with fake values, and Compose secrets for sensitive values.

    Use compose.override.yaml for development conveniences and a separate prod file, instead of one file full of exceptions.

    Add restart: unless-stopped, log rotation (max-size, max-file) and resource limits for anything that runs on a server.

    Never mount /var/run/docker.sock into a container you do not fully trust. It is the same as handing over root.

    Compose is great for one machine. When you need many machines, self-healing and rolling updates, move to Kubernetes.