Cheat Sheet

Docker Compose Commands

Docker Compose runs a multi-container app from one compose.yaml file. This sheet covers both halves of the job: the docker compose commands you type, and the YAML keys you write, from ports and volumes to networks, environment variables and healthchecks.

Last updated September 11, 2026

Compose v2 is a Docker CLI plugin, so the command is with a space. The hyphenated was v1, written in Python, and has been retired. Every command below is v2.

docker compose vs docker-compose

Most tutorials written before 2023 use the hyphen. Translating them is usually one keystroke, but a few things moved.

v1 (retired)v2 (current)Notes
Drop the hyphen; the flags are the same
v2 still reads the old names, but looks for first
at the topdelete the lineObsolete in v2 and ignored, and it prints a warning until you remove it
Container names use hyphens now. Networks and volumes keep the underscore
v2 builds with BuildKit by default, so build errors now read
not availableEvery Compose project on the host, not just this one
not availableBlock until every service is running and healthy
not availableSync or rebuild on file change

Two error messages come out of this split, and they mean opposite things. The third case is the one that confuses people most, because the old command works:

MessageCause
The v2 plugin is not installed. See Install and Version
You typed the v1 name and no wrapper is installed. Use the space
prints The old name is a wrapper around v2. Harmless, and old scripts keep working

If scripts on the machine still call the old name, an alias covers interactive shells:

alias docker-compose='docker compose'

Aliases are not read by shell scripts, so anything running under or cron needs the command edited, or a two-line wrapper in .

Start and Stop

Run these from the directory that holds , or point at it with .

CommandWhat it does
Create and start every service, attached to the logs (Ctrl+C stops)
Start in the background
Rebuild images that have a section first
Only one service (and what it )
Only one service, without its dependencies
Recreate containers even if nothing changed
Pull newer images before starting
Return only when services are running and healthy
Also remove containers for services no longer in the file
Run three copies of a service
Stop containers, keep them and their networks
Stop one service
Start stopped containers again
Restart every service
Restart one service (does not pick up file changes; use )
Stop and remove containers and networks
Also remove named volumes declared in the file. This deletes data
Also remove the images
Also remove leftover containers from removed services
/ Freeze and thaw
SIGKILL a service ( for another signal)

is also how you apply changes: it compares the file against the running containers and recreates only the ones whose configuration changed. does not do that.

Status and Logs

CommandWhat it does
Containers for this project, with state and ports
Include stopped ones
Just the service names
Every Compose project running on this host
Logs from every service
Follow
Follow one service
Last 100 lines, then follow
Only the last ten minutes
With timestamps
Processes in every container
Live CPU and memory per container
Stream container events for the project
Which image and tag each service runs
Which host port maps to container port 80

Exec and Run

runs inside an existing container. starts a new one from the service definition, which is the right tool for one-off jobs like migrations.

CommandWhat it does
Shell in the running container ( where the image has it)
As root
Run one command
No TTY, for scripts and pipes
With an extra variable
In a specific directory
New throwaway container, with the service's volumes, env and network
Run a command in one
Without starting the services it depends on
Publish the service's ports too ( skips them by default)
Override an environment variable
Ignore the image's entrypoint

Without , every leaves a stopped container behind. shows them and clears them.

Build and Pull

CommandWhat it does
Build every service with a section
Build one
Rebuild every layer
Pull a newer base image first
Pass a build argument
Pull the latest image for every service
Pull one
Keep going if one image is unavailable
Push built images to their registry
Create containers without starting them
Sync changed files into running containers, or rebuild, on save

Update a running stack to newer images:

docker compose pull
docker compose up -d

recreates only the containers whose image changed. For services you build yourself, does the same thing in one step. The Dockerfile generator is a quick way to get the side started.

needs a block on the service, and it replaces the usual edit, rebuild, restart loop in development:

    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json

copies changed files into the running container, rebuilds the image and recreates the container, and copies then restarts the process.

Files, Projects and Profiles

