docker compose depends_on DB 연결 실패 — healthcheck로 준비까지 기다리기

@JavaPark · 2026년 11월 7일 · 12 min read

docker compose depends_on DB 연결 실패 커버 — db 시작 0초, app 접속 0.26초에 거절, db 준비 2.2초
docker compose depends_on DB 연결 실패 커버 — db 시작 0초, app 접속 0.26초에 거절, db 준비 2.2초

안녕하세요, 자바파커입니다.

docker compose up 을 했더니 이런 줄이 찍힙니다. psql: error: connection to server at "db" (172.20.0.2), port 5432 failed: Connection refused 분명히 depends_on: [db] 를 걸어 뒀는데요.

결론부터 말씀드리면 — depends_on은 DB 컨테이너가 "시작"되기를 기다릴 뿐, DB가 접속을 받을 "준비"가 되기를 기다리지 않습니다. 준비까지 기다리게 하려면 DB에 healthcheck를 넣고, 앱 쪽 depends_on에 condition: service_healthy를 걸면 됩니다.

이 차이를 제 PC에서 직접 재 봤습니다. 앱은 DB 컨테이너가 시작되고 0.26초 만에 접속을 시도하는데, DB가 실제로 접속을 받기까지는 2.2초가 걸렸습니다. 그 사이에 들어온 접속은 전부 거절됩니다. 이 장면을 41초 영상으로도 정리했습니다.

docker compose depends_on 영상 — 앱은 0.26초에 접속, DB 준비는 2.2초, 그 사이 접속은 실패하는 타임라인
docker compose depends_on 영상 — 앱은 0.26초에 접속, DB 준비는 2.2초, 그 사이 접속은 실패하는 타임라인


depends_on은 무엇을 기다리나 — 시작과 준비는 다르다

카페로 비유하면 쉽습니다.

카페 컨테이너
시작(started) 셔터를 올렸다 DB 컨테이너 프로세스가 떴다
준비(ready) 커피 머신 예열이 끝났다 DB가 5432 포트로 접속을 받는다
depends_on만 썼을 때 셔터가 올라가자마자 손님이 들어와 주문한다 컨테이너가 뜨자마자 앱이 접속한다
결과 "아직 준비 중이에요" Connection refused

depends_on을 짧게 쓰면 Compose는 만드는 순서만 정합니다. db 컨테이너가 시작 상태가 되면 바로 app을 띄웁니다. postgres는 컨테이너가 뜬 뒤에도 데이터 디렉터리를 만들고(첫 실행), 임시 서버로 초기화를 하고, 다시 본 서버를 띄우는 과정을 거칩니다. 그동안 앱이 들어오면 거절당하는 거죠.

Docker 공식 문서 「Control startup order」도 같은 이야기를 합니다. 준비 상태까지 기다리려면 service_healthy 조건과 헬스체크를 쓰라고요.


docker compose depends_on 실패를 직접 재 봤습니다

말로만 하면 감이 안 와서, 앱 대신 psql로 접속을 한 번만 시도하는 구성을 만들어 다섯 번씩 돌렸습니다. 매번 down -v로 볼륨까지 지우고 띄웠습니다(= 첫 실행).

# compose.naive.yml — depends_on 만
services:
  db:
    image: postgres:16-alpine
    environment: { POSTGRES_PASSWORD: pw }
  app:
    image: postgres:16-alpine
    depends_on: [db]
    environment: { PGPASSWORD: pw }
    command:
      ["sh", "-c", "psql -h db -U postgres -tAc 'select 1' && echo APP_OK"]
측정 (5회, 중앙값) 결과
depends_on만 5번 중 5번 실패 — Connection refused
db 컨테이너 시작 → app 시작 0.26초
db 컨테이너 시작 → 실제 접속 가능 2.2초 (마지막 ready to accept connections 로그 기준)
healthcheck + service_healthy 5번 중 5번 성공 (up부터 앱 성공까지 약 4.8초)

