Docker Compose lets you describe several containers, their ports, volumes, environment variables and networks in one YAML file called compose.yaml, then start them all with docker compose up and remove them with docker compose down. It replaces a list of long docker run commands with a file you can commit next to your code.
docker compose up -d # create and start everything in compose.yaml, in the background
docker compose ps # list the project's containers
docker compose logs -f # follow output from all services
docker compose down # stop and remove containers and networks
What problem Docker Compose solves
A typical web app needs at least two containers: the app and a database. Started by hand, that means creating a network, a volume for the database, then two docker run commands with a dozen flags between them, typed in the right order every time (the individual commands are in the Docker commands cheat sheet). Compose moves all of it into a declarative file. Docker's documentation describes the model in three parts:
- Services: each one is a container (or several identical copies) run from one image with one configuration.
- Networks: how services reach each other.
- Volumes: persistent data that outlives individual containers.
Everything in one file belongs to a project. By default the project name is the name of the folder containing the file, and Compose prefixes the containers, network and volumes it creates with that name.
A complete compose.yaml example
This file runs a web app built from the local Dockerfile plus a PostgreSQL 16 database:
services:
web:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
volumes:
db-data:
Line by line:
build: .builds thewebimage from theDockerfilein the same folder. Useimage:instead when the image already exists on a registry, asdbdoes.ports: "8000:8000"publishes container port 8000 on host port 8000 (host first, container second).${DB_PASSWORD}is filled in from your shell or a.envfile (next section).depends_onwithcondition: service_healthymakes Compose wait until the database passes itshealthcheckbefore startingweb.db-data:/var/lib/postgresql/datastores the database files in a named volume, declared at the bottom, so they survive the container being recreated.
The file name matters. Compose looks for compose.yaml (preferred) or compose.yml, and still accepts the older docker-compose.yaml and docker-compose.yml. If you have a file with another name, pass it with -f.
How services talk to each other
Notice the database address in DATABASE_URL: db:5432. Compose creates a network called <project>_default, attaches every service to it, and registers each service name in Docker's internal DNS. So the web container reaches the database at hostname db.
Two port rules from the Compose networking docs are worth memorising:
- Service-to-service traffic uses the container port (5432 here), not any published host port.
- The host port in
ports:only matters for connections from outside, such as your browser. Thedbservice above has noports:entry at all, so the database is reachable bywebbut not exposed on your machine.
A common mistake is pointing an app at localhost:5432 inside a container. Inside a container, localhost is that container itself, so use the service name.
Environment variables and the .env file
Compose reads a file named .env from the project directory automatically and uses it to fill in ${VARIABLE} references in compose.yaml. Create one next to the compose file:
DB_PASSWORD=change-me-to-something-long
Generate a real value with a password generator such as larely's password generator, and add .env to .gitignore so the secret isn't committed. Values set in your shell override the .env file.
If the variable isn't set anywhere, Compose doesn't stop; it substitutes an empty string and warns you. Real output from docker compose config with no .env:
level=warning msg="The \"DB_PASSWORD\" variable is not set. Defaulting to a blank string."
To make a missing value a hard error instead, write ${DB_PASSWORD:?DB_PASSWORD must be set}; Compose then refuses to start with required variable DB_PASSWORD is missing a value: DB_PASSWORD must be set. To supply a default, write ${DB_PASSWORD:-devpassword}.
Check the file before you run it: docker compose config
docker compose config # print the fully resolved file
docker compose config --services # just the service names
config parses the file, fills in variables and prints the result in canonical form, so you can see exactly what Compose will do. It works even when the Docker engine isn't running. For the example above it shows the generated names, which is where the project prefix becomes visible (real output, run in a folder named myapp; the end of the file):
networks:
default:
name: myapp_default
volumes:
db-data:
name: myapp_db-data
A YAML indentation mistake shows up here as an error such as yaml: line 1: did not find expected key before anything is created.
The Compose commands you'll use
| Command | What it does |
|---|---|
docker compose up -d | Build missing images, create or recreate containers, start them in the background |
docker compose up -d --build | Rebuild images first (after changing your code or Dockerfile) |
docker compose ps | List the project's containers and their state |
docker compose logs -f web | Follow one service's output (omit the name for all) |
docker compose exec db psql -U app | Run a command in a running service container |
docker compose run --rm web pytest | Start a one-off container for a service |
docker compose stop / start | Stop containers without removing them, then start them again |
docker compose down | Stop and remove containers and the project's networks |
docker compose down -v | Same, plus delete named and anonymous volumes (your data) |
docker compose pull | Download newer versions of the services' images |
Run them from the folder containing compose.yaml; Compose also searches parent folders for the file. When you edit compose.yaml and run up -d again, Compose only recreates the containers whose configuration or image changed, and keeps mounted volumes.
depends_on doesn't mean "ready"
Plain depends_on: [db] only controls start order. Docker's docs are explicit that Compose waits until a dependency's container is running, not until the program inside is ready to accept connections. A database container is "running" a few seconds before PostgreSQL accepts logins, which is why apps started this way often crash on the first attempt. The fix is the pattern in the example: give the dependency a healthcheck and use condition: service_healthy. The other conditions are service_started (the default) and service_completed_successfully, for one-shot jobs such as migrations.
Compose v1, v2 and the version key
Older tutorials use docker-compose with a hyphen. That is Compose v1, which Docker's migration guide says is no longer maintained. The current version is invoked as docker compose, a plugin of the Docker CLI. Old tutorials also start files with version: "3.8". Current Compose ignores it and warns (real output from Compose v2.27, path shortened):
level=warning msg="docker-compose.yml: `version` is obsolete"
Delete the line. If you need a fixed project name instead of the folder name, use the top-level name: key or docker compose -p myname up.
Compose runs everything on one Docker host. For what containers isolate and where they differ from full VMs, see Docker vs a virtual machine.
Frequently asked questions
- What is the difference between docker-compose and docker compose?
- docker-compose with a hyphen is Compose v1, a separate standalone program that Docker no longer maintains. docker compose with a space is Compose v2, a plugin built into the Docker CLI and included with Docker Desktop. The file format is the same; use docker compose.
- Should I still put version: "3.8" at the top of my compose file?
- No. The top-level version key is obsolete in current Compose. It is only informative, and Compose prints a warning that it is obsolete when it finds one. Delete the line; nothing else changes.
- Does docker compose down delete my database data?
- Not by default. docker compose down removes the project's containers and networks but keeps named volumes. Only docker compose down -v (--volumes) also removes the named volumes declared in the file, which deletes data stored in them.
- Is Docker Compose suitable for production?
- Compose runs containers on a single Docker host. That is fine for small deployments, staging servers and many self-hosted apps. If you need to spread containers across several machines with automatic failover, you need an orchestrator such as Kubernetes instead.
Sources
- Docker docs: How Compose works (application model)
- Docker docs: Networking in Compose
- Docker docs: Control startup order
- Docker docs: Set environment variables with interpolation
- Docker docs: Specify a project name
- Docker docs: docker compose down
- Docker docs: Version and name top-level elements
- Docker docs: Migrate to Compose v2