CommandWhat it does
Use a specific file
Layer files; later ones override earlier ones
Set the project name (default is the directory name)
Variables for substitution in the file
Also start services tagged
Profiles apply to too, or profiled containers stay behind
Resolve relative paths from another directory
Check what the merged file looks like

is loaded automatically alongside when you do not pass , which is the usual way to keep dev-only ports and bind mounts out of the main file. The same flags can live in the environment: , , .

Inside the file, pulls in another compose file as if it were part of this one, and keys hold YAML anchors you can reuse across services. Compose ignores any top-level key starting with , which is what makes them safe to define:

include:
  - ./monitoring/compose.yaml

x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  web:
    image: nginx
    logging: *default-logging
  api:
    image: myapi
    logging: *default-logging

on a service does the same for one service at a time, pulling its definition from another file and letting you override parts of it.

Environment Variables

Two different things share the name. in the file is substituted by Compose before anything starts, reading the shell environment and the project file. and put variables inside the container. A variable in does not reach the container unless you pass it through.

SyntaxWhat it does
Substituted from the shell or the project ; empty with a warning if unset
Fallback when is unset or empty
Fallback only when is unset; an empty value stays empty
Refuse to start, with that message, when is missing
A literal , passed through for the container's shell to expand
Set a variable in the container
No value: pass it through from the shell that ran
Load a file into the container
Several files; later ones win
Change which file feeds substitution
One-off override, highest priority of all
Print the file with every variable resolved

When the same variable is set in more than one place, the order from strongest to weakest is , then , then , then in the image's Dockerfile. means substitution found nothing, and is the fastest way to see what it did use.

For passwords, keeps the value out of the environment and out of . The file is mounted at , and most official images take a variant that reads it:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./db_password.txt

Validate and Clean Up

CommandWhat it does
Validate the file and print the fully resolved version
Validate only, no output; non-zero exit on error
List service names
List volume names
List profiles
Remove stopped service containers
Stop, remove, and drop anonymous volumes without asking
Remove containers for services you deleted from the file
Which Compose you have

on is a warning, not an error: a service was renamed or removed from the file and its old container is still around. on or clears it.

compose.yaml Keys

The keys you actually use, per service. Indentation is two spaces and matters.

KeyExampleNotes
top levelOne entry per container
Image to pull
or Build from a Dockerfile instead. Add too to name the result
Fixed name; default is . Prevents
. Quote them; binds to localhost only
Reachable by other services only, not the host
Named volume (declare it under top-level )
Bind mount; relative paths are allowed here
Read-only
or Variables in list or map form
Load variables from a file into the container
(default), , ,
Start order only; does not wait for readiness
Wait for the healthcheck to pass
see belowHow Compose decides the service is healthy
Attach to a named network (declare under top-level )
Replace the image's CMD
Replace the image's ENTRYPOINT
Run as a UID and GID
Working directory
Only start with
Metadata; see the Traefik label generator
Cap log size
Memory and CPU caps (honoured by too, not only Swarm)

Top-level and declare the names the services refer to:

volumes:
  data:
networks:
  backend:

Services on the same project network reach each other by service name (), which is why works. Compose creates a default network per project, so you only need when you want to split services up.

Volumes

EntryWhat it is
Named volume. Docker manages it, and it survives
Bind mount. A host path, relative to the compose file
A single file, read-only
Anonymous volume. Common trick to stop a bind mount hiding a built directory
Variable in a host path works like anywhere else
In memory, never written to disk, gone on restart
Long syntax, needed for options like
SELinux relabel ( shared, private) on Fedora and RHEL hosts

Named volumes have to be declared at the top level before a service can use one:

volumes:
  data:
  cache:
    external: true

means Compose will use a volume you created yourself with and will not remove it, even on . Everything else gets the project name as a prefix, so in project is really and lives under on a Linux host. lists them, gives the path, and prints the names the file declares.

Networks

Compose puts every service on a network called and registers each service name in its DNS, so resolves from any other container in the project. Nothing else is needed for containers to talk to each other.

KeyWhat it does
Put the service on a declared network instead of the default
Extra DNS names for the service
Share the host's network stack. is ignored, Linux only
Route through another service's stack, the usual VPN container setup
No networking at all
Set the container's own hostname
Extra entries
Reach a service running on the host itself
Override the container's resolver
Publish to the host
Reachable inside the project only, not from the host

