Docker Compose Healthchecks Explained
"It works if I restart the containers, but not on a fresh docker compose up" is almost always a healthcheck problem in disguise. Here's what's actually happening, and how to fix it properly instead of papering over it with a sleep.
depends_on only waits for the container to start
It's a reasonable assumption that depends_on makes Compose wait until a dependency is ready. It doesn't. By default it only waits until the dependency's container has started — which, for something like MySQL, happens well before the database is actually accepting connections.
services:
php:
image: php:8.5-fpm
depends_on:
- mysql # only waits for the container to start, not for MySQL to be ready
mysql:
image: mysql:8On a fresh volume, MySQL's own first-boot initialization can take several seconds. If your app tries to connect during that window, it fails — intermittently, which makes it particularly annoying to debug.
What healthcheck actually does
A healthcheck: block tells Docker to run a command inside the container on a repeating interval to determine whether the service is actually ready, not just running. Docker tracks the result as starting, healthy, or unhealthy.
services:
mysql:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5- test — the command Docker runs; exit code 0 means healthy.
- interval — how often to run it.
- timeout — how long to wait for the command before treating it as a failure.
- retries — consecutive failures required before marking the container
unhealthy. - start_period (optional) — a grace window at startup where failures don't count against
retries, useful for slow-starting services like Elasticsearch.
Actually waiting for it: condition: service_healthy
A healthcheck by itself doesn't change startup order — you still need to tell the dependent service to wait for it, using the long form of depends_on:
services:
php:
image: php:8.5-fpm
depends_on:
mysql:
condition: service_healthy
mysql:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5Now php won't start until mysqladmin ping has succeeded — a real readiness check, not a guess. Every database service the generator on this site produces already ships with a matching healthcheck, precisely for this reason.
Checking health status yourself
docker compose ps
# NAME STATUS
# project-mysql-1 Up 12 seconds (healthy)
docker inspect --format='{{json .State.Health}}' project-mysql-1Common pitfall: the test command isn't in the image
A healthcheck fails immediately — every time — if the command it runs isn't installed in that image. Slim/alpine-based images frequently lack curl or wget. Two ways out: use whatever the image does ship (e.g. mysqladmin in the MySQL image, redis-cli in the Redis image), or install what you need via a custom Dockerfile if you must use curl (see Docker Compose vs Dockerfile). Wrapping the command in CMD-SHELL instead of CMD lets you use shell features like pipes, as the generator does for its Elasticsearch healthcheck.
Generate a ready-to-run Docker Compose setup for your stack — compose.yml, nginx config, .env, and a README, in seconds.
Open the generator →