빌드노트
start-period가 남았는데 HEALTHCHECK 실패가 쌓이는 이유
Docker HEALTHCHECK는 시작 유예 중 한 번 성공하면 이후 연속 실패를 집계한다. 90초 유예를 둔 두 컨테이너를 비교하고, inspect의 첫 성공 기록과 FailingStreak로 실패가 세어지는 이유를 확인한다.
Docker의 HEALTHCHECK --start-period=90s를 설정했는데 90초도 지나기 전에 FailingStreak이 늘고 unhealthy가 될 수 있다. 시작 유예 중 검사가 한 번이라도 성공하면 Docker는 시작이 완료됐다고 보고, 그 뒤의 연속 실패를 집계한다. 90초 내내 모든 실패를 무시하는 고정 대기 구간으로 읽으면 이 동작을 설명할 수 없다. Docker의 HEALTHCHECK 규칙 (새 탭에서 열림)
남은 시간보다 성공 기록을 먼저 본다
먼저 실행 중인 컨테이너의 설정과 헬스 기록을 함께 확인한다. Docker에 연결된 PowerShell에서 $container를 실제 이름이나 ID로 바꾸고 다음 두 조회를 실행한다. 첫 줄은 시작 시각과 적용된 검사 설정을, 둘째 줄은 현재 상태와 검사 결과를 보여 준다.
$container = 'my-container'
docker inspect --type container --format '{{.State.StartedAt}} {{json .Config.Healthcheck}}' $container
docker inspect --type container --format '{{json .State.Health}}' $container
컨테이너를 찾지 못하거나 Docker 연결 오류가 나면 조회 대상을 먼저 바로잡는다. 헬스 설정 자체가 없다면 이 시작 유예 규칙을 적용할 대상이 아니다. --format의 json은 선택한 객체를 JSON으로 출력한다. inspect의 부분 객체 조회 (새 탭에서 열림)
설정에서는 Test, StartPeriod, Retries를 읽는다. Dockerfile을 고친 기억보다 현재 컨테이너에 실제 적용된 값을 기준으로 판단한다. 이번 실험에서 StartPeriod=90000000000은 설정한 90초에 해당했고 Retries=3이었다.
State.Health.Log에서는 Start·End·ExitCode·Output을 시간순으로 읽는다. 종료 코드 0은 성공, 1은 실패다. 시작 유예 안에 코드 0이 있었고 그 뒤에 실패가 이어졌다면, 아직 90초가 안 됐어도 실패 집계가 시작될 수 있다. FailingStreak은 연속 실패를 보는 값이므로 로그에 남은 실패 행의 총개수와 같다고 가정하지 않는다. 검사 종료 코드와 진단 출력 (새 탭에서 열림)
같은 90초 유예에서 두 결과가 갈렸다
이 차이를 확인하려고 BusyBox 1.37.0 기반의 새 컨테이너 두 개를 실행했다. 둘 다 네트워크를 none으로 두고, 외부 서비스 대신 컨테이너 안의 파일 카운터만 검사에 사용했다. 메인 프로세스는 sleep 150으로 유지했다. 운영 앱의 장애 기록이 아니라 실패 집계 조건을 비교한 합성 실험이다.
두 컨테이너 모두 start-period=90s, interval=2s, start-interval=2s, timeout=1s, retries=3이었다. 첫 컨테이너는 모든 검사에서 코드 1을 반환했다. 두 번째는 첫 검사에서 실패하고, 두 번째 검사에서만 코드 0을 반환한 뒤 다시 계속 실패했다. start-interval을 쓰려면 Docker Engine 25.0 이상이 필요하다. 시작 구간의 검사 간격 (새 탭에서 열림)
2026년 10월 22일 실행에서 첫 컨테이너는 시작 후 약 11.6초째 조회까지 다섯 번 모두 실패했다. 그런데 Status=starting, FailingStreak=0이었다. 실패가 로그에 남는 것과 실패 횟수에 더해지는 것은 다르다. 이 경우에는 유예 중 실패를 기록하되 집계하지 않았다.
두 번째 컨테이너에서는 다음 순서로 상태를 관찰했다. 초 단위 값은 StartedAt부터 호스트가 상태를 조회한 시각까지의 경과 시간을 반올림한 것이다. 정확한 상태 전환 시각이나 검사 스케줄을 뜻하지 않는다.
- 약 4.8초: 두 번째 검사 성공 뒤
healthy,FailingStreak=0. - 약 6.7초: 다음 검사가 실패했지만 상태는
healthy,FailingStreak=1. - 약 9.3초: 다시 실패해
healthy,FailingStreak=2. - 약 11.2초: 세 번째 연속 실패 뒤
unhealthy,FailingStreak=3.
마지막 조회의 Log에 들어 있던 Output 값을 줄바꿈으로 나열하면 다음과 같았다.
mode=once probe=1 exit=1
mode=once probe=2 exit=0
mode=once probe=3 exit=1
mode=once probe=4 exit=1
mode=once probe=5 exit=1
실패 행은 네 개지만 집계된 연속 실패는 세 번이다. 두 번째 검사의 성공이 경계다. 첫 실패는 유예 중에 집계되지 않았고, 성공 뒤 세 번의 실패가 retries=3에 도달했다. 중간의 healthy 두 조회에서도 실패 횟수는 이미 늘고 있었다. healthy라는 상태만 읽으면 이 변화를 놓친다.
유예를 늘리기 전에 probe가 언제 0을 내는지 확인한다
내 컨테이너에서도 유예 안의 성공 뒤 실패가 쌓였다면, 성공한 검사의 Output과 Test에 지정된 명령부터 확인한다. 프로세스가 뜬 것만 확인하고 0을 내는 검사라면, 실제 초기화가 끝나기 전에 Docker에 시작 완료를 알렸을 수 있다. 이는 점검할 가설이며 위 파일 카운터 실험이 실제 앱의 준비 상태까지 검증한 것은 아니다.
이 규칙에 따르면 start-period만 늘려도 첫 성공 뒤의 실패를 계속 무시하게 만들 수는 없다. 앱이 사용할 수 있는 상태에서만 검사 명령이 0을 반환하는지 확인해야 한다. 반대로 성공 없이 starting과 FailingStreak=0이 유지되고 있다면, 유예 중 실패를 아직 집계하지 않는 경우와 맞는다. 한 번의 조회로 앞뒤 관계를 확인할 수 없다면 시작 직후부터 같은 두 조회 결과를 시각과 함께 보존한다. 현재 출력에 성공이 없다는 이유만으로 과거에도 성공이 없었다고 단정하지 않는다.
실행 환경은 Windows 11, PowerShell 7.6.6, Docker Desktop 4.92.0의 Linux Engine 29.8.0이었다. 확인한 것은 90초 유예가 끝나기 전 첫 성공 유무에 따른 실패 집계 차이다. 유예 만료 순간, 서로 다른 두 검사 간격의 전환, 검사 시간 초과, 실제 HTTP 서비스의 초기화는 비교하지 않았다. 검사 주기를 2초로 설정했더라도 매번 정확히 2,000ms에 실행된다고 보장하는 관찰은 아니다.