Lesson 4 · Containers & Docker
Docker Compose & multi-service stacks
One YAML file, many containers — and the readiness trick that makes a 25-service stack come up in order.
Your win: read a docker-compose.yaml, explain
services / volumes / networks, and — the interview
money question — the difference between depends_on that waits for
start vs service_healthy that waits for ready.
Why Compose exists
Running one container by hand is fine. Running twenty-five, each with its ports, volumes, env, network, and start order, is not. Docker Compose lets you declare the whole multi-container app in one YAML file and bring it up with a single command.1 A Compose file has three top-level keys you need to know:
services— each container: its image, ports, volumes, env, dependencies, command.volumes— named volumes shared across services.networks— the virtual networks services sit on.
Everything from Lesson 3 reappears here as YAML keys instead of docker run
flags. That's the whole trick — Compose is declarative docker run, at scale.
A minimal example
services: db: image: postgres:15 ports: ["5432:5432"] volumes: [postgres_data:/var/lib/postgresql/data] # named volume healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] app: image: new-backend:locally depends_on: db: { condition: service_healthy } # wait for READY command: [gserver, bob] # which service to run volumes: { postgres_data: {} } networks: { local-manabie-network: {} }
Bring it up: docker compose up -d. Then docker compose ps,
logs -f, down. Containers reach each other by service name
(app → db) on the shared network — Lesson 3's DNS rule.
The gotcha that ends up on every interview
depends_on: started ≠ ready
Plain depends_on: [db] waits only until the DB container has
started — not until Postgres is actually accepting connections. Your app
can race ahead and crash on connect. The fix: give the dependency a
healthcheck and depend on condition: service_healthy, so
Compose waits for ready.2
How this repo wires ~25 services
The stack is split across several Compose files by role, and
local/run.bash picks which to use:
| File | Holds |
|---|---|
docker-compose.infra.yaml | infra deps — db, unleash, messaging |
docker-compose.service.yaml | the ~25 backend services (new-backend:locally) |
docker-compose.migration.yaml | one-shot DB migration containers |
docker-compose.decrypt.yaml | SOPS secret-decryption sidecar |
*.macos.yaml | macOS host-networking variants |
wait-for-it-service, whose healthcheck is
nc -z db 5432 && curl -f http://unleash:4242/unleash/health. Every real
service then declares depends_on: wait-for-it-service: {condition:
service_healthy} (e.g. fink at lines 23–25). So the whole stack waits
for infra to be genuinely ready before any service boots — Compose's
started-≠-ready problem, solved once.
command: selects the service
Every backend service uses the same new-backend:locally image
(line 21) — Lesson 2's "one binary, many services." What makes one container
fink and another bob is the command: args (lines
34–39), plus the configs/secrets bind-mounted in.
Docker — Compose & startup order
How Compose models an app, the file reference, and the depends_on /
service_healthy startup-order guide.
→ docs.docker.com — Docker Compose
→ docs.docker.com — Control startup order
Check yourself (from memory)
Q1. The three top-level keys of a Compose file are…
services (the containers), volumes
(shared storage), networks (the virtual networks).
Q2. Plain depends_on: [db] guarantees only that the DB has…
condition: service_healthy to wait for readiness.
Q3. In this repo, what makes one service container "fink" vs "bob"?
new-backend:locally; the
command: (+ bind-mounted config) selects the service. One binary, many
services.
docker compose up. Three top-level keys: SERVICES (each container: image,
ports, volumes, env, command), VOLUMES (named, shared), NETWORKS. Services reach each other
by SERVICE NAME via Docker DNS on their shared network. GOTCHA: plain
depends_on waits for STARTED, not READY — add a HEALTHCHECK + depend on
condition: service_healthy. Repo: infra vs service vs migration compose files
driven by run.bash; a single wait-for-it-service healthcheck
(nc -z db 5432 && curl unleash/health) gates all ~25 services, each the same
new-backend:locally image selected by command:.1. Docker — Docker Compose overview; Compose file reference.
2. Docker — Control startup order (depends_on & service_healthy).