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 top | delete the line | Obsolete 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 available | Every Compose project on the host, not just this one | |
| not available | Block until every service is running and healthy | |
| not available | Sync 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:
| Message | Cause |
|---|---|
| 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 .
| Command | What 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
| Command | What 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.
| Command | What 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
| Command | What 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 -drecreates 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.jsoncopies changed files into the running container, rebuilds the image and recreates the container, and copies then restarts the process.
Files, Projects and Profiles
| Command | What 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-loggingon 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.
| Syntax | What 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.txtValidate and Clean Up
| Command | What 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.
| Key | Example | Notes |
|---|---|---|
| top level | One 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 below | How 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
| Entry | What 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: truemeans 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.
| Key | What 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: trueor 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
| Policy | Behaviour |
|---|---|
| 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.targetthen 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
| Key | Example | Notes |
|---|---|---|
| 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.
| Key | Example | Notes |
|---|---|---|
| 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
| Situation | What 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.
| Where | How |
|---|---|
| Docker Desktop | Already 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 |
| Anywhere | Drop 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 versionUse 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.