01The Startup Race Condition
Here's a scenario every Docker Compose user has hit: your web app starts before the database is ready, crashes with "connection refused," and you have to manually restart it. Or worse, it starts, connects to an empty database before migrations run, and serves errors to users.
Docker Compose's depends_on without health checks only controls startup order — it ensures container A starts before container B. But "started" doesn't mean "ready." A PostgreSQL container can take 5-10 seconds to initialize on first run, and your app container starts in under a second.
Health checks solve this by letting you define what "ready" actually means for each service, and depends_on conditions let you gate startup on those health checks.
02Writing Good Health Checks
A health check is a command that Docker runs periodically inside the container. If it exits with 0, the container is healthy. If it exits with 1, it's unhealthy. Here are health checks for the most common services:
[docker-compose.yml]
1services: 2 postgres: 3 image: postgres:16-alpine4 healthcheck: 5 test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]6 interval: 5s7 timeout: 5s8 retries: 59 start_period: 10s1011 mysql: 12 image: mysql:8.413 healthcheck: 14 test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]15 interval: 5s16 timeout: 5s17 retries: 51819 redis: 20 image: redis:7-alpine21 healthcheck: 22 test: ["CMD", "redis-cli", "ping"]23 interval: 5s24 timeout: 3s25 retries: 52627 app: 28 image: myapp:latest29 depends_on: 30 postgres: 31 condition: service_healthy32 redis: 33 condition: service_healthySet start_period for services that need initialization time (like databases on first run). During the start period, failed health checks don't count toward the retry limit.
03Depends On Conditions
The depends_on directive supports three conditions:
service_started: (default) Wait for the container to start, not for it to be ready. This is what you get with the simple depends_on: [db] syntax.
service_healthy: Wait for the container's health check to pass. This is what you want for databases, message queues, and any service your app needs to be fully ready.
service_completed_successfully: Wait for the container to run and exit with code 0. Useful for init containers that run migrations or seed data before your app starts.
[docker-compose.yml]
1services: 2 db: 3 image: postgres:16-alpine4 healthcheck: 5 test: ["CMD-SHELL", "pg_isready"]6 interval: 5s7 timeout: 5s8 retries: 5910 migrate: 11 image: myapp:latest12 command: ["npm", "run", "migrate"]13 depends_on: 14 db: 15 condition: service_healthy1617 app: 18 image: myapp:latest19 depends_on: 20 migrate: 21 condition: service_completed_successfully04Combining with Restart Policies
Health checks work hand-in-hand with restart policies. If a container becomes unhealthy, Docker can automatically restart it:
restart: unless-stopped is my recommendation for most services. It restarts the container if it crashes but doesn't restart it if you intentionally stop it.
For services with health checks, Docker will restart unhealthy containers according to the restart policy. Combined with depends_on conditions, this gives you a self-healing stack: if the database restarts, dependent services wait for it to become healthy again before reconnecting.
This is one of the simplest ways to improve the reliability of your Docker Compose deployments. Every recipe on docker.recipes includes appropriate health checks for database and cache services — browse any of our full-stack recipes to see the pattern in action.