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