> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# Docker unhealthy인데 자동 재시작 안 될 때: healthcheck와 restart 정책 구분
- URL: https://mlog.me/docker-unhealthy-healthcheck-restart-policy/
- Published: 2026-09-22T05:46:43.000Z
- Updated: 2026-09-22T05:46:43.000Z
- Description: Docker 컨테이너가 unhealthy인데 재시작되지 않는 이유를 정리합니다. healthcheck, restart 정책, depends_on의 차이와 읽기 전용 진단 명령, 안전한 복구 순서를 확인하세요.
- Author: mLog
- Tags: 서버·인프라, Docker, Linux, #Docker Compose, 모니터링

`docker compose ps`에는 컨테이너가 실행 중이라고 나오는데 뒤에 `unhealthy`가 붙는다. `restart: unless-stopped`도 설정했다. 그런데 재시작은 일어나지 않는다. 설정이 무시된 것일까?

일반 Docker Engine에서 Compose로 실행한 컨테이너라면 먼저 **건강 상태와 프로세스 종료를 구분**해야 한다. `healthcheck`는 검사 결과를 기록하고, 컨테이너 재시작 정책은 종료 등에 대한 동작을 정한다. 주 프로세스가 살아 있는 상태에서 검사만 실패했다고 `restart` 정책이 자동 복구를 수행하는 것은 아니다. 이는 Docker의 두 기능 설명을 함께 적용한 결론이다. [Docker HEALTHCHECK](https://docs.docker.com/reference/dockerfile/?ref=mlog.me#healthcheck), [재시작 정책](https://docs.docker.com/engine/containers/start-containers-automatically/?ref=mlog.me)

이 글은 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](https://docs.docker.com/reference/compose-file/services/?ref=mlog.me#depends%5Fon), [시작 순서](https://docs.docker.com/compose/how-tos/startup-order/?ref=mlog.me)

다음은 위치를 비교하기 위한 **설정 조각**이며 배포 가능한 전체 파일은 아니다. 기존 이미지·DB·healthcheck 설정을 이 코드로 대체하지 않는다.

```yaml
services:
  app:
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
        restart: true

```

DB가 나중에 응답하지 않는 상황에는 앱의 재연결 처리와 장애 알림이 별도로 필요하다. 시작 시 한 번 준비 상태를 확인했다는 사실로 이후의 연결 성공을 보장할 수 없다.

## 2\. 재시작 전에 정확한 컨테이너부터 확인한다

먼저 기존 배포와 같은 Docker context, Compose 파일, 프로젝트를 선택한다. `-f`, `-p`, `--env-file` 및 여러 override 파일을 쓰고 있었다면 같은 인자를 유지한다. 아래 조회도 해당 프로젝트를 가리키는 디렉터리·인자를 확인한 뒤 실행한다.

```bash
docker context show
docker compose version
docker compose ps -a

```

`-a`를 붙여 종료된 컨테이너도 함께 확인한다. 서비스에 인스턴스가 여러 개면 모두 같은 상태라고 가정하지 말고 장애가 난 컨테이너 ID를 고른다. [Compose ps](https://docs.docker.com/reference/cli/docker/compose/ps/?ref=mlog.me)

다음 값은 반드시 방금 확인한 실제 ID로 바꾼다. `YOUR_CONTAINER_ID`는 예시 자리표시자다.

```bash
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](https://docs.docker.com/reference/cli/docker/inspect/?ref=mlog.me)

## 3\. 검사 실패와 앱 장애를 분리한다

건강 상태 검사의 기본 흐름은 `starting`에서 검사 성공 시 `healthy`, 연속 실패가 기준에 이르면 `unhealthy`다. 검사 명령은 컨테이너 안에서 실행되므로, 호스트에서 성공한 명령이 그대로 성공한다는 보장은 없다. [Docker HEALTHCHECK](https://docs.docker.com/reference/dockerfile/?ref=mlog.me#healthcheck)

실패 출력이 필요하면 권한 있는 터미널에서 다음을 확인한다. **출력에는 URL·응답 본문·토큰 등이 포함될 수 있다. 외부 공유 전에 가린다.**

```bash
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 진단](https://mlog.me/ghost-getaddrinfo-enotfound-db-docker-compose-mysql/)으로 분기한다. 내부 검사는 성공하지만 외부에서 502가 발생한다면 [NPM 502 계층별 진단](https://mlog.me/nginx-proxy-manager-502-bad-gateway-docker-network/)을 참고한다. 한 검사 결과로 프록시·TLS·사용자 기능까지 모두 정상이라고 결론 내리지 않는다.

앱 로그도 범위를 제한해 읽는다. `app`은 실제 서비스명으로 바꾼다.

```bash
docker compose logs --since 10m --tail 100 app

```

이 명령은 원인을 고치지 않고 로그만 조회한다. 앱 로그에도 개인정보·비밀값이 있을 수 있으며, 설정된 로깅 방식에 따라 필요한 로그가 여기에 없을 수도 있다. [Compose logs](https://docs.docker.com/reference/cli/docker/compose/logs/?ref=mlog.me)

## 4\. 유예 시간은 장애를 숨기는 설정이 아니다

초기화가 오래 걸리는 앱은 시작 직후의 검사 실패와 평상시 장애를 구분해야 한다. `start_period` 안의 실패는 재시도 실패 횟수에 포함되지 않지만, 그 안에 한 번 성공하면 이후 연속 실패가 집계된다. `timeout`은 검사 한 번의 제한이고 `retries`는 연속 실패 기준이다. [Docker HEALTHCHECK](https://docs.docker.com/reference/dockerfile/?ref=mlog.me#healthcheck)

따라서 고정된 “몇 초면 반드시 unhealthy”라는 계산보다 실제 시작 시간과 검사 기록을 본다. 임계값을 무작정 늘리거나 검사를 끄면 화면은 조용해져도 장애 원인은 남는다. 조정이 필요하면 정상 기동 시간, 외부 의존성 지연, 허용 가능한 탐지 지연을 근거로 결정한다.

## 5\. 수정한 검사 설정은 재시작만으로 반영하지 않는다

Compose의 healthcheck를 수정했다면 파일 변경과 실행 컨테이너의 설정 반영은 별개다. `docker compose restart`는 Compose 설정 변경을 적용하는 명령이 아니다. 관련 차이는 [환경변수 변경과 재생성 글](https://mlog.me/docker-compose-env-change-restart-vs-up/)에서 다뤘다. [Docker restart](https://docs.docker.com/reference/cli/docker/compose/restart/?ref=mlog.me)

적용 전에 대상 서비스의 중단 영향, 데이터 저장 위치, 백업과 되돌릴 설정을 확인한다. 반영이 승인된 경우 기존 배포 인자를 유지한 `docker compose up -d app`처럼 범위를 제한할 수 있다. `up`은 변경된 구성에 따라 컨테이너를 재생성할 수 있으므로 단순 조회나 무중단 명령이 아니다. [Docker up](https://docs.docker.com/reference/cli/docker/compose/up/?ref=mlog.me)

수정 후에는 새 컨테이너 ID를 다시 확인해 2\~3절을 반복하고, 실제 사용자 요청도 점검한다. 이전 컨테이너 ID를 계속 조회하면 새 상태를 보지 못한다. `up --wait` 역시 실행·건강 상태를 기다리는 옵션이지, 이후 장애를 영구 감시하는 복구 서비스는 아니다.

## 6\. 자동 복구가 필요하면 조건과 권한부터 설계한다

자동 복구 도구를 추가하는 선택은 가능하지만, 모든 unhealthy 컨테이너를 무조건 재시작하는 정책은 이 글에서 권하지 않는다. 다음은 운영 설계 제안이지 Docker의 기본 보장 사항이 아니다.

1. 재시작 대상 서비스를 명시적으로 제한한다.
2. 한 번의 실패가 아니라 지속 실패와 실제 기능 장애를 함께 확인한다.
3. 재시도 상한·대기 시간·알림·사람의 개입 조건을 둔다.
4. DB 장애처럼 재시작으로 해결되지 않는 원인은 별도 경로로 처리한다.
5. 유지보수 중 의도적으로 중지한 서비스를 다시 살리지 않도록 한다.

특히 복구 도구에 Docker 소켓 접근을 주는 것은 단순 모니터링 권한 부여로 취급할 일이 아니다. 제어 권한과 신뢰 경계를 먼저 검토한다. [Docker 소켓 보호](https://docs.docker.com/engine/security/protect-access/?ref=mlog.me), [Portainer와 Docker 소켓 보안](https://mlog.me/portainer-docker-management-security-check/)

증거를 남기기 전에 전체 스택을 내리거나 볼륨을 지우지 않는다. 검사를 항상 성공시키는 임시 우회도 장애 해결로 기록하지 않는다. 원인을 고친 뒤 건강 상태와 사용자 기능을 따로 확인하는 편이 다음 장애 때도 재사용할 수 있는 절차다.

## 결론

`unhealthy`는 상태 신호이고, `restart`는 복구 동작을 결정하는 별도 정책이다. 신호가 있다고 동작까지 자동으로 연결된 것은 아니다. 먼저 **대상 컨테이너 → 검사 실패 이유 → 실제 기능 영향 → 최소 범위 수정** 순서로 확인하자. 그 다음에야 자동 복구가 필요한지, 어떤 조건과 권한으로 실행할지 결정할 수 있다.