To join a network created outside the project, for example one shared with a reverse proxy, declare it as external:

services:
  web:
    networks:
      - proxy
      - default

networks:
  proxy:
    external: true

or a connection refused between two containers is almost always one of three things: the services are not on a shared network, you used the published host port instead of the container port (, not ), or you used , which inside a container means that container. settles it in one line. This is how Traefik and Caddy reach app containers: the proxy joins the same network and talks to the service name.

Restart and Autostart

PolicyBehaviour
The default. Never restarted
Restart only on a non-zero exit code
The same, giving up after five attempts
Always restart, including after the Docker daemon or the host restarts
The same, except a container you stopped by hand stays stopped
Signal sent on and (default )
How long to wait before (default 10s)

For a stack that should come back after a reboot, on every service plus is enough. When the stack needs a defined boot order, a mounted disk, or a wait for the network, a unit file is better:

[Unit]
Description=myapp compose stack
Requires=docker.service
After=docker.service network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

then owns the stack. The systemd unit generator writes files like this, and the systemctl commands sheet covers the rest. A container stuck in a restart loop is a different problem; see Docker container keeps restarting.

Limits, Users and Devices

KeyExampleNotes
Hard cap. The container is killed when it goes over
CPU cap, in cores
Soft floor rather than a cap
Run as a UID and GID. Use IDs, not names the image may not have
Extra groups, for the docker socket or
/ Add or drop Linux capabilities
Everything at once. Last resort
Block privilege escalation inside the container
Read-only root filesystem; pair with for scratch space
Pass a host device through
Run a tiny init as PID 1 so signals work and zombies are reaped
Shared memory, needed by Chrome and some databases
File descriptor and process limits
Kernel settings for this container

is honoured by plain , not only by Swarm. GPUs go under reservations:

    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

Example compose.yaml

A web app built from the local Dockerfile, a Postgres database with a healthcheck, and a named volume for its data:

services:
  web:
    build: .
    image: myapp:latest
    ports:
      - "8080:3000"
    env_file: .env
    environment:
      DB_HOST: db
      NODE_ENV: production
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16
    volumes:
      - data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

volumes:
  data:

reads from the shell or the project file and falls back to . The Docker Compose generator builds files like this for common stacks.

Healthcheck and depends_on

alone only orders startup: the container starts first, but Postgres may still be initialising when connects. A healthcheck plus makes Compose wait.

KeyExampleNotes
Exit 0 is healthy. runs directly, runs through
Shell form when you need , pipes or variables
Time between checks (default 30s)
Fail the check if it takes longer
Consecutive failures before
Grace period at startup during which failures do not count
Turn off a healthcheck the image defines
in The default: container started
in Wait until the healthcheck passes
in Wait for a one-shot service (migrations) to exit 0
in Restart this service when the dependency is recreated

The check runs inside the container, so or has to exist in that image. Alpine images often lack ; usually works instead. shows or next to the state, and shows the last few results.

command vs entrypoint

SituationWhat to write
Change the arguments, keep the image's entrypoint
Replace the whole command (image has no entrypoint)
Run a shell script with or pipes
Same, in list form
Ignore the image's entrypoint entirely plus
Keep a container alive with no real process (or )
Interactive shell on or add and
Pass arguments to an entrypoint script; the script gets them as ,

The string form () is split on spaces by Compose without a shell, so quotes inside it are kept literally and is passed as an argument. When in doubt, use the list form and name explicitly. A multi-line script can go under a folded block scalar:

    command: >
      sh -c "npm run migrate &&
             npm run seed &&
             npm start"

Install and Version

Compose v2 is a plugin, so it installs next to the Docker CLI rather than on its own.

WhereHow
Docker DesktopAlready included. Nothing to install
Debian, Ubuntu from Docker's own repository
Fedora, RHEL
Arch (the package is v2)
macOS, Homebrew, for Colima or a plain Docker CLI
AnywhereDrop the binary into
Check it, or for the number alone

