본문 바로가기
빌드노트 검색

빌드노트

Docker 컨테이너가 unhealthy인데 restart 정책이 작동하지 않는 이유

Docker의 unhealthy는 메인 프로세스 종료와 다르다. 같은 on-failure:2 정책에서 헬스체크만 실패한 컨테이너와 코드 7로 종료한 컨테이너를 비교하고, inspect의 PID·StartedAt·RestartCount로 실제 재시작 조건을 판단한다.

Docker 컨테이너에 restart 정책을 설정했는데도 unhealthy인 채 계속 실행될 수 있다. 헬스체크 실패는 검사 명령의 실패이고, 컨테이너 메인 프로세스의 종료와는 다르기 때문이다. 단독 Docker Engine의 재시작 정책은 컨테이너가 종료됐을 때 다시 시작할 조건을 정한다. 헬스 상태가 나빠졌다는 사실만으로 메인 프로세스를 종료하지는 않는다. Docker 재시작 정책 (새 탭에서 열림)

따라서 on-failure를 always로 바꾸기 전에, 실패한 것이 헬스체크인지 컨테이너 자체인지부터 확인한다. 이 글은 실행 중인 단일 컨테이너의 그 차이를 다룬다. Compose에서 다른 서비스가 준비될 때까지 기다리는 시작 순서 문제와는 판단 대상이 다르다.

실행 상태와 헬스 상태를 나란히 읽는다

Docker 엔진에 연결할 수 있는 PowerShell에서 아래 조회를 실행한다. $container에는 문제가 난 컨테이너의 실제 이름이나 ID를 넣는다. 이 명령으로 메인 프로세스가 계속 실행 중인지, 어떤 재시작 정책이 적용됐는지, 헬스체크가 무엇을 반환했는지 확인한다.

$container = '확인할-컨테이너-이름'
docker inspect --type container --format 'id={{.Id}} status={{.State.Status}} pid={{.State.Pid}} started={{.State.StartedAt}} restarts={{.RestartCount}}' $container
docker inspect --type container --format '{{json .HostConfig.RestartPolicy}}' $container
docker inspect --type container --format '{{json .State.Health}}' $container

--format은 필요한 필드를 고르고, json은 정책과 헬스 기록을 JSON으로 보여 준다. 첫 조회가 실패하면 이름·ID와 Docker 연결 대상을 먼저 확인한다. 오류 메시지는 상태 관찰값이 아니다. docker inspect의 형식 지정 (새 탭에서 열림)

다음 필드를 구별해서 읽는다.

  • status=running은 컨테이너 메인 프로세스가 실행 중이라는 뜻이다. State.Health.Status=unhealthy와 동시에 성립할 수 있다.
  • State.Health.Log의 ExitCode는 헬스체크 명령의 종료 코드다. 여기에 1이 찍혔다고 메인 프로세스도 코드 1로 종료한 것은 아니다. 컨테이너가 종료된 뒤의 코드는 별도 필드인 State.ExitCode에서 읽는다. Start, End, Output으로 어떤 검사가 실패했는지 확인한다. HEALTHCHECK의 결과와 진단 출력 (새 탭에서 열림)
  • pid, started, restarts는 시간을 두고 다시 조회해 비교한다. PID는 Docker 엔진이 보는 메인 프로세스의 값이므로 컨테이너 내부의 PID 1과 같은 숫자를 기대하지 않는다. 이름이 같아도 컨테이너를 재생성했을 수 있으니 id도 함께 비교한다.

헬스 기록이 없는 컨테이너라면 현재 설정에 healthcheck가 있는지부터 확인해야 한다. 한 번의 조회로 과거 재시작 전체를 복원할 수는 없다. 여기서는 같은 컨테이너 ID의 전후 값을 비교한다.

헬스체크만 실패시켜도 프로세스는 그대로였다

차이를 확인하려고 BusyBox 1.37.0 기반의 별도 컨테이너 두 개를 만들었다. 둘 다 --restart on-failure:2를 적용했다. 첫 컨테이너의 메인 프로세스는 sleep 120이고, 헬스체크는 컨테이너 안에서 test -f /tmp/probe-ok를 실행한다. 실제 HTTP 서비스의 정상 여부를 검사하는 예제가 아니라, 검사 결과와 프로세스 종료를 분리하는 합성 실험이다.

처음에는 검사할 파일을 만들어 healthy를 확인했다. 컨테이너가 12초 이상 실행된 뒤 파일만 지웠다. 검사 주기는 2초, 연속 실패 기준은 2회, 검사 한 번의 제한 시간은 1초였다. 이후에는 파일을 다시 만들어 검사 결과가 회복되는지도 확인했다.

2026년 10월 21일 실행에서 관찰한 순서는 다음과 같다. 시각은 UTC다.

  • 11:50:29: healthy. PID는 68789, RestartCount는 0이었다.
  • 11:50:32: 검사 명령이 연속 두 번 코드 1을 반환해 unhealthy가 됐다. 컨테이너는 여전히 running이었다.
  • 11:50:46: 계속 실패해 FailingStreak=8이었지만 PID와 시작 시각은 그대로였고 재시작 횟수도 0이었다.
  • 11:50:47: 파일을 복원한 뒤 healthy로 돌아왔다. 이때도 같은 메인 프로세스였다.

