The core idea
depends_on tells Compose to start one service before another. On its own, it only waits until the dependency's
container has started, not until the program inside is ready. Add condition: service_healthy and a healthcheck,
and Compose waits until the dependency reports healthy.
Started is not ready
Postgres takes a few seconds after its container starts before it accepts connections. With plain depends_on, the
API can start in that gap and fail its first connection:
api:
depends_on: [db, cache] # waits for the containers to start, nothing moreWaiting for healthy
Give each dependency a healthcheck, then depend on its health:
api:
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
cache:
image: redis:8-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5Both images ship a client that can check readiness: pg_isready and redis-cli ping. Now up waits:
$ docker compose up -dContainer notes-db-1 StartedContainer notes-cache-1 StartedContainer notes-db-1 WaitingContainer notes-cache-1 WaitingContainer notes-cache-1 HealthyContainer notes-db-1 HealthyContainer notes-api-1 StartingContainer notes-api-1 StartedContainer notes-api-1 WaitingContainer notes-api-1 HealthyContainer notes-proxy-1 StartingContainer notes-proxy-1 Started
Output trimmed. The API starts only after both dependencies are healthy, and the proxy only after the API is, because
the full file also gives api a healthcheck and makes proxy depend on it.
Two details in that healthcheck
CMDvsCMD-SHELL.CMDruns a program directly, like exec-formCMDin a Dockerfile.CMD-SHELLruns the string through/bin/sh, which you need for$VARIABLES.$$. Compose replaces${VAR}in the file with values from your environment before anything runs.$$escapes that, so$${POSTGRES_USER}reaches the container as${POSTGRES_USER}and the shell inside the container expands it from the container's environment.
The three conditions
| Condition | Waits until the dependency… | Use for |
|---|---|---|
service_started (default) | container has started | ordering only |
service_healthy | reports healthy | databases, caches, anything with a startup delay |
service_completed_successfully | ran and exited with 0 | one-off jobs like migrations before the app starts |
With condition: service_healthy on db, the API starts after Postgres is healthy. Later, Postgres restarts while the API is running. What protects the API now?
That's why the notes API keeps its own retry loop even though Compose now orders startup. Ordering makes the happy path clean; retries make the unhappy path survivable.
Run docker compose down -v then docker compose up -d and read the Waiting/Healthy sequence. Change the database's healthcheck to ["CMD", "false"] and run up again: what does Compose report about the API? Restore it afterwards.
STUCK? ▸
If the API starts before the database is healthy, check the condition: form is used. The list form depends_on: [db] only means service_started.
Recap
Interview questions
Your app container starts before the database is ready and crashes. How do you fix it with Compose?