Chapter 51 — Docker Compose: running more than one container together
In one sentence
Docker Compose lets you describe an entire multi-container application — every service, its image, its ports, its volumes, its dependencies on other services — in ONE declarative YAML file, and bring the whole thing up or down with a single command.
Why this matters
Almost no real application is a single container — Compose is how you develop and test a realistic, multi-service setup locally, and it's the direct conceptual predecessor to Kubernetes' declarative model (Part 16).
How it actually works
# docker-compose.yml
services:
web:
build: . # build FROM a local Dockerfile
ports:
- "8080:80" # Chapter 50's -p, declaratively
depends_on:
- db # start `db` first (doesn't wait
# for it to be actually READY --
# a common, real gotcha)
environment:
DATABASE_URL: "postgresql://db:5432/myapp" # Chapter 50's
# container-name DNS resolution,
# used directly
db:
image: postgres:16 # or pull an existing image
volumes:
- dbdata:/var/lib/postgresql/data # Chapter 49's volumes,
# declaratively
environment:
POSTGRES_PASSWORD: "devpassword"
volumes:
dbdata: # declare the named volume
docker compose up # build/pull images, create
# the network, start everything,
# in the RIGHT ORDER
docker compose up -d # detached (background)
docker compose down # stop and remove
# everything (containers,
# network -- volumes SURVIVE by
# default, exactly Chapter 49's
# deliberate separation)
docker compose logs -f web # follow ONE service's
# logs
docker compose ps # what's running, in
# this project
Real-world example
`depends_on`'s actual behaviour is one of the most common, well-
documented real points of confusion for engineers new to Compose:
Docker Compose's own official documentation is explicit that, by
default, `depends_on` only controls STARTUP ORDER -- it starts `db`
BEFORE `web`, but it does NOT wait for the database to actually be
READY to accept connections (a database process can take several
real seconds to initialise after its CONTAINER has technically
started, Chapter 3's process-vs-"actually ready" distinction). A
very common real symptom: `web`'s application code tries to connect
to `db` immediately on startup, the database isn't accepting
connections yet, and the application crashes or errors out --
despite `depends_on` being correctly configured, exactly as
documented.
THE REAL, DOCUMENTED FIX Compose's own docs recommend: either build
RETRY LOGIC into your application's own startup/connection code
(the generally-preferred, more robust real-world approach, since it
also protects you in production, not just local dev), or use
`depends_on`'s `condition: service_healthy` option paired with an
explicit HEALTHCHECK definition on the dependency service, so
Compose actually waits for a real readiness signal, not just "the
container process started." This exact "started" vs. "actually
ready" distinction reappears, in a more powerful form, as
Kubernetes' readiness probes (Chapter 60) -- worth remembering this
Compose gotcha as the direct conceptual ancestor of that later,
more robust mechanism.
Try it yourself
mkdir -p ~/lab/composedemo && cd ~/lab/composedemo
cat > docker-compose.yml << 'EOF'
services:
web:
image: alpine
command: sh -c "echo waiting; sleep 3; ping -c 2 db"
depends_on:
- db
db:
image: alpine
command: sh -c "sleep 300"
EOF
docker compose up
docker compose ps
docker compose logs web
docker compose down
Common mistakes
- Trusting
depends_onalone for "the service I depend on is actually ready." It only guarantees START ORDER — build real readiness checks (retries, healthchecks) into your application. - Forgetting
docker compose downpreserves volumes by default. Good, deliberate safety (Chapter 49) — but surprising the first time if you expected a truly clean slate; add-vexplicitly if you genuinely want volumes removed too. - Hardcoding secrets directly in
docker-compose.yml. Use an.envfile (excluded from version control) or a proper secrets-management approach instead — a committed compose file with real credentials in it is a common, real leak.
Check yourself
- What does
depends_onactually guarantee, and what does it NOT guarantee? - What happens to volumes when you run
docker compose down(without-v)? - What's the more robust, production-relevant fix for the "started vs. ready" problem, and what Kubernetes mechanism does it foreshadow?
Further reading
- Docs: Docker's own official Compose specification and "Control startup and shutdown order" documentation.
- Reference:
docker compose --help. - Practice: convert a small, real two-service idea (a web app + a
database) into your own
docker-compose.ymlfrom scratch.