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:8

On 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: 5

Now 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-1

Common 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.

Try it yourself

Generate a ready-to-run Docker Compose setup for your stack — compose.yml, nginx config, .env, and a README, in seconds.

Open the generator →