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:

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 (appdb) 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:

FileHolds
docker-compose.infra.yamlinfra deps — db, unleash, messaging
docker-compose.service.yamlthe ~25 backend services (new-backend:locally)
docker-compose.migration.yamlone-shot DB migration containers
docker-compose.decrypt.yamlSOPS secret-decryption sidecar
*.macos.yamlmacOS host-networking variants
Anchor — the readiness gate Rather than put a healthcheck on all 25 services, the repo uses one gate container. local/docker-compose.service.macos.yaml:2-18 defines 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.
And the 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.
Read this next

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…

Started ≠ ready. Add a healthcheck and depend on condition: service_healthy to wait for readiness.

Q3. In this repo, what makes one service container "fink" vs "bob"?

All share new-backend:locally; the command: (+ bind-mounted config) selects the service. One binary, many services.
Explain Compose in one breath — keys, DNS, and the depends_on gotcha.
recall, then click to reveal
DOCKER COMPOSE declares a multi-container app in one YAML and runs it with 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:.
✅ Part 1 complete You can now define a container (Lesson 1), read the image/Dockerfile that builds it (Lesson 2), run it with the right ports/volumes/env/network (Lesson 3), and wire many of them with Compose (Lesson 4). That's the whole foundation. Part 2 steps up a level: this repo's env × org model and the second local stack — kind + Skaffold — which is the direct on-ramp to Course 2 (Kubernetes).
Ready for Part 2 (env×org + the two local stacks), or want a mock interview on Part 1 first? Six courses built, no retention check yet — this is a great place to prove the foundation stuck. Ask me.

1. Docker — Docker Compose overview; Compose file reference.

2. Docker — Control startup order (depends_on & service_healthy).