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

빌드노트

Dockerfile에 없는 HEALTHCHECK가 실행될 때

자식 Dockerfile에서 HEALTHCHECK를 생략해도 베이스 이미지의 검사는 남는다. 실행 이미지와 컨테이너의 설정·health 로그로 상속을 확인하고, 새 검사 선언과 HEALTHCHECK NONE의 차이를 실제 빌드로 비교한다.

새 Dockerfile에 HEALTHCHECK를 쓰지 않았는데도 컨테이너가 예전 서비스의 주소를 검사하며 unhealthy가 될 수 있다. FROM의 베이스 이미지에 있던 헬스체크가 상속되기 때문이다. 자식 Dockerfile에서 선언을 생략해도 기존 검사가 없어지지는 않는다. 먼저 실행 중인 검사와 베이스의 설정을 대조하고, 새 서비스에도 검사가 필요하면 대상에 맞게 다시 선언한다. 상속 검사가 실제로 불필요할 때 쓰는 명시적 비활성화가 HEALTHCHECK NONE이다. Dockerfile HEALTHCHECK 문서 (새 탭에서 열림)

Dockerfile보다 현재 적용된 설정을 먼저 본다

다음 조회로 컨테이너를 만든 이미지의 검사, 컨테이너에 적용된 검사, 실제 실행 결과를 나란히 확인한다. Docker에 연결된 PowerShell에서 my-app을 자신의 컨테이너 이름이나 ID로 바꾼다. 이 글의 실행 예는 Linux 컨테이너를 대상으로 한다.

$container = 'my-app'
$imageId = docker container inspect --format '{{.Image}}' $container
if ($LASTEXITCODE -ne 0) { throw '컨테이너를 조회하지 못했습니다.' }
docker image inspect --format '{{json .Config.Healthcheck}}' $imageId
docker container inspect --format '{{json .Config.Healthcheck}}' $container
docker container inspect --format '{{json .State.Health}}' $container

Config.Healthcheck의 Test는 실행할 명령이다. State.Health의 Status와 Log는 그 명령을 실행한 결과다. 이미지 조회에 컨테이너의 .Image 값을 쓰는 이유는, 같은 태그로 나중에 빌드한 이미지와 현재 컨테이너를 만든 이미지를 혼동하지 않기 위해서다. 이미지나 컨테이너를 찾지 못한 오류는 설정이 없다는 뜻이 아니므로 조회부터 바로잡는다. 이미지 inspect (새 탭에서 열림), 컨테이너 inspect (새 탭에서 열림)

이미지와 컨테이너의 검사가 다르면 생성할 때 지정한 --health-cmd, --no-healthcheck 같은 설정을 확인한다. Docker 실행 옵션에서도 검사를 지정하거나 끌 수 있다. 이번 실험에서는 이런 옵션을 넣지 않았다. 두 설정이 같더라도 Config.Healthcheck만으로 어느 Dockerfile에서 선언됐는지 알 수는 없다. 빌드에 사용한 베이스와 대조해야 한다. docker run의 health 옵션 (새 탭에서 열림)

베이스의 검사도 다음과 같이 조회한다. company/service-base:tag는 예시이므로 자신의 Dockerfile에서 해당 이미지를 만드는 FROM의 베이스로 바꾼다. 태그가 갱신됐다면 현재 태그의 내용만으로 과거 빌드의 상속을 단정할 수 없다. 가능하면 그 빌드에 사용한 digest 참조로 확인한다.

$baseImage = 'company/service-base:tag'
docker image inspect --format '{{json .Config.Healthcheck}}' $baseImage

상속 여부를 확인하려고 별도의 실습용 베이스를 만들었다. 베이스의 합성 HTTP 응답기는 127.0.0.1:8080/legacy-ready에서 legacy-ok를 반환하고, 검사는 이 주소를 조회한다. 자식 이미지는 응답 대상을 /new-ready로 바꿔 new-ok를 반환하게 했다. 베이스에서는 유효한 검사가 새 서비스에는 맞지 않는 조건이다. 호스트 포트 공개 없이 컨테이너 내부의 loopback에서 비교했다.

헬스체크를 선언하지 않은 자식의 이미지와 컨테이너에는 베이스와 같은 Test가 남았다. 아래는 앞선 조회 결과에서 Test 값만 발췌한 것이다. BASE_LEGACY_PROBE는 어느 검사가 실행됐는지 식별하려고 넣은 실습용 출력이다.

["CMD-SHELL","echo BASE_LEGACY_PROBE; /bin/busybox wget -q -T 1 -O /dev/null http://127.0.0.1:8080/legacy-ready || exit 1"]

