Why docker compose depends_on doesn't wait for the DB — ending connection refused with healthcheck
Ever run docker compose up to bring up your app and DB together, only to watch the app container die with connection refused? "I clearly set depends_on: [db] — why didn't it wait for the DB?" This post fixes that mental model and gives you copy-paste healthcheck + condition recipes for Postgres, MySQL, Redis, and HTTP so you can kill the problem at the root.
"I set depends_on — why is it still dying?" — the classic failure scenario
The most common form looks like this.
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
api:
build: .
depends_on:
- db # ← when " "When you up, the console prints logs like this and the app exits.
api-1 | could not connect to server: Connection refused
api-1 | Is the server running on host "db" (172.18.0.2) and accepting
api-1 | TCP/IP connections on port 5432?
api-1 | Error: dial tcp 172.18.0.2:5432: connect: connection refused
api-1 exited with code 1The db container is clearly up, but the Postgres process inside hasn't finished initializing yet (data directory creation, WAL prep), so it isn't accepting connections on port 5432. The app can't survive that 0.5–3 second gap and dies.
Mental model: depends_on only guarantees start order
The one-liner: depends_on only controls container start order. It knows nothing about whether the process inside the container is ready.
[ depends_on ]
db created → started ──┐
├─→ api started ( !)
│
db Postgres boot ...... ─┘ ← = connection refusedIn other words, "the container started" ≠ "the DB is ready to accept connections (healthy)". Default short-syntax depends_on only waits until started, so it's useless in front of a DB or message queue that needs readiness. To fix this you need to (1) define a healthcheck that decides whether the service is healthy, and (2) make depends_on wait for that signal with condition: service_healthy.
The fix, part 1 — four copy-paste healthcheck recipes
First, the healthcheck options:
| Option | Meaning | Recommended (for DBs) |
|---|---|---|
test | Health probe command. Exit code 0 means healthy | Per-service ping command |
interval | How often to check | 5s |
timeout | Time limit for a single check | 5s |
retries | Consecutive failures before unhealthy | 5 |
start_period | Boot grace period (failures inside this window don't count) | 10s–30s |
Tip:
CMD-SHELLruns through a shell, so||and env var expansion work.CMDexecs without a shell. To avoid colliding with Compose env vars, escape variables that should be evaluated inside the container as$$.
Postgres
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
healthcheck:
# $$ → compose
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10sMySQL
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: appdb
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s # MySQLRedis
redis:
image: redis:7
healthcheck:
# PONG
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5HTTP app (its own /health endpoint)
web:
build: .
healthcheck:
# curl alpine wget -qO-
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15sThe fix, part 2 — depends_on long syntax + condition
Once you've defined the healthcheck, declare the condition on the dependent side with long syntax.
services:
api:
build: .
depends_on:
db:
condition: service_healthy # db healthy whento
migrate:
condition: service_completed_successfully # 0 whento
redis:
condition: service_started # OKHere's how the three conditions differ.
| condition | What it waits for | When to use it |
|---|---|---|
service_started | Container start | Sidecar services that don't need readiness |
service_healthy | healthcheck reports healthy | DBs, queues, anything you connect to |
service_completed_successfully | Exits with code 0 | DB migration / seed jobs |
Version note: The condition syntax is part of the Compose Spec and works correctly with Docker Compose v2 (the docker compose plugin). Older version: "3.x" schemas with short-syntax (depends_on: [db]) ignored condition. These days the version key itself is deprecated, so drop it and standardize on current docker compose.
When you can't use healthcheck — comparing alternatives
Sometimes the image has no ping tool, or you can't add a healthcheck. Here are your options.
| Approach | How it works | Pros | Cons / when to use |
|---|---|---|---|
wait-for-it.sh | Poll until the TCP port is open, then run the command | No extra deps, simple | Only sees the port, not "ready for queries". Fine for light waits |
dockerize -wait | Wait on port/HTTP + templates | Supports both TCP and HTTP | Need to add a binary. For waiting on multiple deps |
| App-level retry (backoff) | The app reconnects itself | Infra-independent, most robust | Requires code. Best practice |
App-level backoff example (Node.js):
async function connectWithRetry(retries = 10, delay = 1000) {
for (let i = 0; i < retries; i++) {
try { return await db.connect(); }
catch (e) {
console.warn(`DB failed, when ${i + 1}/${retries}`);
await new Promise(r => setTimeout(r, delay * 2 ** i)); //
}
}
throw new Error("DB failed");
}In practice, healthcheck + condition kills about 90% of boot-timing issues, but it can't cover a DB that briefly drops and recovers in production (rolling updates, network blips). From a 12-factor / cloud-native angle, healthcheck is a recommended accelerator; app-level retry is the required safety net. They complement each other, they don't compete.
Common mistakes FAQ
See also: official docs
The primary source for the behavior, config, and errors in this post is the official docs below. Check there for version-specific options and exact semantics.
FAQ
Q. I set condition: service_healthy but it still goes unhealthy and the app never starts.
A. Almost always start_period is too short. For images with long init like MySQL, bump it to start_period: 30s or more. Failures inside that window don't count toward retries, so transient boot failures get ignored.
Q. The healthcheck test keeps failing. The command looks right.
A. (1) If you use shell syntax (||, variables) with CMD, it fails → switch to CMD-SHELL. (2) If you write Compose variables as $, Compose interpolates them first → escape variables that should be evaluated in the container as $$. (3) Alpine images often don't have curl, so use wget -qO- instead.
Q. db is healthy but the app still dies occasionally.
A. healthcheck only solves boot timing. Transient disconnects in production need app-level retry (exponential backoff). Also pair it with a restart policy like restart: unless-stopped, but watch that you don't pile up logs from infinite restarts while the healthcheck is unhealthy.
Wrap-up — practical checklist and a final template
- Remember: start order (started) ≠ ready (healthy).
- Define a healthcheck on DBs and queues.
- Wire it with
condition: service_healthyon dependents. - Still put app-level retry in place (production safety net).
Final compose.yml you can copy-paste:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
migrate:
build: .
command: ["npm", "run", "migrate"]
depends_on:
db:
condition: service_healthy
api:
build: .
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
restart: unless-stoppedNo more getting paged at 3 a.m. by connection refused. The moment you add a condition to depends_on, "why isn't it waiting?" turns into "it just waits."
Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us
This article has no recorded human check (date and scope) yet.
Comments
Be the first to comment.