You add depends_on: [mysql], run docker compose up, and the API dies
with a connection refused. Compose did exactly what you asked. What you asked for was not what you
wanted.
started is not ready
depends_on waits for the container, not for the service inside it.
A container is "started" the instant its main process launches — and MySQL takes several more
seconds after that to initialise its data directory, run its own start-up, and begin accepting
connections on the socket.
So the sequence is: MySQL's container starts, Compose considers the dependency satisfied, the API starts, the API connects, MySQL is not listening yet, the API exits.
The symptom is a stack that works when you run up twice — because the second time
MySQL was already warm — and fails on a clean machine or in CI. Which is the worst kind of bug to
have, since it only appears where you cannot easily look.
A healthcheck
The fix is to define what "ready" means, and then wait for that:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 5s
timeout: 5s
retries: 20
start_period: 30sDocker runs test inside the container on a schedule. Exit 0 is healthy, anything
else is not.
The four timings, each doing something specific:
interval — how often to probe once it is up and running.
timeout — how long one probe may take before it counts as a
failure. Worth setting deliberately: a probe that hangs rather than failing is what makes a stack
sit there for minutes.
retries — consecutive failures before the container is marked
unhealthy.
start_period — the grace window at the beginning during which
failures do not count toward retries. This is the one people leave out and then
wonder why a slow-starting service is marked unhealthy and killed before it ever had a chance. A
JVM application with Liquibase migrations to run needs a generous one.
condition: service_healthy
The healthcheck alone changes nothing about ordering. It has to be depended on:
depends_on:
mysql:
condition: service_healthyNote the shape. The list form — depends_on: [mysql] — cannot express a condition;
you need the map form to say anything more than "start it first".
Three conditions exist:
service_started the default. Container exists. Says nothing about readiness.
service_healthy the healthcheck is passing.
service_completed_successfully the container ran and exited 0 — for a
migration or seed job that must finish first.And now the output says what you want it to:
$ docker compose up -d
Container pizza-mysql-1 Starting
Container pizza-mysql-1 Started
Container pizza-mysql-1 Waiting
Container pizza-mysql-1 Healthy
Container pizza-api-1 Starting
Container pizza-api-1 StartedWaiting, then Healthy, then the API. That is the race gone.
Not everything should wait
Worth resisting the urge to make every dependency service_healthy. The web tier in
this stack deliberately does not:
depends_on:
- apinginx serves static files perfectly well before the API is up, and it resolves api
lazily, per request. Waiting would delay the login page for no benefit — and would mean a slow API
start-up makes the whole site unavailable rather than just the data on it.
Ask what actually breaks if the dependency is not ready. If the answer is "the first request
fails and then it works", service_started is right.
Check with something the image has
The rule from lesson 13, because it is the mistake that costs the most time here: a healthcheck
that cannot run its own command reports unhealthy forever, which looks identical to a
service that failed to start.
The demo app's Artemis broker has no curl in its image, so a curl-based probe
"fails" every time while the broker serves happily. It uses the tool that is present:
test: ["CMD", "/var/lib/artemis-instance/bin/artemis", "check", "node", "--up",
"--user", "admin", "--password", "admin"]Which also probes the messaging port rather than the web console — the port clients actually use.
Handy forms for common images:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
test: ["CMD-SHELL", "pg_isready -U stayhub -d stayhub"]
test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health || exit 1"]
test: ["CMD-SHELL", "wget -qO- http://localhost:8085/actuator/health || exit 1"]CMD runs the command directly; CMD-SHELL wraps it in
/bin/sh -c, which you need for pipes, || and variables.
⚠️ curl -f is doing real work in those: without it, curl exits 0 on an HTTP 500,
and your healthcheck cheerfully reports a broken service as healthy.
up --wait
docker compose up -d --waitBlocks until every service with a healthcheck is healthy, then returns — and returns non-zero if one never gets there. This is what you want in CI, where the next step is a test suite that assumes the stack is up.
Retry anyway
Healthchecks fix start-up ordering. They do not fix a database that restarts at three in the
morning while your API is running, and depends_on has nothing to say about that — it
applies to up, not to the rest of the service's life.
So the application still needs a connection pool that reconnects, and start-up retries with backoff. What healthchecks buy you is that the common case stops being a coin flip; they are not a substitute for an application that survives its dependencies going away.