위 조회를 실제 PowerShell에서 정상·실패 상태에 각각 실행했다. 두 상태의 첫 조회에서 ID 뒤 필드를 발췌한 값은 아래와 같았다. 정책 조회도 두 번 모두 같은 결과였다.

status=running pid=68789 started=2026-10-21T11:50:16.373692095Z restarts=0
{"Name":"on-failure","MaximumRetryCount":2}

달라진 것은 세 번째 조회의 헬스 기록이었다. 실패 상태에서는 Status가 unhealthy, FailingStreak이 2였으며 최근 두 검사의 ExitCode는 각각 1이었다. 검사 실패를 기록하고도 컨테이너의 실행 상태와 재시작 횟수는 바뀌지 않았다. 이후 검사만 다시 성공해도 메인 프로세스 교체 없이 헬스 상태가 회복됐다.

Docker 문서에서도 헬스 상태는 일반 실행 상태에 추가되는 값으로 설명한다. 검사에 성공하면 healthy, 정해진 횟수만큼 연속 실패하면 unhealthy가 된다. 이 상태 전환을 컨테이너 종료로 해석하면 재시작 조건을 잘못 판단하게 된다. HEALTHCHECK의 상태 전환 (새 탭에서 열림)

메인 프로세스가 코드 7로 종료하자 두 번 재시작했다

두 번째 컨테이너에는 헬스체크를 넣지 않았다. 대신 메인 프로세스가 sleep 12; echo intentional-exit-7; exit 7을 실행하게 했다. 각 실행에서 12초를 기다린 것은 Docker 문서가 재시작 정책의 시작 조건으로 설명하는 10초보다 길게 실행시키기 위해서다. 이번 비교에서는 시작 직후 실패하는 경우를 다루지 않았다. 재시작 정책의 적용 조건 (새 탭에서 열림)

같은 컨테이너 ID에서 RestartCount가 0 → 1 → 2로 증가했다. 실행 중 PID도 68834 → 69080 → 69310으로 바뀌었고 StartedAt도 갱신됐다. 마지막에는 status=exited, PID 0, 종료 코드 7, RestartCount=2로 남았다. 로그의 intentional-exit-7도 세 줄이었다. 최초 실행 한 번과 재시작 두 번이므로 on-failure:2를 총 두 번 실행하는 설정으로 읽으면 안 된다.

이 비교에서는 헬스체크가 없어도 메인 프로세스의 비정상 종료가 재시작으로 이어졌다. 반대로 첫 컨테이너는 헬스체크가 실패해도 메인 프로세스가 살아 있어 재시작하지 않았다. on-failure의 실패 조건은 컨테이너의 0이 아닌 종료 코드다. always 역시 컨테이너가 멈췄을 때의 정책이므로, 이를 선택하는 것만으로 unhealthy를 종료 이벤트로 바꿀 수는 없다. 정책별 재시작 조건 (새 탭에서 열림)

재시작을 기다릴지 판단하려면

같은 ID에서 running + unhealthy이고 PID·시작 시각·재시작 횟수가 유지된다면, 먼저 헬스체크의 실패 출력을 읽는다. 검사 도구가 없는지, 검사 주소가 틀렸는지, 앱이 실제로 응답하지 못하는지 좁혀야 한다. 이 상태에서는 restart 값만 바꾸는 것으로 복구 동작이 생기지 않는다.

프로세스가 회복할 수 없는 오류를 만났을 때 종료하도록 설계하거나, 헬스 상태를 읽고 복구를 수행하는 별도 관리 기능을 구성하는 것은 다른 조치다. 헬스체크 명령 끝에 exit 1을 추가하는 것만으로는 메인 프로세스가 종료하지 않는다. 그것도 검사 명령의 종료 코드다. 자동 재시작이 필요한 상황이라면 누가 어떤 조건에서 메인 프로세스를 종료하거나 교체하는지까지 정해야 한다.

이미 메인 프로세스가 비정상 종료했는데도 재시작하지 않는 경우에는 적용된 정책과 재시도 상한을 확인한다. 이번 비교의 마지막 상태처럼 상한을 모두 썼을 수 있다. 수동 docker stop은 정책을 무시하게 하는 별도 조건이므로 자동 복구 시험의 비정상 종료와 섞지 않는다. 이 수동 중지 규칙은 공식 문서 설명이며 이번 실험의 비교 조건은 아니다. 수동 중지와 재시작 정책 (새 탭에서 열림)

실행 환경은 Windows의 PowerShell 7.6.6, Docker Desktop 4.92.0, Linux Engine 29.8.0이었다. 직접 비교한 정책은 on-failure:2 하나이며 always와 수동 중지는 문서로 확인했다. 실제 서비스의 HTTP 장애, Docker 데몬 재시작, Swarm·Kubernetes의 복구 동작은 검증하지 않았다.

← 목록으로 돌아가기