Docker Compose
Most real projects are several containers that must find each other. Compose describes the whole stack in one file and starts it with one command.
? What do the diagram colours mean
key idea / Docker itselfcontainer / good pathhost / watch outproblem / blockedneutral
Why Compose
A real app is several containers plus a network and a volume. Starting them by hand means remembering every flag, in the right order, on every machine.
Reading a compose file
Each key under services becomes a container. The keys inside are the docker run flags you already know, written as YAML.
services:
web:
image: nginx:1.27
ports:
- "8080:80" # the only door from outside
depends_on: [api]
api:
build: ./api # docker build ./api for you
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/shop
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD} # read from .env next to this file
POSTGRES_DB: shop
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "app"]
interval: 5s
retries: 10
volumes:
pgdata:| compose.yaml | Same as |
|---|---|
image: nginx:1.27 | docker run nginx:1.27 |
build: ./api | docker build ./api, then run the result |
ports: ["8080:80"] | -p 8080:80 (chapter 8) |
environment: | -e KEY=value (chapter 6) |
volumes: [pgdata:/var/lib/…] | -v pgdata:/var/lib/… (chapter 7) |
| (nothing) | docker network create plus --network on every container |
What it creates
Start order is not readiness
depends_on: [db] only waits for the db container to start, not for Postgres to accept connections, which can take seconds. Give the dependency a healthcheck and wait for condition: service_healthy, as the file above does. Your app should still retry its first connection: databases restart in production too.
The commands you will use
| Command | Does |
|---|---|
docker compose up -d | create or update everything, in the background |
docker compose up -d --build | rebuild images with build: first |
docker compose ps | this project’s containers and their health |
docker compose logs -f api | follow one service’s logs |
docker compose exec db psql -U app shop | a shell or tool inside a running service |
docker compose down | remove containers and the network; keep volumes |
docker compose down -v | also delete the volumes, and your data |
docker compose config | print the file with every ${VAR} filled in |
Compose reads a .env file next to compose.yaml to fill in ${DB_PASSWORD}. Keep that file out of Git. A compose.override.yaml, if present, is merged on top automatically, which is a common place for development-only bind mounts.
Try it yourself
-
Step 1 · Write a two-service file
Work in an empty folder. The folder name becomes the project name.
$ mkdir demo && cd demo $ cat > compose.yaml << 'EOF' services: web: image: nginx:1.27 ports: - "8080:80" cache: image: redis:7-alpine EOF -
Step 2 · Bring it up
Compose creates the network first, then both containers on it.
$ docker compose up -d ✔ Network demo_default Created ✔ Container demo-cache-1 Started ✔ Container demo-web-1 Started $ docker compose ps NAME SERVICE STATUS PORTS demo-cache-1 cache Up 3 seconds 6379/tcp demo-web-1 web Up 3 seconds 0.0.0.0:8080->80/tcp -
Step 3 · Service names resolve
webfindscacheby name with no extra setup, because both are ondemo_default.$ docker compose exec web getent hosts cache 172.20.0.2 cache $ docker compose exec cache redis-cli ping PONG $ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080 200 -
Step 4 · Tear it down
downremoves whatupmade. There are no volumes here, so nothing is left behind.$ docker compose down ✔ Container demo-web-1 Removed ✔ Container demo-cache-1 Removed ✔ Network demo_default Removed
localhost:5432 from the api service to reach the database. Each service is its own container with its own localhost. Use the service name: db:5432.Recap
compose.yamldeclares services, networks and volumes;docker compose upcreates them.- Every service key maps to a
docker runflag you already know. - Compose makes a project network, so services reach each other by service name.
depends_onorders start-up; add a healthcheck andservice_healthyto wait for readiness.downkeeps volumes;down -vdeletes them.
Quick check
Your api starts before Postgres accepts connections and crashes, even with depends_on: [db]. Why?
Show answer
Answer: depends_on waits for the db container to start, not for Postgres to be ready.
Add a healthcheck to db and depends_on with condition: service_healthy, and let the app retry its first connection.
You run docker compose down -v. What happens to the pgdata volume?
Show answer
Answer: It is deleted, with all its data.
down removes containers and networks; -v also removes the named volumes declared in the file.
Docker Compose: common questions
Does depends_on wait for a database to be ready?
Not by itself. It waits for the container to start. Add a healthcheck to the database and use depends_on with condition: service_healthy.
Does docker compose down delete my data?
docker compose down removes containers and networks but keeps named volumes. docker compose down -v also deletes the volumes.