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 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 부팅 중...... ─┘ ← 아직 포트 안 열림 = 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 # MySQL은 초기화가 길어 넉넉히Redis
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 될 때까지 대기
migrate:
condition: service_completed_successfully # 마이그레이션 잡이 0으로 끝날 때까지
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 연결 실패, 재시도 ${i + 1}/${retries}`);
await new Promise(r => setTimeout(r, delay * 2 ** i)); // 지수 백오프
}
}
throw new Error("DB 연결 최종 실패");
}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."
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.