Compose applications

Add health and startup handling

Distinguish process startup from service readiness and make the API tolerate a database that is still initializing.

A container being “running” tells you the process started. It doesn’t tell you the process is ready to do its job.

PostgreSQL is a good example. Right after start, it may still be creating its first database or replaying its write-ahead log. The container is up, but a connection attempt fails. If the API starts at the same moment and connects immediately, it crashes before the database is ready.

Add a health check

A health check is a command Docker runs inside the container at intervals. If it succeeds, the container is healthy. Compose can then wait for that state before starting the services that depend on it:

database:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U notes -d notes"]
    interval: 5s
    timeout: 3s
    retries: 10
api:
  depends_on:
    database:
      condition: service_healthy

pg_isready ships with the PostgreSQL image and checks whether the server accepts connections. Docker runs it every 5 seconds, gives it 3 seconds to answer, and marks the container unhealthy after 10 failures in a row. depends_on with condition: service_healthy makes Compose hold the API until the database passes.

Recreate the stack and watch it:

docker compose up -d
docker compose ps

The database goes from starting to healthy in a few seconds, and only then does the API container start.

Write a check that means something

The command runs inside the database container, so it can only use tools that exist in that image. curl isn’t in postgres:17-alpine, for instance.

Test the behavior your app needs, and keep it cheap. pg_isready is perfect: fast, read-only, specific. A check that always returns 0 proves nothing. A check that runs a heavy query every 5 seconds can cause the outage it’s supposed to detect.

When a service stays unhealthy

Docker keeps the last few probe results. Read them with:

docker inspect database --format '{{json .State.Health}}'

You get the status, the failure count, and the output of each probe. In Compose the container is named notes-api-database-1, so use that name, or docker compose ps -q database to find it.

Health checks don’t replace retries

depends_on helps only at startup. A minute later the database can restart for its own reasons, and the API is on its own. So the API still needs to handle connection failures: retry a few times with a growing delay, log what it’s waiting for, and exit with a clear error if the database never comes back. The health check surfaces the problem. The retry logic survives it.

Try this: add the health check, recreate the stack, and run docker compose ps repeatedly while PostgreSQL moves from starting to healthy.

Lesson completed