The distribution package called is v1 on older Debian and Ubuntu releases, which is why the plugin package has a different name and comes from Docker's repository, not the distribution's. To install by hand:

mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 \
  -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version

Use instead of to install it for every user. If every command needs , add yourself to the group with and log out and back in; see Cannot connect to the Docker daemon when it still fails.

Gotchas

  • does not read the file again. After editing , run ; it recreates only what changed.
  • and are not the same. keeps the containers so resumes them with their writable layer intact; deletes them and the project network, and builds new ones. Named volumes outlive both.
  • deletes every named volume declared in the file, including the database. alone keeps them.
  • is v2 telling you to delete the line at the top of the file. It has no effect and never did in v2.
  • is a schema error: a misspelled key, or a service key written at the top level (or the other way round). points at the line.
  • without a only orders container starts. Add a healthcheck and when the app crashes on boot because the database is not ready.
  • Ports in YAML must be quoted (); unquoted is parsed as a base-60 number by some YAML parsers and becomes garbage.
  • in the project directory feeds substitution in the file. feeds the container. They are different files with different jobs, and a variable in does not reach the container unless you also pass it through or .
  • Services started with need the same on , or their containers are left running.
  • on means another container, or a process on the host, holds that port. and find it; see Error response from daemon for the other variants.
  • Bare commands work on Compose containers too: , . The docker commands sheet covers them.

Docker Compose FAQ

What is the difference between docker-compose and docker compose?

The hyphen is v1 and the space is v2. v1 was a separate Python program; v2 is written in Go and ships as a plugin to the docker CLI, which is why it is a subcommand now. v1 reached end of support in 2023 and was dropped from Docker Desktop. The catch is that docker-compose often still works, because Docker installs a small wrapper under the old name that forwards to v2: run docker-compose version and a 2.x answer means you are already on v2 through the wrapper. The change that actually breaks things is naming. v1 called containers myproject_web_1 and v2 calls them myproject-web-1, so log greps, monitoring rules and scripts that match container names need updating. Networks and volumes kept the underscore form, so myproject_default and myproject_data are unchanged.

Does docker compose down remove volumes?

No. down removes the containers and the project network and leaves every named volume alone, which is why a database survives a down and up. down -v is the one that deletes them, along with any anonymous volumes those containers had, and there is no undo. A volume declared external: true is skipped even by -v, since Compose did not create it. Bind mounts are just host directories, so no form of down touches them. Volumes from a project you renamed or deleted stay on disk under their old myproject_name and only docker volume ls will show you they are still there.

Does docker compose up rebuild images?

No. up builds an image only when none exists under that name yet, so after the first run it keeps using the image it already has and your Dockerfile edits appear to do nothing. up -d --build rebuilds, and build --no-cache ignores the layer cache when a step like apt-get update has gone stale. Pulling works the same way: up fetches a missing image but never checks whether a tag has moved, so :latest stays on whatever you downloaded first until you run docker compose pull, or up -d --pull always. docker compose images prints the image ID behind each running service, which is the quickest way to tell whether a rebuild actually took.

Can docker compose be used in production?

Yes, on a single host, and plenty of small production stacks run exactly this way. What you give up is multi-host scheduling and rolling updates: up -d stops the old container before it starts the new one, so there is a short gap on every deploy. A reverse proxy in front absorbs that, which is why Traefik and Caddy are both configured from the same compose file. Pin image tags rather than latest so a pull cannot change the version under you, keep passwords in secrets or an env file that is not in git, set restart: unless-stopped, and start the stack from a systemd unit so a reboot brings it back.

Where is the docker compose file?

Compose looks in the current directory for compose.yaml, then compose.yml, then docker-compose.yaml, then docker-compose.yml, and if it finds none it walks up the parent directories until it hits the filesystem root. no configuration file provided: not found means that search came up empty, almost always because of the directory you are standing in. compose.override.yaml next to the file is merged in automatically. For a stack someone else started, docker compose ls prints the config file path of every running project, and docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' container-name gets it from a single container.

Related cheat sheets