Topic 5.2
Startup Order, Dependencies & Healthchecks
In one line
depends_on alone only controls START ORDER, not READINESS — a subtle distinction that causes a huge number of 'my app crashes on startup because the database wasn't ready yet' bugs.
Think of it like this
A relay race where the second runner starts the moment the first runner LEAVES the starting blocks, rather than waiting for them to actually reach and hand off the baton. depends_on alone is exactly this — it starts the DEPENDENT container the moment the dependency container process BEGINS, not when it's actually ready to serve requests.
Key ideas
- 01
Plain
depends_on: [db]guarantees Docker starts thedbcontainer's PROCESS before startingapp— but a database process starting is NOT the same moment as the database being ready to accept connections (Postgres, for example, does real startup/initialization work after its process launches). Your app can start and immediately fail to connect, even thoughdepends_onwas correctly configured. - 02
The fix: add a
healthcheckto the dependency service, and use the extendeddepends_onsyntax with acondition: service_healthy— this tells Compose to genuinely WAIT until the dependency reports itself healthy (not just 'process started') before starting the dependent service. - 03
A healthcheck is a command Docker periodically runs INSIDE the container to determine if the app is actually working, not just technically running (the same HEALTHCHECK concept from Phase 2.4's Dockerfile best practices, expressible directly in Compose too) — for Postgres, a common healthcheck is
pg_isready; for a web app, hitting its own health endpoint. - 04
This same 'started vs actually ready' distinction is exactly why production orchestrators (Kubernetes) have separate concepts for 'liveness' (is the process alive) and 'readiness' (is it ready to serve traffic) — Compose's healthcheck-based
depends_onis the simpler, single-host ancestor of that same idea. - 05
Beyond startup ordering, apps should ALSO be written defensively regardless of Compose configuration — retrying a failed database connection a few times with backoff on startup, rather than crashing immediately on the first failed attempt, is a resilience habit worth having independent of Compose's guarantees.
In your stack
- →
Spring Boot's own connection pool (HikariCP) has configurable connection-retry behavior, and Spring can be configured to retry its initial datasource connection rather than failing fast — combining THIS with a proper Compose healthcheck-based depends_on gives you two independent layers of protection against the exact same startup-race class of bug.
Code & diagrams
The corrected version — app genuinely WAITS for db to be ready, not just started.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/postgres
depends_on:
db:
condition: service_healthy # <- waits for HEALTHY, not just "started"
volumes:
pgdata:Plain depends_on only guarantees the top row — the race happens in the gap before the bottom row is true.
Explain it without notes
Why can an app container crash on startup with a 'connection refused' error even though its compose.yaml correctly lists depends_on: [db]?
What's the practical difference between a container being 'started' and a container being 'healthy', and why does an orchestrator care about both separately?
Practice
Take a compose.yaml WITHOUT a healthcheck-based depends_on and try to reliably reproduce the startup-race failure (may require a few attempts, or artificially slowing db startup) — then add the healthcheck fix and confirm the failure stops happening.
Write a healthcheck for a simple web service that curls its own /health or / endpoint, and use docker compose ps to watch its status transition from 'starting' to 'healthy'.
Trade-offs
- ↔
Healthchecks add a small amount of ongoing overhead (Docker runs the check command repeatedly, forever, at the configured interval) — negligible for almost every real workload, and a worthwhile trade for eliminating an entire class of flaky startup-race bugs.
Done when you can
I use condition: service_healthy for any service whose dependents genuinely need it to be READY, not just started.
I can explain the difference between a container being started and being healthy.
I write healthchecks that actually verify functionality, not just process existence.