※ 제 PC(Docker Desktop 28, postgres:16-alpine) 기준입니다. 숫자는 PC마다 다르지만, 앱이 DB보다 먼저 준비된다는 순서는 어디서나 같습니다.


해결 1 — healthcheck + condition: service_healthy

가장 정석적인 방법입니다. DB가 스스로 "준비 완료"를 알리게 하고, 앱은 그 신호를 기다립니다.

services:
  db:
    image: postgres:16-alpine
    environment: { POSTGRES_PASSWORD: pw }
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1"]
      interval: 1s
      timeout: 2s
      retries: 30
  app:
    image: my-app
    depends_on:
      db:
        condition: service_healthy
옵션 뜻 팁
test 준비됐는지 확인하는 명령 종료 코드 0이면 healthy
interval 확인 간격 개발용은 1~2초면 충분
timeout 한 번 확인할 때 기다리는 시간
retries 몇 번 실패하면 unhealthy interval × retries가 최대 대기 시간
start_period 처음 이 시간 동안의 실패는 세지 않음 초기화가 긴 DB에 유용

depends_on의 condition에는 세 가지가 있습니다.

condition 기다리는 것
service_started 컨테이너 시작 (짧은 문법과 같다)
service_healthy 헬스체크 통과
service_completed_successfully 컨테이너가 성공(0)으로 끝남 — 마이그레이션 작업에 쓴다

pg_isready에 -h 127.0.0.1을 붙이는 이유

postgres 이미지는 첫 실행 때 초기화용 임시 서버를 띄웁니다. 이 임시 서버는 TCP를 열지 않고(listen_addresses='') 유닉스 소켓으로만 받습니다. 이미지 안의 docker-entrypoint.sh에서 직접 확인했습니다.

-h 없이 pg_isready를 쓰면 유닉스 소켓으로 확인하기 때문에, 임시 서버가 떠 있는 순간을 "준비 완료"로 볼 여지가 있습니다. 정작 다른 컨테이너는 아직 TCP로 못 붙는데요.

솔직히 말하면 제 PC에서는 -h 없이도 5번 다 성공했습니다. 로그를 보니 임시 서버가 뜬 뒤 본 서버가 TCP를 열기까지의 틈이 0.2초 정도로 짧아서, 1초 간격 헬스체크가 그 순간에 걸리기 어려웠던 겁니다. 그래도 틈은 있으니 -h 127.0.0.1을 붙여 TCP까지 확인하는 편이 확실합니다.

다른 DB에서 자주 쓰는 헬스체크도 정리해 둡니다.

DB healthcheck test
PostgreSQL pg_isready -U postgres -h 127.0.0.1
MySQL / MariaDB mysqladmin ping -h 127.0.0.1
Redis redis-cli ping

해결 2 — 앱에서 재시도하기

솔직히 말하면, 헬스체크만으로는 반쪽입니다. 헬스체크는 "처음 뜰 때"만 지켜 줍니다. 운영 중에 DB가 잠깐 재시작되면 앱은 또 연결이 끊깁니다. 그래서 앱도 연결 실패 시 잠깐 기다렸다가 다시 시도하는 게 좋습니다.

// Node.js — 연결될 때까지 최대 10번, 1초 간격으로 재시도
async function connectWithRetry(connect, tries = 10) {
  for (let i = 1; i <= tries; i++) {
    try {
      return await connect()
    } catch (e) {
      if (i === tries) throw e
      console.log(`DB 연결 실패 (${i}/${tries}) — 1초 뒤 재시도`)
      await new Promise(r => setTimeout(r, 1000))
    }
  }
}

드라이버나 커넥션 풀마다 시작할 때의 동작이 다르니 꼭 확인해 보세요. 예를 들어 Spring Boot의 기본 커넥션 풀인 HikariCP는 기본 설정(initializationFailTimeout)이면 시작할 때 첫 연결을 못 얻으면 바로 실패합니다. 그래서 컨테이너 환경에서는 헬스체크가 더 중요해집니다. 헬스체크(처음) + 재시도(운영 중) 를 같이 쓰는 게 가장 튼튼합니다.

