How do you handle service dependencies and health checks in Docker Compose, and why does depends_on alone fail to guarantee a dependent service is actually ready?
Quick Answer
Plain depends_on only waits for a container to start (its PID 1 process launches), not for the application inside it to be ready to accept traffic — a Postgres container can report 'started' seconds before it finishes replaying WAL files and can actually accept connections. Pairing depends_on with condition: service_healthy and a real healthcheck (like pg_isready) makes Compose wait for actual readiness before starting the dependent service.
Detailed Answer
Imagine a relay race where the baton handoff rule is 'start running as soon as your teammate leaves the starting blocks,' rather than 'start running once your teammate is actually within arm's reach.' The first rule sounds fine until you realize the runner might trip, false-start, or take a few strides before finding their stride — and now you're reaching for a baton that isn't there yet. Docker Compose's default depends_on is that first rule: it guarantees start order, not readiness. Your checkout-worker container gets scheduled to start the instant the postgres container process launches, not once Postgres has actually finished its startup sequence and opened its listening socket for real queries.
Compose was designed this way because 'started' and 'ready' are fundamentally different concepts that only the application itself can answer. Docker doesn't know that Postgres needs to run crash recovery, load extensions, and open a TCP listener before it's actually usable — from Docker's point of view, a container is 'up' the moment its entrypoint process exists. The healthcheck mechanism exists specifically to let each service define, in its own terms, what 'ready' means: for Postgres that's pg_isready succeeding, for an HTTP API that's a 200 response on /health, for a queue consumer that might be a successful connection handshake to the broker.
Internally, when you define a healthcheck block, the Docker Engine runs the specified test command inside the container on a schedule (interval), and gives it timeout seconds to complete. If it fails retries times in a row, the container's health status flips from starting to unhealthy; if it succeeds, it flips to healthy. When a dependent service declares depends_on: db: condition: service_healthy, Compose's orchestration logic polls the dependency's health status before issuing the start call for the dependent container — it literally will not create checkout-worker's container until postgres reports healthy, or it fails the whole up command if postgres never becomes healthy within its healthcheck's retry budget.
At production and CI scale, this matters most during full-stack spin-ups — integration test pipelines that bring up a database, a message broker, and five services are exactly where naive depends_on causes flaky, non-deterministic test failures: the app connects before the DB is ready, throws a connection-refused error, and the whole pipeline run fails intermittently, which teams often misdiagnose as 'flaky tests' rather than a missing health check. What to monitor: healthcheck failure counts in docker inspect (an unhealthy container that never recovers usually means a bad healthcheck test command, not necessarily an unhealthy app), and startup latency creep, since every added health-checked dependency adds to the critical path of docker compose up.
The gotcha experienced engineers hit: healthchecks only run after the container's start_period (a grace window during which failures don't count against retries) has elapsed, and if you don't set start_period for a slow-starting JVM-based service, the healthcheck can exhaust its retries and mark the container unhealthy before the app has even finished its warm-up — causing Compose to declare the dependency permanently broken even though it would have become healthy ten seconds later.