sehaj/learn
Docker/06 Compose

06.03 · with code · 10 min

Startup order and healthchecks

Make Compose start the database before the API, and wait until it's actually ready, not just running.

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:

not enough
api:
  depends_on: [db, cache]    # waits for the containers to start, nothing more

Waiting for healthy

Give each dependency a healthcheck, then depend on its health:

compose.yaml
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: 5

Both images ship a client that can check readiness: pg_isready and redis-cli ping. Now up waits:

❯terminal
$ docker compose up -d
Container notes-db-1 Started
Container notes-cache-1 Started
Container notes-db-1 Waiting
Container notes-cache-1 Waiting
Container notes-cache-1 Healthy
Container notes-db-1 Healthy
Container notes-api-1 Starting
Container notes-api-1 Started
Container notes-api-1 Waiting
Container notes-api-1 Healthy
Container notes-proxy-1 Starting
Container 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

  • CMD vs CMD-SHELL. CMD runs a program directly, like exec-form CMD in a Dockerfile. CMD-SHELL runs 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

ConditionWaits until the dependency…Use for
service_started (default)container has startedordering only
service_healthyreports healthydatabases, caches, anything with a startup delay
service_completed_successfullyran and exited with 0one-off jobs like migrations before the app starts
Predict first

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?

Commit to an answer before you read on.

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.

CheckpointWatch the order

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

MidfreqMust-knowScenario

Your app container starts before the database is ready and crashes. How do you fix it with Compose?

Report a mistakeEdit this page
☰ Compose06.03 · 34/44next →