restart: on-failure는 어떤가요?

앱이 연결 실패로 죽으면 Compose가 다시 띄우게 하는 방법입니다. 결국은 붙긴 하지만, 로그에 에러와 재시작이 몇 번씩 찍혀서 진짜 장애와 구분하기 어렵습니다. 임시방편으로만 추천합니다.


쿠버네티스에서는 readinessProbe

쿠버네티스에는 depends_on 같은 시작 순서가 없습니다. 대신 각 파드가 readinessProbe로 "나 준비됐어"를 알리고, 준비된 파드에만 트래픽이 갑니다. 앱이 DB를 기다려야 한다면 initContainer로 기다리거나, 앞에서 본 재시도를 넣습니다. 쿠버네티스 기본은 「K8S에 대한 이해」에 정리해 두었습니다.


내 PC에선 되는데 CI에선 실패하는 이유

depends_on만 걸어 둔 프로젝트에서 자주 보는 증상입니다.

  • 내 PC: 볼륨이 이미 있어서 DB 초기화를 건너뛴다 → 준비가 빠르다 → 우연히 성공
  • CI: 매번 새 환경이라 초기화부터 한다 → 준비가 느리다 → 실패

제가 잰 2.2초도 첫 실행(초기화 포함) 기준입니다. 볼륨이 있으면 더 짧아지지만 0이 되지는 않습니다. 운에 맡기지 말고 헬스체크를 넣어 두세요.


자주 묻는 것

Q. depends_on만 써도 될 때가 있나요? A. 상대가 "뜨자마자 바로 받는" 서비스이거나, 앱이 재시도를 확실히 한다면 괜찮습니다. DB처럼 초기화 시간이 있는 서비스라면 service_healthy를 권합니다.

Q. healthcheck를 넣었는데 계속 unhealthy예요. A. docker inspect --format '{{json .State.Health}}' <컨테이너>로 헬스체크 로그를 보세요. 컨테이너 안에 명령(pg_isready, curl 등)이 없거나, 사용자·포트가 틀린 경우가 대부분입니다.

Q. 예전 문서의 wait-for-it.sh는요? A. 앱 이미지에 스크립트를 넣어 포트가 열릴 때까지 기다리는 방법입니다. 지금은 Compose의 service_healthy로 같은 일을 설정만으로 할 수 있어서, 새 프로젝트라면 헬스체크를 권합니다.


정리

하고 싶은 것 쓸 것
시작 순서만 정하기 depends_on: [db]
DB 준비까지 기다리기 db에 healthcheck + app에 condition: service_healthy
운영 중 DB 재시작에도 버티기 앱에서 재시도
쿠버네티스 readinessProbe (+ initContainer 또는 재시도)

한 줄로 기억하세요. 시작 말고, 준비를 기다리기.

여러분 compose 파일에는 healthcheck가 들어 있나요? 아니면 재시도로 버티고 계신가요? 댓글로 알려 주세요.

함께 보면 좋은 글: 커넥션 풀 고갈 — 멀쩡한 서버가 멈추는 진짜 이유

퀴즈

depends_on: [db] 만 적었을 때, Compose는 app을 띄우기 전에 무엇을 기다릴까요?

이 문제가 쉬웠다면 — 백엔드 CS 능력고사 20문제→
그림으로 말하는 백엔드 CS 20 무료 샘플 표지

판매 중 · 14,900원 · 무료 샘플 15쪽

그림으로 말하는 백엔드 CS 20

백엔드 CS 20문항을 면접에서 60초로 설명하는 연습. 샘플 5문항부터 받아 보세요.

→
@JavaPark
AI 시대의 개발자 도구, 실전 경험을 공유합니다