Docker Compose 서비스 의존성: healthcheck로 데이터베이스 시작 순서 문제 해결하기

금요일 밤 10시, 터미널에는 같은 오류 로그가 벌써 열 번 넘게 흘러가고 있습니다.
애플리케이션 컨테이너는 계속 재시작하고 데이터베이스는 올라왔지만 늘 한 박자 늦습니다. docker-compose.yml을 확인해 보니 depends_on도 설정되어 있는데, 왜 여전히 작동하지 않을까요?
많은 개발자가 이 문제를 겪습니다. 로컬 개발 환경에서 docker-compose up을 실행하면 처음 두 번은 어김없이 실패하고, 10여 초 동안 몇 차례 재시작해야 정상적으로 실행되곤 합니다.
원인은 간단합니다. Docker의 depends_on은 컨테이너 시작 순서만 제어할 뿐, 서비스가 실제로 준비되었는지는 확인하지 않습니다.
여기서는 다음 내용을 하나씩 살펴봅니다.
depends_on의 세 가지condition설정(90%는 기본값만 알고 있습니다)- PostgreSQL과 MySQL의 올바른
healthcheck작성법(전체 설정 포함) wait-for-it스크립트를 대체하는 현대적인 방법- 컨테이너를 안정적으로 시작하기 위한 문제 해결 체크리스트
depends_on만으로 부족한 이유: 시작 ≠ 준비
Docker 공식 문서에는 매우 중요하지만 놓치기 쉬운 문장이 있습니다.
Compose does not wait until a container is “ready”, only until it’s running.
즉, Compose는 컨테이너가 실행될 때까지만 기다리며 서비스가 실제로 사용 가능한지는 확인하지 않습니다.
컨테이너 시작과 서비스 준비 사이의 시간 차이
PostgreSQL 컨테이너의 시작 과정을 떠올려 보겠습니다.
- 0초: Docker가 컨테이너를 시작하고 postgres 프로세스가 실행됩니다. ←
depends_on은 여기서 다음 단계로 진행합니다. - 2초: 데이터 디렉터리를 초기화합니다.
- 5초: 설정 파일을 불러옵니다.
- 8초: init 스크립트가 있으면 실행합니다.
- 12초: 마침내 ready 상태가 되어 연결을 받을 수 있습니다.
이 사이에 12초의 간격이 있습니다. Web 애플리케이션이 1초째에 데이터베이스 연결을 시도하면 반드시 Connection refused가 발생합니다.
실제 사례는 더 심했습니다. 예전에 유지보수하던 레거시 프로젝트에서는 데이터베이스 초기화 스크립트가 500MB의 테스트 데이터를 가져오는 데만 40초가 걸렸습니다. 기본 depends_on 설정을 사용하면 애플리케이션 컨테이너가 데이터베이스에 연결되기 전에 최소 다섯 번은 crash 후 재시작해야 했습니다.
depends_on의 세 가지 condition
많이 알려지지 않았지만 depends_on은 세 가지 condition을 지원합니다.
services:
web:
depends_on:
db:
condition: service_started # 기본값, 컨테이너가 시작되면 OK
# condition: service_healthy # 상태 검사 통과까지 대기
# condition: service_completed_successfully # 컨테이너의 성공적인 종료까지 대기(init 컨테이너에 적합)
service_started(기본값): 컨테이너가 running 상태가 되면 바로 다음 단계로 진행합니다. depends_on을 설정해도 문제가 생기는 이유입니다.
service_healthy: healthcheck를 통과해 컨테이너 상태가 healthy가 될 때까지 기다립니다. 우리가 실제로 필요한 설정입니다.
service_completed_successfully: 컨테이너가 성공적으로 종료될 때까지 기다립니다(exit code 0). 데이터 마이그레이션 같은 일회성 작업에 적합합니다.
기본값이 service_healthy가 아닌 이유
service_healthy가 이렇게 유용하다면 왜 기본값이 아닐까요?
두 가지 이유가 있습니다.
- 모든 서비스에 상태 검사가 필요한 것은 아닙니다(예: 순수 stateless worker).
- 상태 검사는 직접 설정해야 하며, Docker는 서비스가 어떤 상태일 때 ‘준비 완료’인지 알 수 없습니다.
이제 상태 검사를 설정하는 방법을 살펴보겠습니다.
healthcheck 전체 설정 가이드
healthcheck의 원리는 단순합니다. Docker가 주기적으로 명령을 실행하고, 반환값이 0이면 정상, 1이면 비정상으로 판단합니다.
전체 healthcheck 설정
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"] # 검사 명령
interval: 10s # 10초마다 한 번 검사
timeout: 5s # 한 번의 검사는 5초 후 시간 초과
retries: 3 # 3회 실패해야 unhealthy로 표시
start_period: 30s # 시작 후 30초 안의 실패는 retries에 포함하지 않음
다섯 매개변수 모두 중요하므로 하나씩 살펴보겠습니다.
test: 검사 명령
두 가지 형식을 사용할 수 있습니다.
# 방법 1: shell 사용(권장)
test: ["CMD-SHELL", "pg_isready -U postgres"]
# 방법 2: 명령 직접 실행(shell을 거치지 않음)
test: ["CMD", "pg_isready", "-U", "postgres"]
대부분의 경우 파이프나 리디렉션 같은 shell 기능을 쓸 수 있는 CMD-SHELL이면 충분합니다.
흔한 함정: 검사 명령에 쓰는 도구가 이미지 안에 반드시 있어야 합니다. 예를 들어 curl로 HTTP 엔드포인트를 검사하지만 이미지에 curl이 설치되어 있지 않다면 healthcheck는 계속 실패합니다. 저도 이 문제를 겪고 30분을 헤맨 뒤에야 Dockerfile에 RUN apk add curl을 추가해야 한다는 사실을 발견했습니다.
interval: 검사 간격
얼마나 자주 검사할지를 정합니다. 너무 잦으면 리소스를 낭비하고, 너무 느리면 응답이 늦어집니다.
- 10초는 대부분의 상황에 적합한 기본값입니다.
- 데이터베이스 같은 핵심 서비스는 5초로 설정할 수 있습니다.
- 가벼운 서비스는 15~30초도 괜찮습니다.
timeout: 단일 검사 제한 시간
한 번의 검사에 허용할 시간입니다. 명령이 멈추면 Docker는 timeout만큼 기다린 뒤 포기합니다.
너무 짧으면 오탐이 발생하고 너무 길면 장애 감지 속도가 느려집니다. 5~10초가 안전한 범위입니다.
retries: 실패 재시도 횟수
몇 번 연속 실패해야 unhealthy로 표시할지 정합니다.
일종의 디바운스 장치입니다. 일시적인 네트워크 흔들림이나 데이터베이스 재로딩으로 단일 검사가 실패할 수 있으므로, retries를 설정하면 시스템이 더 안정적입니다.
3~5회가 적당합니다. retries=1은 너무 민감하고, retries=10은 반응이 너무 늦습니다.
start_period: 시작 유예 기간(가장 놓치기 쉬운 항목)
가장 자주 놓치고 문제도 많이 일으키는 매개변수입니다.
start_period 안에서 발생한 실패는 retries에 포함되지 않습니다. 즉, 서비스에 ‘시작 버퍼 시간’을 주는 것입니다.
데이터베이스 시작에는 시간이 걸리기 때문에 중요합니다. PostgreSQL은 데이터 디렉터리를 초기화해야 하고 MySQL은 테이블 인덱스를 불러와야 합니다. start_period가 없으면 2초째부터 실패 횟수를 세기 시작해, 서비스가 준비되기도 전에 retries가 소진되고 unhealthy로 표시될 수 있습니다.
권장값:
- PostgreSQL/MySQL: 30~60초
- 가벼운 서비스(Redis): 15~30초
- 초기화 스크립트가 많은 서비스: 최대 120초
저는 보통 60초로 설정합니다. 조금 더 기다리더라도 오탐을 피하는 편이 낫습니다.
흔한 오류와 예방법
오류 1: 잘못된 환경 변수 참조
# ❌ 오류: Compose가 시작 전에 보간하므로 컨테이너에는 호스트의 변수 값이 전달됨
test: ["CMD", "mysqladmin", "ping", "-p$MYSQL_ROOT_PASSWORD"]
# ✅ 올바름: $$로 이스케이프하여 컨테이너 안의 shell이 해석하게 함
test: ["CMD-SHELL", "mysqladmin ping -p$$MYSQL_ROOT_PASSWORD"]
오류 2: start_period가 너무 짧음
# ❌ 데이터베이스 초기화가 끝나기 전에 실패 횟수를 세어 곧바로 unhealthy로 표시됨
healthcheck:
test: ["CMD", "pg_isready"]
interval: 5s
retries: 3
start_period: 10s # 너무 짧음!
# ✅ 시작 시간을 충분히 확보
healthcheck:
start_period: 60s # 이제 안심할 수 있음
오류 3: 검사 도구가 없음
Docker는 이 오류를 조용히 실패로만 처리하므로 특히 알아채기 어렵습니다.
# ❌ 이미지에 curl이 없으면 healthcheck는 계속 실패함
test: ["CMD", "curl", "-f", "http://localhost/health"]
# ✅ 도구가 있는지 확인하거나 이미지에 기본 제공되는 도구 사용
test: ["CMD", "wget", "--spider", "http://localhost/health"] # Alpine 이미지에는 wget이 기본 제공됨
상태 검사 설정을 마쳤으니 이제 데이터베이스별 설정을 살펴보겠습니다.
PostgreSQL healthcheck 실전 설정
PostgreSQL 공식 이미지에는 유용한 도구인 pg_isready가 기본으로 들어 있습니다.
PostgreSQL의 준비 상태를 확인하기 위한 전용 도구이므로 직접 SQL 쿼리를 작성하는 것보다 안정적입니다.
기본 설정(권장)
version: '3.8'
services:
web:
image: node:20-alpine
depends_on:
db:
condition: service_healthy # 핵심: 상태 검사 통과까지 대기
restart: true # 데이터베이스가 재시작하면 애플리케이션도 함께 재시작
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/myapp
command: npm start
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
pg_isready 명령 자세히 보기
pg_isready -U postgres -d myapp
-U: 실제 존재하는 사용자 이름을 지정합니다.-d: 데이터베이스 이름을 지정합니다(선택 사항이지만 추가하는 편이 좋습니다).
왜 -U가 필요할까요? 지정하지 않으면 pg_isready가 현재 시스템 사용자로 연결을 시도해 로그에 warning이 많이 생깁니다. 기능에는 영향이 없지만 불필요한 로그가 쌓입니다.
고급 설정: 실제 쿼리 추가
pg_isready는 포트에 연결할 수 있는지만 확인하며, 데이터베이스가 실제로 쿼리를 실행할 수 있는지는 보장하지 않습니다. 더 엄격하게 검사하려면 다음과 같이 설정합니다.
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres && psql -U postgres -d myapp -c 'SELECT 1'"]
interval: 10s
timeout: 10s # 추가 쿼리가 있으므로 timeout을 늘려야 함
retries: 3
start_period: 60s
SELECT 1은 가장 단순한 쿼리입니다. 성공하면 데이터베이스가 시작되었을 뿐 아니라 SQL을 정상적으로 처리할 수 있다는 뜻입니다.
하지만 대부분의 상황에서는 기본 pg_isready만으로 충분합니다.
실제 실행 결과
설정을 마쳤다면 시작해 봅니다.
$ docker-compose up
Creating network "myapp_default" ... done
Creating myapp_db_1 ... done
Waiting for myapp_db_1 to be healthy... ← 이 줄을 확인
Creating myapp_web_1 ... done
db_1 | PostgreSQL init process complete; ready for start up.
db_1 | database system is ready to accept connections
web_1 | Server listening on port 3000 ← 데이터베이스가 ready 상태가 된 뒤 애플리케이션 시작
Docker가 db 컨테이너가 healthy 상태가 되기를 기다리므로 눈에 띄는 지연이 생깁니다. 30~60초가 걸릴 수 있지만, 그 대신 실패 없이 시작할 수 있습니다.
문제 해결: 컨테이너가 계속 unhealthy 상태인 경우
데이터베이스 컨테이너가 계속 unhealthy로 표시되면 상태 검사 로그를 확인합니다.
# 컨테이너 상태 확인
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq
{
"Status": "unhealthy",
"FailingStreak": 5,
"Log": [
{
"Start": "2024-12-17T03:15:30Z",
"End": "2024-12-17T03:15:30Z",
"ExitCode": 1,
"Output": "pg_isready: could not connect to server: Connection refused"
}
]
}
흔한 원인은 다음과 같습니다.
- start_period가 너무 짧음: 데이터베이스가 초기화되는 동안 실패 횟수를 세기 시작합니다.
- 사용자 이름 또는 데이터베이스 이름 오류:
pg_isready가 연결하지 못합니다. - PostgreSQL 시작 실패:
docker logs myapp_db_1로 컨테이너 로그를 확인합니다.
MySQL healthcheck 실전 설정
MySQL 상태 검사에는 MySQL에 기본 제공되는 관리 도구인 mysqladmin ping을 사용합니다.
기본 설정(권장)
version: '3.8'
services:
web:
image: node:20-alpine
depends_on:
db:
condition: service_healthy
restart: true
environment:
DATABASE_URL: mysql://root:password@db:3306/myapp
command: npm start
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: myapp
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
mysqladmin ping 명령 자세히 보기
mysqladmin ping -h localhost -u root -ppassword
-h: 호스트 주소(컨테이너 안에서는localhost)-u: 사용자 이름-p: 비밀번호(-p와 비밀번호 사이에 공백이 없어야 함)
MySQL이 정상이면 다음과 같이 반환됩니다.
mysqld is alive
종료 코드는 0이며 상태 검사를 통과합니다.
비밀번호를 올바르게 처리하는 방법
방법 1: 비밀번호 직접 입력(개발 환경에 적합)
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
간단하지만 설정 파일에 비밀번호가 하드코딩됩니다.
방법 2: 환경 변수 사용(권장)
db:
environment:
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
# 주의: $가 아니라 $$를 사용
여기서 주의할 점은 $가 아니라 $$를 사용해야 한다는 것입니다.
Docker Compose는 시작 전에 환경 변수를 해석합니다. $MYSQL_ROOT_PASSWORD를 사용하면 컨테이너가 아니라 호스트에서 이 변수를 찾습니다. $$는 Compose에 “건드리지 말고 컨테이너 안의 shell이 해석하게 하라”고 알려 줍니다.
방법 3: 비밀번호 없이 검사(가장 간단하지만 논란의 여지가 있음)
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
일부 MySQL 설정은 로컬에서 비밀번호 없는 연결을 허용하므로 이 방법이 가장 간단할 수 있습니다. 다만 프로덕션 환경에서는 권장하지 않습니다.
MySQL 8.0에서 특별히 주의할 점
MySQL 8.0은 기본적으로 caching_sha2_password 인증 플러그인을 사용하므로 일부 오래된 클라이언트는 연결하지 못할 수 있습니다. 애플리케이션에서 인증 오류가 발생하면 다음과 같이 기존 인증 방식을 강제로 사용할 수 있습니다.
db:
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: myapp
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
흔한 문제
문제 1: Access denied for user ‘root’@‘localhost’
비밀번호가 틀렸거나 환경 변수가 적용되지 않았습니다. 다음 항목을 확인합니다.
MYSQL_ROOT_PASSWORD철자가 올바른가?healthcheck의 비밀번호와 일치하는가?$$로 이스케이프했는가?
문제 2: 컨테이너 시작이 느리고 계속 starting 상태임
MySQL이 데이터 디렉터리를 초기화하는 데는 시간이 걸리며, 특히 처음 시작할 때 더 오래 걸립니다. start_period를 충분히 길게 설정했는지 확인합니다. 보통 60초면 충분하지만 init 스크립트가 대량의 데이터를 가져와야 한다면 120초 이상 필요할 수 있습니다.
문제 3: 상태 검사를 통과했지만 애플리케이션이 데이터베이스에 연결하지 못함
네트워크 또는 애플리케이션 설정 문제일 수 있습니다. 다음을 확인합니다.
- 애플리케이션 연결 문자열이 올바른가? 호스트 이름은
localhost가 아니라db를 사용합니다. - Docker 네트워크 설정이 정상인가?
docker network inspect로 컨테이너가 같은 네트워크에 있는지 확인합니다.
다른 데이터베이스와 서비스의 상태 검사
PostgreSQL과 MySQL 설정 방법을 알면 다른 서비스에도 같은 원리를 적용할 수 있습니다. 빠르게 참고할 수 있는 예제를 정리했습니다.
Redis
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 15s
redis-cli ping은 PONG과 종료 코드 0을 반환합니다. Redis는 빠르게 시작되므로 start_period를 짧게 설정할 수 있습니다.
MongoDB
mongo:
image: mongo:7
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
MongoDB 6.0부터는 기존 mongo 명령 대신 mongosh를 사용합니다. 이전 버전을 사용한다면 다음과 같이 설정합니다.
test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]
RabbitMQ
rabbitmq:
image: rabbitmq:3-management-alpine
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 3
start_period: 40s
RabbitMQ는 시작이 비교적 느리므로 start_period를 40초 이상으로 설정하는 편이 좋습니다.
일반 HTTP 서비스
서비스에서 /health 또는 /ping 같은 HTTP 상태 검사 엔드포인트를 제공한다면 wget이나 curl을 사용할 수 있습니다.
api:
image: myapp:latest
healthcheck:
test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:8080/health"]
# 또는 curl 사용(이미지에 설치되어 있는 경우)
# test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
--spider는 wget이 파일을 내려받지 않고 확인만 하게 하며, --quiet는 로그 출력을 막습니다.
주의: 이미지에 wget 또는 curl이 있는지 확인해야 합니다. Alpine 이미지에는 기본적으로 wget이 있고, Debian/Ubuntu 이미지에는 curl이 있습니다.
전용 도구가 없는 서비스
서비스에 상태 검사 도구가 없다면 netcat(nc)으로 포트를 확인할 수 있습니다.
service:
image: some-service:latest
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 9000 || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
nc -z는 실제 연결을 맺지 않고 포트가 열려 있는지만 확인합니다. 하지만 포트만 검사할 뿐 서비스가 실제로 ready 상태인지는 알 수 없으므로 앞선 방법보다 단순합니다.
wait-for-it 스크립트가 지금도 필요할까?
Docker 시작 순서를 검색해 보면 wait-for-it 또는 wait-for 스크립트를 권장하는 글이 많이 나옵니다.
이 스크립트는 애플리케이션 컨테이너의 entrypoint에 대기 로직을 추가하고, 의존 서비스의 TCP 포트에 연결할 수 있는지 확인합니다.
기존 wait-for-it 방식
web:
image: node:20-alpine
depends_on:
- db # 일반 depends_on일 뿐 상태는 검사하지 않음
volumes:
- ./wait-for-it.sh:/wait-for-it.sh # 스크립트 마운트
command: ["/wait-for-it.sh", "db:5432", "--", "npm", "start"]
wait-for-it.sh는 db:5432 포트에 연결할 수 있을 때까지 반복해서 확인한 뒤 npm start를 실행합니다.
이제 권장하지 않는 이유
2024년의 모범 사례는 기본 제공되는 healthcheck를 쓸 수 있다면 스크립트를 사용하지 않는 것입니다.
이유는 다음과 같습니다.
- 설정 명확성: 상태 검사 로직이 데이터베이스 서비스에 있어 의존 관계를 한눈에 파악할 수 있습니다.
- 추가 파일 불필요: 스크립트를 유지보수하거나 volume으로 마운트할 필요가 없습니다.
- 더 강력한 기능:
healthcheck는 실제 서비스 준비 상태를 검사할 수 있지만 TCP 포트 검사는 너무 단순합니다. - 재사용성:
healthcheck를 한 번 설정하면 해당 서비스에 의존하는 모든 서비스가 자동으로 혜택을 받습니다.
커뮤니티에서 큰 인기를 얻은 글 중에는 “Forget wait-for-it, use docker-compose healthcheck and depends_on instead”라는 제목도 있습니다.
wait-for-it이 여전히 필요한 경우
두 가지 예외 상황이 있습니다.
상황 1: 이미지 또는 compose 설정을 수정할 수 없음
예를 들어 전용 healthcheck가 없는 서드파티 이미지를 사용하고 설정을 추가할 권한도 없다면, 애플리케이션 쪽에 wait-for-it을 추가하는 것이 유일한 선택입니다.
상황 2: 여러 서비스를 기다려야 함
./wait-for-it.sh db:5432 redis:6379 rabbitmq:5672 -- npm start
depends_on에도 여러 서비스를 설정할 수 있지만 wait-for-it이 더 간단할 때가 있습니다. 다만 이런 상황은 많지 않습니다.
다른 대체 도구
wait-for-it 외에도 비슷한 도구가 있습니다.
- dockerize: Go로 작성되었으며 기능이 더 많고 환경 변수 템플릿을 지원합니다.
- wait-for:
wait-for-it의 간소화 버전이며 순수 shell로 구현되었습니다. - docker-compose-wait: Python으로 작성되었으며 HTTP 검사를 지원합니다.
하지만 2024년 기준으로 healthcheck를 쓸 수 있다면 이런 도구를 추가할 필요는 없습니다.
문제 해결 체크리스트와 모범 사례
설정했는데도 작동하지 않는다면 다음 체크리스트를 따라가 보세요. 문제의 90%는 여기서 해결할 수 있습니다.
빠른 진단 명령
# 1. 모든 컨테이너 상태 확인
$ docker-compose ps
NAME COMMAND SERVICE STATUS PORTS
myapp_db postgres db healthy 5432/tcp
myapp_web npm start web running 0.0.0.0:3000-초과3000/tcp
# 2. 상태 검사 세부 정보 확인
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq
# 3. 컨테이너 로그 확인
$ docker-compose logs db
$ docker-compose logs web
# 4. 로그 실시간 추적
$ docker-compose logs -f --tail=100
흔한 문제 결정 트리
문제: 컨테이너가 계속 starting으로 표시되고 healthy로 바뀌지 않음
start_period가 너무 짧은지 확인 → 60s로 변경해 봅니다.- 상태 검사 명령이 올바른지 확인 →
docker inspect로 실제 실행 명령을 확인합니다. - 컨테이너에 들어가 상태 검사 명령을 직접 실행 →
docker exec -it myapp_db_1 pg_isready -U postgres
문제: 컨테이너가 unhealthy가 되었다가 다시 healthy로 돌아오기를 반복함
interval이 너무 짧고 리소스가 부족함 → 10s 또는 15s로 변경합니다.retries가 너무 적어 일시적인 장애에도 상태가 바뀜 → 5로 변경합니다.- 데이터베이스에 실제 성능 문제가 있음 → 데이터베이스 로그를 확인합니다.
문제: 상태 검사는 통과했지만 애플리케이션이 데이터베이스에 연결하지 못함
- 애플리케이션 연결 문자열이 올바른지 확인 → 호스트 이름은
localhost가 아니라 서비스 이름(예:db)을 사용합니다. - 포트 매핑이 올바른지 확인 → 컨테이너 간 통신은 매핑된 포트가 아니라 내부 포트(5432)를 사용합니다.
- 네트워크 설정이 올바른지 확인 → 모든 서비스가 같은 network에 있는지 확인합니다.
프로덕션 환경 모범 사례
1. 권장 매개변수 값(보수적인 설정)
healthcheck:
interval: 10s # 응답 속도와 리소스 사용량의 균형
timeout: 5s # 명령 실행에 충분한 시간 제공
retries: 5 # 일시적인 장애 허용
start_period: 60s # 데이터베이스 시작 시간 충분히 확보
이 설정은 대부분의 상황에서 안정적으로 작동합니다. 데이터베이스 초기화 스크립트가 많다면 start_period를 120s로 늘릴 수 있습니다.
2. restart 정책 사용
web:
depends_on:
db:
condition: service_healthy
restart: true # 데이터베이스가 재시작하면 애플리케이션도 재시작
restart: unless-stopped # 컨테이너가 종료되면 자동 재시작
restart: true를 설정하면 데이터베이스를 업그레이드하거나 재시작한 뒤 의존 서비스도 재시작해 다시 연결합니다.
3. 리소스 제한
상태 검사도 아주 적게나마 리소스를 사용합니다. 시스템 리소스가 부족하다면 다음과 같이 조정할 수 있습니다.
healthcheck:
interval: 30s # 검사 간격 늘리기
timeout: 3s # 제한 시간 줄이기
다만 수백 개의 컨테이너를 실행하는 경우가 아니라면 healthcheck의 오버헤드는 대체로 무시할 수 있습니다.
4. 상태 모니터링
프로덕션 환경에서는 모니터링 도구로 상태 검사 결과를 추적하는 편이 좋습니다. Docker 상태 검사 이벤트는 Prometheus나 Grafana 같은 도구에서 수집할 수 있습니다.
# docker-compose.yml
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 3
start_period: 60s
labels:
- "prometheus.io/scrape=true" # Prometheus에서 상태를 수집하도록 설정
5. 환경별 설정
개발 환경과 프로덕션 환경에 서로 다른 설정을 사용할 수 있습니다.
# docker-compose.yml(개발)
db:
healthcheck:
start_period: 30s # 개발 환경은 데이터가 적어 빠르게 시작됨
# docker-compose.prod.yml(프로덕션)
db:
healthcheck:
start_period: 120s # 프로덕션 환경은 데이터가 많아 느리게 시작됨
interval: 5s # 더 자주 검사
사용할 때는 설정 파일을 지정합니다.
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up
디버깅 요령
요령 1: 상태 검사 명령 직접 테스트
컨테이너에 들어가 상태 검사 명령을 직접 실행하면 정확히 어디서 오류가 발생하는지 알 수 있습니다.
$ docker exec -it myapp_db_1 sh
/# pg_isready -U postgres -d myapp
/var/run/postgresql:5432 - accepting connections
/# echo $?
0 ← 0이 반환되면 성공
요령 2: 상태 검사 일시적으로 비활성화
디버깅할 때 healthcheck를 주석 처리하고 일반 depends_on을 사용하면 상태 검사 자체의 문제인지 구분할 수 있습니다.
web:
depends_on:
- db # 임시로 단순 모드 사용
# db:
# condition: service_healthy
애플리케이션이 데이터베이스에 정상적으로 연결되는지 확인한 뒤 healthcheck를 다시 추가합니다.
요령 3: Docker 이벤트 로그 확인
Docker는 상태 변화 등 모든 컨테이너 이벤트를 기록합니다.
$ docker events --filter 'event=health_status'
2024-12-17T03:15:30.123456789Z container health_status: healthy (name=myapp_db_1)
2024-12-17T03:16:45.987654321Z container health_status: unhealthy (name=myapp_db_1)
컨테이너가 언제 unhealthy 상태가 되었는지 확인하고 로그 타임스탬프와 대조해 문제를 찾을 수 있습니다.
결론
처음 질문으로 돌아가 보겠습니다. depends_on을 설정했는데도 왜 작동하지 않을까요?
답은 간단합니다. 그것만으로는 부족하기 때문입니다.
depends_on은 기본적으로 컨테이너 시작만 제어하며 서비스 준비 여부는 확인하지 않습니다. 데이터베이스 컨테이너가 running 상태라고 해서 연결을 받을 수 있는 것은 아닙니다. 그 사이의 간격이 문제의 원인입니다.
해결 방법도 명확합니다.
- 데이터베이스에 healthcheck 설정:
pg_isready또는mysqladmin ping으로 실제 준비 상태를 확인합니다. - service_healthy condition 사용: 애플리케이션이 상태 검사 통과를 기다리게 합니다.
- 적절한 start_period 설정: 60초부터 시작해 데이터베이스 초기화 시간을 충분히 확보합니다.
이 세 가지를 적용하면 컨테이너 시작 실패 문제는 거의 사라집니다.
마지막으로 빠르게 적용할 수 있는 방법을 정리합니다.
- 지금 바로: 글의 PostgreSQL 또는 MySQL 설정을 프로젝트에 복사하고 환경 변수만 수정해 사용합니다.
- 오늘 저녁: 팀 프로젝트의
depends_on을 모두service_healthy로 업그레이드해 문제를 한 번에 해결합니다. - 다음 주 팀 회의: 동료들과 공유해 팀 설정 표준을 통일합니다.
이 글에서 다루지 않은 문제가 있다면 댓글로 남겨 주세요. Docker Compose에는 아직도 여러 함정이 있으니 함께 해결해 봅시다.
이 글이 오래된 시작 문제를 해결하는 데 도움이 되었다면 좋아요로 알려 주세요. 마침내 해결했을 때의 그 순간이 제가 이 글을 쓴 원동력입니다.
Docker Compose healthcheck 설정 전체 과정
데이터베이스가 준비되기 전에 애플리케이션이 시작되어 실패하는 문제를 해결하며, PostgreSQL과 MySQL의 전체 설정 템플릿을 제공합니다.
⏱️ Estimated time: 30 min
- 1
Step 1: 문제의 원인 이해하기: depends_on의 한계
문제의 원인:
• Docker의 depends_on은 컨테이너 시작 순서만 제어할 뿐, 서비스가 실제로 준비되었는지는 확인하지 않습니다.
• Compose는 컨테이너가 실행될 때까지만 기다리며 서비스가 실제로 사용 가능한지는 확인하지 않습니다.
• Docker 공식 문서: Compose does not wait until a container is "ready", only until it's running
• 뜻: Compose는 컨테이너가 실행될 때까지만 기다리며 서비스가 실제로 사용 가능한지는 확인하지 않습니다.
컨테이너 시작과 서비스 준비 사이의 시간 차이:
• 0초: Docker가 컨테이너를 시작하고 postgres 프로세스가 실행됩니다(depends_on은 여기서 다음 단계로 진행).
• 2초: 데이터 디렉터리를 초기화합니다.
• 5초: 설정 파일을 불러옵니다.
• 8초: init 스크립트가 있으면 실행합니다.
• 12초: 마침내 ready 상태가 되어 연결을 받을 수 있습니다.
이 사이에 12초의 간격이 있습니다. Web 애플리케이션이 1초째에 데이터베이스 연결을 시도하면 반드시 Connection refused가 발생합니다. - 2
Step 2: depends_on의 세 가지 condition 설정
depends_on의 세 가지 condition 설정:
1. service_started(기본값)
• 컨테이너가 시작될 때까지만 기다리고 서비스 준비 여부는 확인하지 않습니다.
• 권장하지 않습니다.
2. service_healthy(권장)
• 컨테이너가 시작되고 상태 검사를 통과할 때까지 기다립니다.
• 서비스가 실제로 준비되었는지 확인합니다.
3. service_completed_successfully
• 컨테이너 실행이 끝나고 성공적으로 종료될 때까지 기다립니다.
• 일회성 작업에 적합합니다.
service_healthy 사용을 권장합니다:
• docker-compose.yml에 depends_on: db: condition: service_healthy를 설정합니다.
• 데이터베이스가 실제로 준비된 뒤 애플리케이션이 시작되도록 보장합니다. - 3
Step 3: PostgreSQL과 MySQL의 올바른 healthcheck 작성법
PostgreSQL 상태 검사:
• pg_isready 명령으로 데이터베이스 준비 여부를 확인합니다.
• 설정 예제:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s(데이터베이스가 초기화될 시간을 충분히 확보)
MySQL 상태 검사:
• mysqladmin ping 명령으로 데이터베이스 준비 여부를 확인합니다.
• 설정 예제:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s
전체 설정 예제:
• docker-compose.yml에 healthcheck를 설정합니다(상태 검사 명령, 간격, 제한 시간, 재시도 횟수 정의).
• depends_on에 service_healthy 조건을 사용합니다.
• 데이터베이스가 실제로 준비된 뒤 애플리케이션이 시작되도록 보장합니다. - 4
Step 4: 문제 해결과 모범 사례
문제 해결 체크리스트:
1. 상태 검사 명령이 올바른지 확인합니다.
• 컨테이너 안에서 상태 검사 명령을 직접 실행해 반환값이 0인지 확인합니다.
• 예: docker exec -it db_container pg_isready -U postgres
2. 상태 검사를 일시적으로 비활성화합니다.
• 디버깅할 때 healthcheck를 먼저 주석 처리할 수 있습니다.
• 일반 depends_on을 사용해 상태 검사 자체의 문제인지 구분합니다.
3. Docker 이벤트 로그를 확인합니다.
• docker events로 컨테이너 시작과 상태 검사 이벤트를 확인합니다.
모범 사례:
1. 데이터베이스에 healthcheck를 설정하고 pg_isready 또는 mysqladmin ping으로 실제 준비 상태를 확인합니다.
2. service_healthy condition을 사용해 애플리케이션이 상태 검사 통과를 기다리게 합니다.
3. 적절한 start_period(60초부터 시작)를 설정해 데이터베이스 초기화 시간을 충분히 확보합니다.
이 세 가지를 적용하면 컨테이너 시작 실패 문제는 거의 사라집니다.
wait-for-it 스크립트를 대체하는 현대적인 방법:
• depends_on + healthcheck 조합을 사용합니다.
• 별도 스크립트가 필요 없고 설정이 간단하며 신뢰성이 높습니다.
• 모든 Docker Compose 프로젝트에 적합합니다.
FAQ
depends_on만으로는 왜 부족하며, 컨테이너 시작과 서비스 준비는 어떻게 다른가요?
Docker 공식 문서에는 매우 중요하지만 놓치기 쉬운 문장이 있습니다. Compose does not wait until a container is "ready", only until it's running. 즉, Compose는 컨테이너가 실행될 때까지만 기다리며 서비스가 실제로 사용 가능한지는 확인하지 않습니다.
컨테이너 시작과 서비스 준비 사이의 시간 차이를 PostgreSQL 컨테이너 시작 과정으로 살펴보겠습니다.
• 0초: Docker가 컨테이너를 시작하고 postgres 프로세스가 실행됩니다(depends_on은 여기서 다음 단계로 진행).
• 2초: 데이터 디렉터리를 초기화합니다.
• 5초: 설정 파일을 불러옵니다.
• 8초: init 스크립트가 있으면 실행합니다.
• 12초: 마침내 ready 상태가 되어 연결을 받을 수 있습니다.
이 사이에 12초의 간격이 있습니다. Web 애플리케이션이 1초째에 데이터베이스 연결을 시도하면 반드시 Connection refused가 발생합니다.
depends_on에는 어떤 세 가지 condition 설정이 있나요?
1) service_started(기본값):
• 컨테이너가 시작될 때까지만 기다리고 서비스 준비 여부는 확인하지 않습니다.
• 권장하지 않습니다.
2) service_healthy(권장):
• 컨테이너가 시작되고 상태 검사를 통과할 때까지 기다립니다.
• 서비스가 실제로 준비되었는지 확인합니다.
• docker-compose.yml에 depends_on: db: condition: service_healthy를 설정합니다.
3) service_completed_successfully:
• 컨테이너 실행이 끝나고 성공적으로 종료될 때까지 기다립니다.
• 일회성 작업에 적합합니다.
service_healthy 사용을 권장합니다. 데이터베이스가 실제로 준비된 뒤 애플리케이션이 시작되도록 보장합니다.
PostgreSQL과 MySQL 상태 검사는 어떻게 설정하나요?
• pg_isready 명령으로 데이터베이스 준비 여부를 확인합니다.
• 설정 예제:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s(데이터베이스가 초기화될 시간을 충분히 확보)
MySQL 상태 검사:
• mysqladmin ping 명령으로 데이터베이스 준비 여부를 확인합니다.
• 설정 예제:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s
전체 설정 예제: docker-compose.yml에 healthcheck를 설정해 상태 검사 명령, 간격, 제한 시간, 재시도 횟수를 정의하고 depends_on에는 service_healthy 조건을 사용합니다. 그러면 데이터베이스가 실제로 준비된 뒤 애플리케이션이 시작됩니다.
상태 검사 문제는 어떻게 해결하나요?
1) 상태 검사 명령이 올바른지 확인합니다.
• 컨테이너 안에서 상태 검사 명령을 직접 실행해 반환값이 0인지 확인합니다.
• 예: docker exec -it db_container pg_isready -U postgres
• 반환값이 0이면 성공입니다.
2) 상태 검사를 일시적으로 비활성화합니다.
• 디버깅할 때 healthcheck를 먼저 주석 처리할 수 있습니다.
• 일반 depends_on을 사용해 상태 검사 자체의 문제인지 구분합니다.
• 애플리케이션이 데이터베이스에 정상적으로 연결되는지 확인한 뒤 healthcheck를 다시 추가합니다.
3) Docker 이벤트 로그를 확인합니다.
• docker events로 컨테이너 시작과 상태 검사 이벤트를 확인합니다.
상태 검사 설정의 모범 사례는 무엇인가요?
1) 데이터베이스에 healthcheck를 설정하고 pg_isready 또는 mysqladmin ping으로 실제 준비 상태를 확인합니다.
2) service_healthy condition을 사용해 애플리케이션이 상태 검사 통과를 기다리게 합니다.
3) 적절한 start_period(60초부터 시작)를 설정해 데이터베이스 초기화 시간을 충분히 확보합니다.
이 세 가지를 적용하면 컨테이너 시작 실패 문제는 거의 사라집니다.
wait-for-it 스크립트를 대체하는 현대적인 방법:
• depends_on + healthcheck 조합을 사용합니다.
• 별도 스크립트가 필요 없고 설정이 간단하며 신뢰성이 높습니다.
• 모든 Docker Compose 프로젝트에 적합합니다.
빠른 실행 제안:
• 지금 바로: 글의 PostgreSQL 또는 MySQL 설정을 프로젝트에 복사하고 환경 변수만 수정해 사용합니다.
• 오늘 저녁: 팀 프로젝트의 depends_on을 모두 service_healthy로 업그레이드해 문제를 한 번에 해결합니다.
• 다음 주 팀 회의: 동료들과 공유해 팀 설정 표준을 통일합니다.
5분 읽기 · 게시일: 2025년 12월 17일 · 수정일: 2026년 9월 4일
Docker 실전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Docker Compose 멀티 서비스 오케스트레이션: 로컬 개발 환경을 한 번에 시작하기
Docker Compose로 여러 서비스를 구성해 Web, API, MySQL, Redis 로컬 개발 환경을 한 번에 시작하는 방법을 설명합니다. 번거로운 수동 설치, 버전 충돌, 포트 점유 문제를 해결해 새 팀원이 저장소를 clone한 뒤 5분 안에 개발을 시작하고 프로젝트도 빠르게 전환할 수 있습니다.
33편 중 8편
다음
Docker Compose 오류 해결 가이드: 자주 발생하는 5가지 문제와 해결 방법
Docker Compose 실행 오류를 해결하는 체계적인 점검 절차를 정리했습니다. 포트 충돌, 네트워크 문제, 빌드 실패, 컨테이너 종료, 권한 오류의 원인과 해결 방법을 따라 5분 안에 문제를 좁혀 보세요.
33편 중 10편



댓글
GitHub로 로그인하여 댓글을 남기세요