Docker Compose Healthchecks and depends_on: Start Services in the Right Order
depends_on alone only waits for a container to start, not for it to be ready — which is why your app crashes with ECONNREFUSED while Postgres boots. How to write healthchecks for Postgres, MySQL, Redis and your own app, use condition: service_healthy, tune intervals, and debug unhealthy containers.
A classic Docker Compose bug: you run docker compose up, the database and app start together, and the app immediately crashes with ECONNREFUSED because Postgres is still initialising. You added depends_on: [db], so why?
Because plain depends_on only waits for the container to start, not for the service inside to be ready. The fix is a healthcheck plus condition: service_healthy. (ECONNREFUSED: Database Connection)
The pattern
services:
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
interval: 5s
timeout: 3s
retries: 10
start_period: 20s
app:
build: .
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/app
Now Compose starts db, waits until its healthcheck passes, and only then starts app.
The healthcheck options
| Option | Meaning |
|---|---|
test |
The command to run. Exit code 0 = healthy |
interval |
How often to run it |
timeout |
How long a single check may take |
retries |
Consecutive failures before "unhealthy" |
start_period |
Grace period at startup where failures don't count |
start_interval |
How often to check during the start period (newer Docker versions) |
CMD runs the command directly; CMD-SHELL runs it through a shell (needed for ||, pipes or variables).
Healthchecks for common services
Postgres:
test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
MySQL / MariaDB:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
Redis:
test: ["CMD", "redis-cli", "ping"]
Your own web app — hit a health endpoint. Slim images often lack curl, so use whatever the image has:
# Node image without curl
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
The three depends_on conditions
depends_on:
db:
condition: service_healthy # wait for healthcheck to pass
migrate:
condition: service_completed_successfully # wait for a one-off job to exit 0
cache:
condition: service_started # the default: just started
service_completed_successfully is perfect for a migrations container that must finish before the app starts. (Prisma Migrations in Production)
Debugging an unhealthy container
docker compose ps # shows (healthy) / (unhealthy) / (starting)
docker inspect --format '{{json .State.Health}}' myproject-db-1
The inspect output includes the last few check results with their output, which usually tells you exactly what failed: wrong username, missing binary in the image, wrong port.
Healthchecks don't restart things (in plain Docker)
In plain Docker and Compose, an unhealthy container is just marked unhealthy — it isn't restarted automatically. restart: unless-stopped only reacts when the process exits. Orchestrators like Docker Swarm and Kubernetes act on health status; with Compose, your app should still handle a database blip by retrying connections. (Exponential Backoff, Docker Container Keeps Restarting)
Don't make the healthcheck too clever
A healthcheck that queries the database, calls three APIs and checks disk space will fail for reasons that have nothing to do with whether this container works — and with load balancers, that takes your whole app offline. Check that this service can answer.
The summary
depends_onwaits for start, not readiness. Add ahealthcheckandcondition: service_healthy.- Use
pg_isready,mysqladmin ping,redis-cli ping, or your app's/health. - Use
service_completed_successfullyfor migration jobs; debug withdocker inspect.
EasySpawn runs your app and its database on a managed server with startup order and health monitoring handled, and Claude Code can debug a container that won't go healthy. See how it works or join the waitlist.
Related: Docker Compose for Local Development · Docker vs Docker Compose · Docker Compose Networking · PID 1 in Containers
Keep reading
How to Self-Host n8n: Docker, Postgres, HTTPS and Backups
A production-minded guide to self-hosting n8n: Docker Compose with Postgres instead of SQLite, the environment variables that matter (encryption key, webhook URL, timezone), HTTPS behind a reverse proxy, backups of both the database and the encryption key, updates, and when queue mode is worth it.
Docker Compose Networking: How Containers Talk to Each Other
How services in a Compose project find each other by name, why localhost doesn't work between containers, ports vs expose, custom and external networks, reaching the host from a container, connecting two Compose projects, and the security gotcha of published ports.