베이스 컨테이너는 이 검사로 healthy가 됐다. 같은 검사를 상속한 자식에서는 /legacy-ready가 404여서 unhealthy가 됐다. 자식의 State.Health.Log에서 시각을 제외한 최근 검사 한 건은 다음과 같았다.

{"ExitCode":1,"Output":"BASE_LEGACY_PROBE\nwget: server returned error: HTTP/1.1 404 Not Found\n"}

이 자식의 /new-ready에는 별도 HTTP 요청이 200으로 성공했고 본문도 new-ok였다. 검사 도구가 없어서 실패한 상황이 아니라, 실행된 검사가 이전 서비스의 경로를 계속 보고 있는 상황이었다. 자신의 로그에서 현재 서비스에 필요한 주소가 실패한다면 이 실험과 원인이 같다고 볼 수 없다. 응답 오류 자체를 조사해야 한다.

검사가 필요하면 새 서비스의 주소로 다시 선언한다

이 실습의 자식 Dockerfile에는 다음 선언을 추가했다. 이미지에 있는 /bin/busybox wget으로 실제 성공을 확인한 /new-ready를 검사하고, 식별 출력도 CHILD_NEW_PROBE로 바꿨다. 자신의 서비스에서는 컨테이너 안에서 사용할 수 있는 도구와 내부 포트·경로로 지정해야 한다.

HEALTHCHECK --interval=1s --timeout=2s --retries=2 \
  CMD echo CHILD_NEW_PROBE; /bin/busybox wget -q -T 1 -O /dev/null http://127.0.0.1:8080/new-ready || exit 1

이 선언으로 빌드한 자식은 healthy가 됐고 최근 검사 결과는 ExitCode=0, Output="CHILD_NEW_PROBE\n"였다. 이미지의 Test도 새 주소를 가리켰다. 기존 검사와 새 검사가 함께 실행된 것이 아니라 새 선언이 적용됐다. Dockerfile에 여러 HEALTHCHECK를 적어도 마지막 선언만 유효하다는 문서의 규칙과도 맞는다. HEALTHCHECK 선언 규칙 (새 탭에서 열림)

대체 검사가 무조건 성공하는지 확인하기 위해 이 컨테이너에서 /new-ready의 응답 파일을 잠시 없앴다. 새 검사도 코드 1로 실패하며 unhealthy가 됐고, 파일을 복원하자 다시 healthy가 됐다. 이 결과가 확인한 것은 합성 HTTP 경로의 응답 여부다. 실제 서비스의 DB 연결이나 업무 처리까지 검증한 것은 아니다. 1s 주기도 비교를 빨리 관찰하기 위한 실습 설정이므로, 실제 주기와 제한 시간·실패 횟수는 서비스 조건에 맞춰 정한다.

NONE은 정상 판정이 아니라 검사 비활성화다

새 이미지에서 Docker 헬스체크를 사용하지 않기로 결정했고 상속 검사도 필요 없다면, 자식 Dockerfile에 다음 한 줄을 명시한다. 검사 대상만 바꾸면 되는 앞의 조건과 구분해서 선택한다.

HEALTHCHECK NONE

같은 새 서비스를 실행하되 이 선언만 적용한 자식도 빌드해 비교했다. 앞의 이미지·컨테이너 설정 조회는 모두 {"Test":["NONE"]}을 반환했고, State.Health 조회는 null이었다. 전체 State에서도 Health 필드가 없었다. 컨테이너는 실행 중이었고 /new-ready 요청은 성공했지만, Docker가 검사를 실행해 healthy로 판정한 결과는 아니었다.

따라서 unhealthy 표시가 사라졌다는 이유만으로 장애가 해결됐다고 판단하면 안 된다. 이 경우 확인한 변화는 상속 검사가 실행되지 않는다는 것뿐이다. 새 서비스의 준비 상태를 계속 확인해야 한다면 적합한 검사를 선언하는 쪽을 선택한다.

Dockerfile을 수정한 뒤에는 새 이미지로 컨테이너를 재생성하고 처음의 조회를 다시 실행한다. 대체했다면 새 Test와 성공한 검사 로그를, 비활성화했다면 NONE과 health 필드 부재를 각각 확인한다. 예전 컨테이너를 다시 시작한 것만으로 새 Dockerfile의 설정이 적용됐다고 보지 않는다.

직접 확인한 환경은 Windows의 PowerShell 7.6.6, Docker Desktop 4.92.0의 Linux Engine 29.8.0, Alpine 3.22.7과 BusyBox 1.37.0이었다. 베이스·생략·NONE·대체 이미지를 실제 빌드하고 컨테이너로 비교했다. Windows 컨테이너, Compose의 health 설정 덮어쓰기, 실제 서비스 배포는 검증하지 않았다.

← 목록으로 돌아가기