Docker Compose runs multi-container applications from a single compose.yaml. This reference covers the CLI as of Compose v5.5.0 (17 August 2026) — 38 subcommands, the file-schema keys you reach for weekly, and the startup-ordering rules that cause most "works on my machine" bugs. Every command here is docker compose (v2 CLI plugin, Go); the hyphenated docker-compose v1 was retired in June 2023 and is not covered.
When to reach for this
- A service starts before its database is actually accepting connections and you need
depends_onwith a real health condition, not just start ordering. - You want file changes on the host to reach a running container without a manual rebuild — the
develop.watchblock. - You need to run one stack in several variants (dev tooling, seed jobs, observability) without maintaining separate files — profiles.
Which Commands Do You Actually Use?
| Command | Description |
|---|---|
| docker compose up -d | Create and start everything, detached |
| docker compose up --build | Rebuild images first, then start |
| docker compose up --watch | Start and live-sync files per develop.watch |
| docker compose down | Stop and remove containers and networks |
| docker compose down -v | Also delete named volumes — this drops your data |
| docker compose ps | List the containers in this project, with health |
| docker compose logs -f <svc> | Follow the logs for one service |
| docker compose exec <svc> sh | Shell into a running container |
| docker compose run --rm <svc> <cmd> | One-off task in a fresh container |
| docker compose restart <svc> | Restart without recreating |
| docker compose config | Print the fully resolved file with vars substituted |
| docker compose ls | List every Compose project running on the host |
docker compose config is the single most useful debugging command: it resolves every ${VAR}, merges override files, and shows you exactly what Compose will act on. Run it before blaming Docker.
The Rest of the Subcommands
| Command | Description |
|---|---|
| build | Build or rebuild service images |
| create | Create containers without starting them |
| start / stop | Start or stop existing containers |
| pause / unpause | Suspend and resume processes in containers |
| kill | Send SIGKILL rather than a graceful stop |
| rm | Remove stopped service containers |
| pull / push | Download or upload service images |
| images | Show images used by the project |
| volumes | List volumes declared by the project |
| port | Print the host binding for a container port |
| top | Show running processes per service |
| stats | Live resource usage per container |
| events | Stream container events as they happen |
| cp | Copy files between host and service container |
| attach | Attach to the stdio of a running service container |
| wait | Block until services stop, then return their exit code |
| scale | Change the replica count for a service |
| watch | Run only the file-watch loop, without app logs |
| commit | Create an image from a changed container |
| export | Export a container filesystem as an archive |
| publish | Publish a Compose application |
| bridge | Convert a Compose file to another format |
| version | Print the Compose version |
How Do You Run Only Part of the Stack?
| Pattern | What it does |
|---|---|
| docker compose up api worker | Start only these services (plus their dependencies) |
| docker compose up --no-deps api | Start api alone, skipping dependencies |
| docker compose --profile debug up | Activate a profile, adding its services |
| COMPOSE_PROFILES=debug,seed docker compose up | Activate several profiles via the environment |
| docker compose -f base.yaml -f prod.yaml up | Merge override files, later wins |
| docker compose --project-name staging up | Run an isolated second copy of the same stack |
A service with a profiles: key is skipped unless that profile is active. A service with no profiles: key always runs. That asymmetry is the whole feature: put your optional extras behind a profile and leave the core stack bare.
How Do You Make a Service Wait for a Healthy Dependency?
Plain depends_on: [db] only waits for the container to start, not for Postgres to accept connections. That gap is why an API can still fail its first query on a cold start, intermittently, in a way that disappears the moment you retry. Use the long syntax with a condition:
| Condition | Waits until |
|---|---|
| service_started | Container has started — same as the short syntax |
| service_healthy | The healthcheck on the dependency passes |
| service_completed_successfully | Dependency ran to completion with exit code 0 |
| depends_on field | Meaning |
|---|---|
| condition | One of the three values above |
| restart: true | Restart this service when the dependency is updated (Compose 2.17+) |
| required: false | Only warn if the dependency is missing (Compose 2.20+); defaults to true |
service_completed_successfully is the one people forget — it is how you express "run migrations, then start the API" without a sleep loop in an entrypoint script.
Healthcheck Keys
| Key | Purpose |
|---|---|
| test | The probe; a list starting with CMD, CMD-SHELL, or NONE |
| interval | Time between checks once running |
| timeout | How long a single check may take |
| retries | Consecutive failures before the container is unhealthy |
| start_period | Grace window where failures do not count |
| start_interval | Shorter probe gap during start_period (Compose 2.20.2+) |
| disable | Set true to turn off a healthcheck inherited from the image |
start_period plus start_interval is the combination that makes service_healthy fast instead of painful: give a slow database a 40s grace window, but probe it every second inside that window so you proceed the moment it is genuinely up.
How Do You Live-Reload Code Into a Container?
The develop.watch block replaces the old bind-mount-everything trick. Run it with docker compose up --watch, or docker compose watch to keep sync events out of your application logs.
| Watch key | Purpose |
|---|---|
| path | Required. Host file or directory to watch |
| action | Required. What happens on change |
| target | Where the path maps inside the container |
| ignore | Patterns to exclude, relative to path |
| initial_sync | Bring files up to date before watching starts |
| action | Behaviour | Use for |
|---|---|---|
| sync | Copy changed files into the container | Frameworks with their own hot reload |
| rebuild | Build a new image and replace the container | Compiled languages, lockfile changes |
| sync+restart | Copy files, then restart the container | Config files that are read at boot |
The common mistake is watching a directory that includes node_modules or a build output folder. Every write inside it triggers a sync, and the loop never settles — set ignore explicitly.
Where Do Environment Variables Fit?
Compose has five separate environment mechanisms that do not all reach the container, and the precedence between them is the most common source of missing-value bugs. Rather than duplicate it here, the Docker Compose environment variables guide maps every form — environment:, env_file:, .env, --env-file, and ${VAR} substitution — to its scope and precedence, and covers the COMPOSE_* variables that configure the CLI itself. The one-line version:
| Mechanism | Reaches the container? |
|---|---|
| environment: | Yes — highest precedence |
| env_file: | Yes — lower than environment: and the shell |
| .env (auto-loaded) | No — Compose-file interpolation only |
| --env-file | No — replaces .env for interpolation only |
What Are the Common Gotchas?
| Symptom | Cause | Fix |
|---|---|---|
| Data disappeared after renaming the folder | Project name defaults to the directory basename, so volumes are orphaned under the old prefix | Set COMPOSE_PROJECT_NAME explicitly; find old volumes with docker volume ls |
| App cannot reach the database at localhost | Each container has its own network namespace | Use the service name as the hostname: postgres://db:5432 |
| Port is already allocated | Another project or a host process holds the port | docker compose ls to find the other project, or change the host side of the mapping |
| Env changes are ignored after an edit | Existing containers keep the environment they were created with | docker compose up -d --force-recreate |
| Changes to compose.yaml do nothing | A stale override file or COMPOSE_FILE is in play | Check docker compose config to see what is actually merged |
| Build is slow on every run | Layer cache busted by copying source before installing dependencies | Copy lockfiles and install first, then copy the rest of the source |
For the wider Docker command surface — run, image management, networking, volumes — see the Docker cheat sheet.