Docker unhealthy인데 자동 재시작 안 될 때: healthcheck와 restart 정책 구분
docker compose ps에는 컨테이너가 실행 중이라고 나오는데 뒤에 unhealthy가 붙는다. restart: unless-stopped도 설정했다. 그런데 재시작은 일어나지 않는다. 설정이 무시된 것일까?
일반 Docker Engine에서 Compose로 실행한 컨테이너라면 먼저 건강 상태와 프로세스 종료를 구분해야 한다. healthcheck는 검사 결과를 기록하고, 컨테이너 재시작 정책은 종료 등에 대한 동작을 정한다. 주 프로세스가 살아 있는 상태에서 검사만 실패했다고 restart 정책이 자동 복구를 수행하는 것은 아니다. 이는 Docker의 두 기능 설명을 함께 적용한 결론이다. Docker HEALTHCHECK, 재시작 정책
이 글은 2026-09-22 공식 문서를 기준으로 한 진단 가이드다. Swarm 서비스·Kubernetes·별도 자동 복구 도구의 동작은 범위 밖이다. 작성 환경에는 Docker CLI가 없어 실제 컨테이너에서 명령을 실행하지 않았다. 아래 명령과 설명은 운영 장애를 해결한 실측 기록이 아니다.
1. 네 가지 설정을 같은 기능으로 보지 않는다
| 설정·명령 | 담당하는 일 |
|---|---|
healthcheck |
컨테이너 안에서 검사 명령을 실행해 건강 상태를 기록 |
서비스의 restart: unless-stopped |
컨테이너 종료 등에 대한 런타임 재시작 정책 |
depends_on의 condition: service_healthy |
의존 서비스의 건강 상태를 확인한 뒤 종속 서비스를 시작 |
depends_on의 restart: true |
명시적인 Compose 의존 서비스 갱신·재시작에 따른 종속 서비스 재시작 |
마지막 두 항목은 실행 중 장애를 계속 감시하는 감시자가 아니다. 특히 depends_on.restart는 런타임이 컨테이너 종료 후 자동으로 다시 시작하는 경우를 포함하지 않는다. 이름이 같은 restart라도 YAML 위치를 함께 읽어야 한다. Compose services, 시작 순서
다음은 위치를 비교하기 위한 설정 조각이며 배포 가능한 전체 파일은 아니다. 기존 이미지·DB·healthcheck 설정을 이 코드로 대체하지 않는다.
services:
app:
restart: unless-stopped
depends_on:
db:
condition: service_healthy
restart: true
DB가 나중에 응답하지 않는 상황에는 앱의 재연결 처리와 장애 알림이 별도로 필요하다. 시작 시 한 번 준비 상태를 확인했다는 사실로 이후의 연결 성공을 보장할 수 없다.
2. 재시작 전에 정확한 컨테이너부터 확인한다
먼저 기존 배포와 같은 Docker context, Compose 파일, 프로젝트를 선택한다. -f, -p, --env-file 및 여러 override 파일을 쓰고 있었다면 같은 인자를 유지한다. 아래 조회도 해당 프로젝트를 가리키는 디렉터리·인자를 확인한 뒤 실행한다.
docker context show
docker compose version
docker compose ps -a
-a를 붙여 종료된 컨테이너도 함께 확인한다. 서비스에 인스턴스가 여러 개면 모두 같은 상태라고 가정하지 말고 장애가 난 컨테이너 ID를 고른다. Compose ps
다음 값은 반드시 방금 확인한 실제 ID로 바꾼다. YOUR_CONTAINER_ID는 예시 자리표시자다.
CID='YOUR_CONTAINER_ID'
docker inspect --type container --format \
'status={{.State.Status}} running={{.State.Running}} restarting={{.State.Restarting}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' \
"$CID"
docker inspect --type container --format \
'policy={{.HostConfig.RestartPolicy.Name}} restartCount={{.RestartCount}} started={{.State.StartedAt}}' \
"$CID"
전체 inspect JSON 대신 필요한 필드만 출력한다. health=none은 건강함을 증명하는 값이 아니라 이 조회에서 Health 정보가 없다는 뜻이다. 재시작 횟수 한 번의 값만 보고 변화가 있었다고 판단하지 말고 시간과 컨테이너 ID를 함께 기록한다. docker inspect
3. 검사 실패와 앱 장애를 분리한다
건강 상태 검사의 기본 흐름은 starting에서 검사 성공 시 healthy, 연속 실패가 기준에 이르면 unhealthy다. 검사 명령은 컨테이너 안에서 실행되므로, 호스트에서 성공한 명령이 그대로 성공한다는 보장은 없다. Docker HEALTHCHECK
실패 출력이 필요하면 권한 있는 터미널에서 다음을 확인한다. 출력에는 URL·응답 본문·토큰 등이 포함될 수 있다. 외부 공유 전에 가린다.
docker inspect --type container --format \
'{{if .State.Health}}{{range .State.Health.Log}}{{.End}} exit={{.ExitCode}} {{printf "%q" .Output}}{{println}}{{end}}{{else}}no healthcheck{{end}}' \
"$CID"
최근 검사 기록은 영구 감사 로그가 아니다. 출력이 없다고 예전 실패가 없었다고 단정하지 않는다. 다음 질문으로 원인을 좁힌다.
- 검사에 사용한 실행 파일이 이미지 안에 있는가? 도구가 없는 오류와 서비스가 응답하지 않는 오류를 구분한다.
- 검사 주소·포트·경로가 이 컨테이너 기준으로 맞는가? 호스트에 게시한 포트와 컨테이너 내부 포트를 혼동하지 않았는가?
- 인증이 필요한 경로를 무인 검사하고 있지는 않은가? 민감한 인증값을 명령 문자열에 직접 붙이지 않았는가?
- DB 장애 때문에 검사에 실패했는가, 앱 자신의 기능에 문제가 있는가?
DB 서비스명 해석 오류라면 Ghost ENOTFOUND db 진단으로 분기한다. 내부 검사는 성공하지만 외부에서 502가 발생한다면 NPM 502 계층별 진단을 참고한다. 한 검사 결과로 프록시·TLS·사용자 기능까지 모두 정상이라고 결론 내리지 않는다.
앱 로그도 범위를 제한해 읽는다. app은 실제 서비스명으로 바꾼다.
docker compose logs --since 10m --tail 100 app
이 명령은 원인을 고치지 않고 로그만 조회한다. 앱 로그에도 개인정보·비밀값이 있을 수 있으며, 설정된 로깅 방식에 따라 필요한 로그가 여기에 없을 수도 있다. Compose logs
4. 유예 시간은 장애를 숨기는 설정이 아니다
초기화가 오래 걸리는 앱은 시작 직후의 검사 실패와 평상시 장애를 구분해야 한다. start_period 안의 실패는 재시도 실패 횟수에 포함되지 않지만, 그 안에 한 번 성공하면 이후 연속 실패가 집계된다. timeout은 검사 한 번의 제한이고 retries는 연속 실패 기준이다. Docker HEALTHCHECK
따라서 고정된 “몇 초면 반드시 unhealthy”라는 계산보다 실제 시작 시간과 검사 기록을 본다. 임계값을 무작정 늘리거나 검사를 끄면 화면은 조용해져도 장애 원인은 남는다. 조정이 필요하면 정상 기동 시간, 외부 의존성 지연, 허용 가능한 탐지 지연을 근거로 결정한다.
5. 수정한 검사 설정은 재시작만으로 반영하지 않는다
Compose의 healthcheck를 수정했다면 파일 변경과 실행 컨테이너의 설정 반영은 별개다. docker compose restart는 Compose 설정 변경을 적용하는 명령이 아니다. 관련 차이는 환경변수 변경과 재생성 글에서 다뤘다. Docker restart
적용 전에 대상 서비스의 중단 영향, 데이터 저장 위치, 백업과 되돌릴 설정을 확인한다. 반영이 승인된 경우 기존 배포 인자를 유지한 docker compose up -d app처럼 범위를 제한할 수 있다. up은 변경된 구성에 따라 컨테이너를 재생성할 수 있으므로 단순 조회나 무중단 명령이 아니다. Docker up
수정 후에는 새 컨테이너 ID를 다시 확인해 2~3절을 반복하고, 실제 사용자 요청도 점검한다. 이전 컨테이너 ID를 계속 조회하면 새 상태를 보지 못한다. up --wait 역시 실행·건강 상태를 기다리는 옵션이지, 이후 장애를 영구 감시하는 복구 서비스는 아니다.
6. 자동 복구가 필요하면 조건과 권한부터 설계한다
자동 복구 도구를 추가하는 선택은 가능하지만, 모든 unhealthy 컨테이너를 무조건 재시작하는 정책은 이 글에서 권하지 않는다. 다음은 운영 설계 제안이지 Docker의 기본 보장 사항이 아니다.
- 재시작 대상 서비스를 명시적으로 제한한다.
- 한 번의 실패가 아니라 지속 실패와 실제 기능 장애를 함께 확인한다.
- 재시도 상한·대기 시간·알림·사람의 개입 조건을 둔다.
- DB 장애처럼 재시작으로 해결되지 않는 원인은 별도 경로로 처리한다.
- 유지보수 중 의도적으로 중지한 서비스를 다시 살리지 않도록 한다.
특히 복구 도구에 Docker 소켓 접근을 주는 것은 단순 모니터링 권한 부여로 취급할 일이 아니다. 제어 권한과 신뢰 경계를 먼저 검토한다. Docker 소켓 보호, Portainer와 Docker 소켓 보안
증거를 남기기 전에 전체 스택을 내리거나 볼륨을 지우지 않는다. 검사를 항상 성공시키는 임시 우회도 장애 해결로 기록하지 않는다. 원인을 고친 뒤 건강 상태와 사용자 기능을 따로 확인하는 편이 다음 장애 때도 재사용할 수 있는 절차다.
결론
unhealthy는 상태 신호이고, restart는 복구 동작을 결정하는 별도 정책이다. 신호가 있다고 동작까지 자동으로 연결된 것은 아니다. 먼저 대상 컨테이너 → 검사 실패 이유 → 실제 기능 영향 → 최소 범위 수정 순서로 확인하자. 그 다음에야 자동 복구가 필요한지, 어떤 조건과 권한으로 실행할지 결정할 수 있다.