infra
Platform

모듈 맵

[Docker] 컨테이너 상태 진단을 위한 헬스체크와 Restart Policy 설정

0 / 27 완료

펼치기
0 / 27 완료0%

Docker · 19 / 27

[Docker] 컨테이너 상태 진단을 위한 헬스체크와 Restart Policy 설정

HEALTHCHECK 지시어와 restart 정책으로 자가 치유(self-healing) 컨테이너를 구성합니다

헬스체크와 자동 복구 정책

🚨INCIDENT ALERT
HIGH

컨테이너는 "Up" 상태인데 사용자 요청은 실패하고, 장애는 아침이 되어서야 발견됩니다. 실무에서 중요한 건 프로세스 생존이 아니라 서비스 응답 가능 상태를 지속적으로 검증하는 것입니다. HEALTHCHECK와 restart 정책을 잘못 조합하면 오히려 조기 실패나 무한 재시작을 만들 수 있습니다. 이 모듈은 상태 판단 기준과 자동 복구 경계를 명확히 잡는 데 집중합니다.

컨테이너가 실행 중(Running)이라는 사실이 곧 서비스가 정상적으로 동작한다는 의미는 아닙니다. 프로세스는 살아 있지만 DB 커넥션 풀이 고갈되거나, 메모리 누수로 응답 불능 상태가 되거나, 초기화 도중 의존성이 준비되지 않아 요청을 처리하지 못하는 경우가 실무에서 빈번히 발생합니다. Docker의 HEALTHCHECKrestart 정책을 조합하면 이런 상황을 자동으로 감지하고 복구하는 자가 치유(self-healing) 컨테이너를 구현할 수 있습니다.


이번 챕터에서 배울 것

컨테이너가 단순히 '켜져 있음'을 넘어 '실제로 요청을 처리할 수 있는 상태'인지 지속적으로 검증하고, 문제 발생 시 자동으로 복구되도록 구성하는 방법을 익힙니다.

  • 1HEALTHCHECK 지시어 문법 — --interval, --timeout, --retries, --start-period 4개 옵션의 역할
  • 2컨테이너 헬스 상태 3가지 — starting / healthy / unhealthy 와 상태 전환 조건
  • 3restart 정책 4가지 — no / always / on-failure / unless-stopped 비교와 선택 기준
  • 4docker-compose healthcheck + depends_on condition: service_healthy 연동
  • 5실전 DB/웹서버 헬스체크 패턴 — pg_isready, mysqladmin ping, curl, wget
실습 환경 준비

HEALTHCHECK 지시어는 Dockerfile에서 직접 지정하거나 docker-compose.yml의 healthcheck 키로 설정할 수 있습니다. 이미 빌드된 공식 이미지에 헬스체크를 추가할 때는 Compose 파일 방식이 편리합니다.

Docker 버전 확인 (HEALTHCHECK는 1.12 이상에서 지원)
docker --version
실습용 디렉토리 생성
mkdir -p ~/healthcheck-lab && cd ~/healthcheck-lab
실습에 필요한 이미지 미리 pull
docker pull nginx:alpine && docker pull postgres:16-alpine && docker pull redis:7-alpine
💡개념

HEALTHCHECK 지시어 문법과 옵션

docker ps에서 컨테이너가 "Up 3 hours"로 표시되는데 사용자는 502 Bad Gateway를 받고 있습니다. 프로세스는 살아 있지만 DB 커넥션이 고갈되어 요청을 처리하지 못하는 상태입니다. "컨테이너가 실행 중"이라는 사실이 "서비스가 정상"을 의미하지 않습니다. HEALTHCHECK는 컨테이너 내부에서 실제로 요청을 처리할 수 있는지 주기적으로 검사하고, 그 결과에 따라 starting/healthy/unhealthy 세 가지 상태를 부여합니다. 이 ConceptBlock에서는 HEALTHCHECK 지시어의 문법과 네 가지 옵션의 역할을 다룹니다.

HEALTHCHECK 문법과 옵션

HEALTHCHECK란

Docker의 HEALTHCHECK 지시어는 컨테이너의 상태를 주기적으로 검사하여 세 가지 상태 중 하나로 분류합니다.

상태의미전환 조건
starting초기화 중컨테이너 시작 직후 기본 상태
healthy정상헬스체크 명령어가 exit code 0 반환
unhealthy비정상헬스체크가 --retries 횟수만큼 연속 실패

Dockerfile HEALTHCHECK 기본 문법

Dockerfile
HEALTHCHECK [옵션] CMD <명령어>

네 가지 핵심 옵션: interval, timeout, retries, start_period 네 가지로 헬스체크 동작을 제어합니다.

Dockerfile
HEALTHCHECK \
  --interval=30s \    # 헬스체크 실행 주기 (기본값: 30s)
  --timeout=10s \     # 명령어 응답 대기 시간 (기본값: 30s)
  --start-period=40s \ # 시작 유예 기간 (기본값: 0s)
  --retries=3 \       # 연속 실패 허용 횟수 (기본값: 3)
  CMD curl -f http://localhost/health || exit 1

옵션별 동작 상세 설명

타임라인:
컨테이너 시작
    │
    ├─ [start-period 40s] ─────────────────┐
    │  이 기간 동안 실패해도 retries 카운터  │
    │  증가 없음 (starting 상태 유지)        │
    │                                      │
    └──────────────────────────────────────┘
    │
    ├─ interval=30s ──┐ interval=30s ──┐ interval=30s
    │                 │                │
   [check1: 성공]   [check2: 실패]   [check3: 실패]
    │                 │                │
  healthy           retries=1        retries=2
                                       │
                              [check4: 실패] → retries=3 → unhealthy!

실제 Dockerfile 예시

Nginx 웹서버 헬스체크: curl로 HTTP 응답을 확인하는 가장 일반적인 패턴입니다.

Dockerfile
FROM nginx:alpine

# nginx에 curl 설치
RUN apk add --no-cache curl

HEALTHCHECK \
  --interval=15s \
  --timeout=5s \
  --start-period=10s \
  --retries=3 \
  CMD curl -f http://localhost/ || exit 1

COPY nginx.conf /etc/nginx/nginx.conf

Node.js 애플리케이션 헬스체크: /health 엔드포인트가 있는 경우 이를 활용합니다.

Dockerfile
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

EXPOSE 3000

HEALTHCHECK \
  --interval=20s \
  --timeout=5s \
  --start-period=30s \
  --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"

CMD ["node", "server.js"]

헬스체크 비활성화

베이스 이미지에 이미 HEALTHCHECK가 정의되어 있을 때 이를 무효화하려면:

Dockerfile
HEALTHCHECK NONE

💡개념

restart 정책 4가지 비교

새벽 3시에 OOM으로 Node.js 컨테이너가 종료됐습니다. 아침에 출근해서야 서비스가 내려간 것을 알았습니다. restart: unless-stopped를 설정했다면 컨테이너가 자동으로 재시작되어 사용자는 짧은 중단만 경험했을 것입니다. 반대로 유지보수를 위해 docker stop으로 컨테이너를 내렸는데 서버 재부팅 후 다시 올라오면 의도치 않은 동작입니다. 재시작 정책은 "어떤 상황에서 자동으로 다시 켜지게 할 것인가"를 제어합니다. 이 ConceptBlock에서는 no/always/on-failure/unless-stopped 네 가지 정책의 동작 차이와 선택 기준을 다룹니다.

restart 정책 비교

restart 정책이란

컨테이너가 종료됐을 때 Docker 데몬이 어떻게 처리할지를 지정합니다. docker run --restart 또는 docker-compose.ymlrestart 키로 설정합니다.

4가지 정책 상세 비교

1. no (기본값)

YAML
restart: "no"

컨테이너가 종료되면 재시작하지 않습니다. 개발 중 일회성으로 실행하는 컨테이너나 배치 작업에 적합합니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part5/exam_19 && cd /tmp/docker/part5/exam_19

$ docker run --rm alpine echo "작업 완료"
작업 완료
# 종료 후 자동 삭제, 재시작 없음

2. always

YAML
restart: always
  • 종료 코드와 무관하게 항상 재시작
  • Docker 데몬이 재시작될 때도 컨테이너를 자동으로 시작
  • 수동으로 docker stop해도 데몬 재시작 시 다시 켜짐
로컬 터미널
$ docker run -d --restart=always --name demo nginx:alpine
$ docker stop demo
$ sudo systemctl restart docker
$ docker ps   # demo 컨테이너가 다시 실행 중!

3. on-failure[:횟수]

YAML
restart: on-failure:3
  • 비정상 종료(exit code != 0) 시에만 재시작
  • 최대 재시작 횟수 제한 가능
  • 정상 종료(exit code 0)에는 재시작하지 않음
  • 크래시 루프를 방지할 수 있어 디버깅에 유용
로컬 터미널
# 의도적으로 실패하는 컨테이너 테스트
$ docker run -d --restart=on-failure:3 --name crasher \
    alpine sh -c "echo '크래시!' && exit 1"

$ docker ps -a
# CONTAINER ID   STATUS
# abc123         Restarting (1) 5 seconds ago
# → 3회 실패 후 더 이상 재시작 안 함

$ docker inspect crasher --format '{{.RestartCount}}'
3

4. unless-stopped

YAML
restart: unless-stopped
  • always와 거의 동일하지만 수동 중지 상태를 기억
  • docker stop으로 명시적으로 중지한 컨테이너는 Docker 데몬 재시작 후에도 다시 시작하지 않음
  • 운영 환경에서 가장 권장되는 정책
재시작 정책 비교표:

상황                          | no  | always | on-failure | unless-stopped
-----------------------------|-----|--------|------------|---------------
정상 종료 (exit 0)            |  -  |  재시작 |     -      |    재시작
비정상 종료 (exit != 0)       |  -  |  재시작 |   재시작    |    재시작
docker stop 후 데몬 재시작    |  -  |  재시작 |   재시작    |      -
크래시 무한 루프 방지          |  O  |   X    | O (횟수제한)|      X

실무 선택 기준

YAML
# 개발/테스트: 재시작 없이 로그 확인
restart: "no"

# 웹서버, API 서버 등 항상 켜져야 하는 서비스
restart: unless-stopped

# 시스템 필수 서비스 (모니터링 에이전트 등)
restart: always

# 빠른 크래시-재시작 루프를 방지해야 하는 서비스
restart: on-failure:5

기본 실습

1nginx 컨테이너에 HEALTHCHECK 추가

nginx에 헬스체크를 추가한 커스텀 이미지를 만들고 상태 변화를 관찰합니다.

실습 전 디렉토리와 예제 파일을 먼저 준비합니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part3/exam_4 && cd /tmp/docker/part3/exam_4

# 헬스체크 실습용 기본 Dockerfile 생성
cat > Dockerfile << 'EOF'
FROM nginx:alpine
RUN apk add --no-cache curl
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost/ || exit 1
EOF

이제 실습을 진행합니다.

Dockerfile 작성: 헬스체크가 포함된 Dockerfile입니다.

Dockerfile
# ~/healthcheck-lab/nginx-health/Dockerfile
FROM nginx:alpine

# curl 설치 (헬스체크에 필요)
RUN apk add --no-cache curl

# 커스텀 헬스체크 엔드포인트 생성
RUN echo '{"status":"ok","service":"nginx"}' > /usr/share/nginx/html/health

HEALTHCHECK \
  --interval=10s \
  --timeout=3s \
  --start-period=5s \
  --retries=3 \
  CMD curl -f http://localhost/health || exit 1

EXPOSE 80

이미지 빌드 및 실행: 빌드 후 실행해서 헬스체크가 동작하는지 확인합니다.

로컬 터미널
cd ~/healthcheck-lab/nginx-health
docker build -t nginx-health:v1 .
docker run -d --name nginx-hc -p 8090:80 nginx-health:v1

헬스 상태 확인 (시작 직후 — starting 상태): start_period 동안은 starting 상태로 표시됩니다.

로컬 터미널
docker ps --format "table {{.Names}}\t{{.Status}}"
# NAMES       STATUS
# nginx-hc    Up 3 seconds (health: starting)

10초 후 healthy 상태 확인: 헬스체크가 통과하면 healthy로 전환됩니다.

로컬 터미널
docker ps --format "table {{.Names}}\t{{.Status}}"
# NAMES       STATUS
# nginx-hc    Up 15 seconds (healthy)

# 헬스체크 이력 상세 확인
docker inspect nginx-hc --format '{{json .State.Health}}' | python3 -m json.tool
JSON
{
  "Status": "healthy",
  "FailingStreak": 0,
  "Log": [
    {
      "Start": "2026-03-28T10:00:10Z",
      "End": "2026-03-28T10:00:10.123Z",
      "ExitCode": 0,
      "Output": "  % Total    % Received\n100    28  100    28  0:00:00\n{\"status\":\"ok\",\"service\":\"nginx\"}"
    }
  ]
}

unhealthy 상태 강제 유발 — nginx 프로세스 중단: nginx를 중단하면 헬스체크가 실패하고 unhealthy로 바뀝니다.

Docker
# 컨테이너 내부에서 nginx 중지 (프로세스 종료 아님)
docker exec nginx-hc nginx -s stop

# 잠시 후 상태 확인 (retries=3이므로 약 30초 후 unhealthy)
watch docker ps --format "table {{.Names}}\t{{.Status}}"
# nginx-hc    Up 45 seconds (unhealthy)

정리: 실습 컨테이너를 삭제합니다.

위험 명령어강제 삭제 시 헬스체크 로그/상태 이력을 더 이상 분석할 수 없습니다.

실행 중 컨테이너 강제 삭제 전에 상태를 확정

안전한 실행 조건: 실습 검증이 끝났고, 해당 컨테이너를 더 이상 진단하지 않을 때만 실행합니다.

실행 전 반드시 확인

  • docker inspect로 Health 상태를 이미 확인했는가?
  • 필요한 로그를 캡처했는가?
  • 운영 컨테이너가 아닌 실습용 컨테이너가 맞는가?
docker rm -f nginx-hc

위 항목을 모두 확인한 후 복사할 수 있습니다

로컬 터미널
docker rm -f nginx-hc
mkdir -p ~/healthcheck-lab/nginx-health && cd ~/healthcheck-lab/nginx-health
2DB가 준비될 때까지 기다리는 Compose 설정

PostgreSQL이 완전히 초기화될 때까지 애플리케이션 서비스가 시작을 기다리는 구성을 실습합니다.

docker-compose.yml 작성: DB가 healthy 상태가 될 때까지 앱이 대기하는 구성입니다.

YAML
# ~/healthcheck-lab/db-wait/docker-compose.yml
version: "3.9"

services:
  postgres:
    image: postgres:16-alpine
    container_name: hc_postgres
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppass
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      # pg_isready: PostgreSQL 전용 연결 준비 확인 도구
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 15s
    networks:
      - app_net

  redis:
    image: redis:7-alpine
    container_name: hc_redis
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app_net

  app:
    image: alpine:latest
    container_name: hc_app
    # DB와 Redis가 모두 healthy 상태가 된 이후에만 시작
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: >
      sh -c "
        echo '=== 의존성 서비스 준비 완료 — 앱 시작 ===';
        echo 'PostgreSQL 연결 확인...';
        echo 'Redis 연결 확인...';
        echo '앱이 정상적으로 시작되었습니다.';
        sleep 60
      "
    networks:
      - app_net
    restart: on-failure:3

networks:
  app_net:
    driver: bridge

volumes:
  pg_data:

실행 및 시작 순서 관찰: 의존 서비스가 healthy가 될 때까지 기다리는 것을 확인합니다.

로컬 터미널
cd ~/healthcheck-lab/db-wait
docker-compose up

# 출력 예시:
# hc_postgres | PostgreSQL init process complete; ready for start up.
# hc_postgres | LOG:  database system is ready to accept connections
# hc_redis    | Ready to accept connections
# hc_app      | === 의존성 서비스 준비 완료 — 앱 시작 ===
# hc_app      | PostgreSQL 연결 확인...
# hc_app      | Redis 연결 확인...
# hc_app      | 앱이 정상적으로 시작되었습니다.

백그라운드 실행 후 헬스 상태 일괄 확인: 모든 서비스의 헬스 상태를 한 번에 확인합니다.

로컬 터미널
docker-compose up -d

# 각 서비스의 헬스 상태 확인
docker-compose ps
# NAME          SERVICE    STATUS                   PORTS
# hc_app        app        Up (healthy)
# hc_postgres   postgres   Up (healthy)             5432/tcp
# hc_redis      redis      Up (healthy)             6379/tcp

# PostgreSQL 헬스체크 로그 확인
docker inspect hc_postgres --format '{{range .State.Health.Log}}ExitCode: {{.ExitCode}} | {{.Output}}{{println}}{{end}}'
# ExitCode: 0 | /var/run/postgresql:5432 - accepting connections

정리: 실습 환경과 볼륨을 삭제합니다.

로컬 터미널
docker-compose down -v
mkdir -p ~/healthcheck-lab/db-wait && cd ~/healthcheck-lab/db-wait
3restart: on-failure:3 적용 후 의도적 크래시 테스트

재시작 정책이 실제로 어떻게 동작하는지 의도적으로 크래시를 발생시켜 확인합니다.

docker-compose.yml 작성: 재시작 정책을 테스트하기 위한 의도적 크래시 환경입니다.

YAML
# ~/healthcheck-lab/crash-test/docker-compose.yml
version: "3.9"

services:
  # 1) 정상 서비스 — restart: unless-stopped
  stable:
    image: nginx:alpine
    container_name: ct_stable
    restart: unless-stopped
    ports:
      - "8091:80"

  # 2) 크래시 반복 서비스 — restart: on-failure:3 (3회 후 포기)
  crasher:
    image: alpine:latest
    container_name: ct_crasher
    restart: on-failure:3
    command: sh -c "echo '크래시 시뮬레이션!' && exit 1"

  # 3) 헬스체크 실패 서비스 — unhealthy 상태 관찰
  sick:
    image: nginx:alpine
    container_name: ct_sick
    restart: on-failure:5
    healthcheck:
      # 존재하지 않는 엔드포인트를 체크 → 항상 실패
      test: ["CMD", "curl", "-f", "http://localhost/nonexistent-health"]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 3s
    ports:
      - "8092:80"

실행 후 재시작 카운터 관찰: 헬스체크 실패 시 컨테이너가 자동으로 재시작되는 것을 확인합니다.

로컬 터미널
cd ~/healthcheck-lab/crash-test
docker-compose up -d

# 30초 간격으로 크래셔 상태 관찰
watch -n 3 'docker inspect ct_crasher --format "재시작 횟수: {{.RestartCount}} | 상태: {{.State.Status}}"'

# 예상 출력 (30초 내):
# 재시작 횟수: 1 | 상태: restarting
# 재시작 횟수: 2 | 상태: restarting
# 재시작 횟수: 3 | 상태: exited
# (3회 초과 후 더 이상 재시작하지 않음)

sick 컨테이너 unhealthy 상태 확인: 헬스체크 실패 후 unhealthy로 전환된 것을 확인합니다.

로컬 터미널
# 약 20초 후 (start-period 3s + interval 5s × 3회)
docker ps --format "table {{.Names}}\t{{.Status}}"
# NAMES        STATUS
# ct_stable    Up 25 seconds
# ct_crasher   Exited (1) 10 seconds ago
# ct_sick      Up 25 seconds (unhealthy)

재시작 이력 로그 확인: 컨테이너가 몇 번이나 재시작됐는지 확인합니다.

로컬 터미널
docker inspect ct_crasher | python3 -c "
import sys, json
data = json.load(sys.stdin)[0]
print('최종 상태:', data['State']['Status'])
print('재시작 횟수:', data['RestartCount'])
print('종료 코드:', data['State']['ExitCode'])
print('오류 메시지:', data['State']['Error'])
"
# 최종 상태: exited
# 재시작 횟수: 3
# 종료 코드: 1
# 오류 메시지:

정리:

로컬 터미널
docker-compose down
mkdir -p ~/healthcheck-lab/crash-test && cd ~/healthcheck-lab/crash-test
🔍실행 후 확인할 것
  • starting/healthy/unhealthy 전환이 interval, retries, start-period 설정과 일치했는가?
  • restart 정책이 종료 코드 기반으로 의도대로 동작하는지 확인했는가?
  • 의존 서비스 준비 전 앱이 기동되지 않도록 service_healthy 조건을 검증했는가?

트러블슈팅

증상

Spring Boot, JVM 애플리케이션, 또는 초기화 시간이 긴 서비스에서 컨테이너가 시작된 직후 unhealthy 상태가 됩니다.

로컬 터미널
$ docker ps
CONTAINER ID   STATUS
abc123         Up 12 seconds (unhealthy)

$ docker inspect myapp --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{println}}{{end}}'
1 curl: (7) Failed to connect to localhost port 8080: Connection refused
1 curl: (7) Failed to connect to localhost port 8080: Connection refused
1 curl: (7) Failed to connect to localhost port 8080: Connection refused

원인 분석

--start-period가 설정되지 않으면 기본값 0s가 적용됩니다. 컨테이너가 시작되는 즉시 헬스체크가 시작되고, JVM 워밍업(통상 10~30초)이 완료되기 전에 --retries 횟수를 모두 소진합니다.

실패 타임라인 (--start-period=0s, --interval=5s, --retries=3):

T+0s:  컨테이너 시작 (JVM 부팅 중...)
T+5s:  1차 헬스체크 → Connection refused (실패 1/3)
T+10s: 2차 헬스체크 → Connection refused (실패 2/3)
T+15s: 3차 헬스체크 → Connection refused (실패 3/3) → unhealthy!
T+20s: JVM 부팅 완료 (이미 늦음)

해결 방법

--start-period를 애플리케이션 최대 시작 시간보다 넉넉하게 설정합니다.

Dockerfile
# 수정 전 (문제 있는 설정)
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

# 수정 후 (Spring Boot 권장 설정)
HEALTHCHECK \
  --interval=10s \
  --timeout=5s \
  --start-period=60s \  # JVM + Spring Boot 초기화를 위한 충분한 유예 기간
  --retries=5 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

docker-compose.yml에서 설정 시: Compose 파일에서 헬스체크를 정의하는 방법입니다.

YAML
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
  interval: 10s
  timeout: 5s
  start_period: 60s   # ← 이 설정이 핵심
  retries: 5

검증: 헬스체크 설정이 실제로 동작하는지 확인합니다.

로컬 터미널
docker ps --format "{{.Names}}: {{.Status}}"
# myapp: Up 65 seconds (healthy)   ← start-period 후 정상 판정

증상과 배경

PostgreSQL 컨테이너의 헬스체크를 curl로 설정했는데 작동하지 않거나, pg_isready를 사용했을 때 실제 DB 준비 여부와 다른 결과가 나옵니다.

로컬 터미널
# 잘못된 방법: curl은 PostgreSQL과 통신할 수 없음
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:5432/"]
  # 결과: curl: (1) Received HTTP/0.9 when not allowed

서비스별 올바른 헬스체크 명령어

PostgreSQL: pg_isready 명령어로 DB 준비 상태를 확인합니다.

YAML
# 방법 1: pg_isready (권장) — TCP 연결 준비 여부만 확인
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
  interval: 5s
  timeout: 3s
  retries: 5
  start_period: 10s

# 방법 2: psql (실제 쿼리 실행 — 더 엄격한 검증)
healthcheck:
  test: ["CMD-SHELL", "psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} -c 'SELECT 1' || exit 1"]

pg_isready 출력 예시: 정상일 때와 아닐 때의 출력 차이입니다.

로컬 터미널
$ docker exec hc_postgres pg_isready -U appuser -d appdb
/var/run/postgresql:5432 - accepting connections   # exit code 0 → healthy

$ docker exec hc_postgres pg_isready -U appuser -d appdb
/var/run/postgresql:5432 - rejecting connections   # exit code 1 → 실패

MySQL/MariaDB: mysqladmin ping으로 DB 응답을 확인합니다.

YAML
# mysqladmin ping 사용
healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]

# 또는 mysql 클라이언트로 쿼리 실행
healthcheck:
  test: ["CMD-SHELL", "mysql -u root -p${MYSQL_ROOT_PASSWORD} -e 'SELECT 1' || exit 1"]

Redis: redis-cli ping 명령어로 응답 여부를 확인합니다.

YAML
healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  # 성공 시 출력: PONG (exit code 0)

HTTP/HTTPS 웹서버: curl로 HTTP 상태코드를 확인하는 범용 패턴입니다.

YAML
# curl 사용 (curl이 이미지에 포함된 경우)
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost/health"]

# wget 사용 (Alpine 이미지 기본 포함)
healthcheck:
  test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]

# nc (netcat) — 포트 열림 여부만 확인
healthcheck:
  test: ["CMD-SHELL", "nc -z localhost 80 || exit 1"]

CMD vs CMD-SHELL 차이

YAML
# CMD: 환경변수 치환 불가, exec 형식으로 직접 실행
test: ["CMD", "pg_isready", "-U", "appuser"]

# CMD-SHELL: sh -c로 실행, 환경변수 치환 가능, 파이프/조건 연산자 사용 가능
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} || exit 1"]
#                                    ↑ 환경변수 사용 가능

실무 맥락

💼
실무 맥락Kubernetes 이전 단계에서 docker-compose 기반 self-healing 구성
현업 패턴

상황

스타트업 A사는 트래픽이 적은 내부 관리 도구를 EC2 단일 서버에 Docker Compose로 운영합니다. Kubernetes를 도입할 규모는 아니지만, 서버를 24시간 모니터링할 인력도 없어 자동 복구가 필요합니다.

실무 수준의 self-healing Compose 구성

YAML
# production/docker-compose.yml
version: "3.9"

x-healthcheck-defaults: &hc-defaults
  interval: 15s
  timeout: 5s
  retries: 3
  start_period: 30s

x-restart-policy: &restart-policy
  restart: unless-stopped

services:
  nginx:
    <<: *restart-policy
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/www:/var/www/certbot:ro
      - ./certbot/conf:/etc/letsencrypt:ro
    healthcheck:
      <<: *hc-defaults
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
      start_period: 10s
    depends_on:
      api:
        condition: service_healthy
    networks:
      - frontend

  api:
    <<: *restart-policy
    image: company/internal-api:${VERSION}
    env_file: .env.production
    healthcheck:
      <<: *hc-defaults
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
      start_period: 45s    # Node.js 초기화 시간 고려
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend
      - backend

  postgres:
    <<: *restart-policy
    image: postgres:16-alpine
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./backups:/backups
    env_file: .env.db
    healthcheck:
      <<: *hc-defaults
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      start_period: 15s
    networks:
      - backend

  redis:
    <<: *restart-policy
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      <<: *hc-defaults
      test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep PONG || exit 1"]
      start_period: 5s
    networks:
      - backend

networks:
  frontend:
  backend:

volumes:
  pg_data:
  redis_data:

운영팀이 추가하는 모니터링 스크립트

로컬 터미널
#!/bin/bash
# /opt/scripts/health-monitor.sh — cron으로 5분마다 실행

PROJECT_DIR="/opt/app/production"
ALERT_EMAIL="ops@company.com"

cd "$PROJECT_DIR"

# unhealthy 컨테이너 감지
UNHEALTHY=$(docker-compose ps --format json | \
  python3 -c "
import sys, json
services = [json.loads(l) for l in sys.stdin]
unhealthy = [s['Name'] for s in services if 'unhealthy' in s.get('Health', '')]
print('\n'.join(unhealthy))
")

if [ -n "$UNHEALTHY" ]; then
  echo "ALERT: unhealthy 컨테이너 감지: $UNHEALTHY"

  # 헬스체크 로그 수집
  for container in $UNHEALTHY; do
    docker inspect "$container" \
      --format '{{range .State.Health.Log}}{{.ExitCode}}: {{.Output}}{{end}}'
  done | mail -s "Docker Health Alert" "$ALERT_EMAIL"

  # 자동 재시작 시도
  docker-compose restart $UNHEALTHY
fi

Kubernetes 이전 시 주요 변경점

기능Docker ComposeKubernetes
헬스체크HEALTHCHECK / healthchecklivenessProbe / readinessProbe
자동 재시작restart 정책restartPolicy
의존성 순서depends_on + conditioninitContainers
자가 치유단일 서버 내에서만노드 장애 시에도 복구

HEALTHCHECKrestart 정책으로 익힌 개념은 Kubernetes의 Probe 설정으로 자연스럽게 이어집니다. 원리는 동일하고 키워드만 바뀝니다.


💡개념

HEALTHCHECK 상태 전파와 depends_on 연동

docker compose up으로 앱과 DB를 함께 올렸는데 앱이 "DB 연결 실패"로 크래시합니다. DB 컨테이너는 시작됐지만 PostgreSQL 프로세스가 아직 초기화 중이었기 때문입니다. depends_on: db만으로는 컨테이너 시작 순서만 보장하고 DB가 실제로 연결을 받을 준비가 됐는지는 보장하지 않습니다. condition: service_healthy를 쓰면 DB가 pg_isready를 통과할 때까지 앱 시작을 지연시킬 수 있습니다. 이 ConceptBlock에서는 HEALTHCHECK 상태 머신의 동작 원리와 depends_on과의 연동 방법을 다룹니다.

depends_on 연동 흐름

HEALTHCHECK 상태 머신

컨테이너 시작 후 헬스체크 상태는 세 단계를 거칩니다.

컨테이너 시작
     ↓
 starting       ← --start-period 동안 (헬스체크 실패해도 unhealthy로 안 감)
     ↓
 healthy        ← 헬스체크 성공
     또는
 unhealthy      ← --retries 횟수 연속 실패 시
로컬 터미널
# 컨테이너 상태 확인 (STATUS 컬럼)
docker ps
# NAME      STATUS
# api       Up 2 min (healthy)    ← 정상
# db        Up 30s (starting)     ← 초기화 중
# worker    Up 5 min (unhealthy)  ← 문제 발생!

Dockerfile HEALTHCHECK 완전 문법

Dockerfile
HEALTHCHECK [OPTIONS] CMD <command>

# 옵션:
# --interval=DURATION   헬스체크 실행 간격 (기본: 30s)
# --timeout=DURATION    타임아웃 (기본: 30s)
# --start-period=DURATION  초기화 유예 기간 (기본: 0s)
# --retries=N           연속 실패 횟수 → unhealthy (기본: 3)

# HTTP 서버 헬스체크
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

# TCP 포트 확인 (nc 사용)
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
  CMD nc -z localhost 5432 || exit 1

# 커스텀 스크립트
HEALTHCHECK --interval=30s CMD /app/healthcheck.sh

# 헬스체크 비활성화 (상위 이미지의 HEALTHCHECK 제거)
HEALTHCHECK NONE

depends_on + service_healthy 패턴

YAML
services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3

  app:
    image: myapp
    depends_on:
      db:
        condition: service_healthy     # DB가 healthy 될 때까지 대기
      redis:
        condition: service_healthy     # Redis도 healthy 될 때까지 대기
    # depends_on 없이 그냥 실행하면:
    # DB 컨테이너가 시작됐어도 PostgreSQL이 아직 초기화 중
    # → 앱이 DB 연결 실패로 크래시 → restart → 연결 성공
    # (retry 루프로 해결 가능하지만, service_healthy가 더 깔끔)

헬스체크 이력 확인

로컬 터미널
# 최근 헬스체크 결과 확인
docker inspect my-container --format \
  '{{range .State.Health.Log}}{{.Start}} Exit:{{.ExitCode}} {{.Output}}{{end}}'

# Python으로 보기 좋게 출력
docker inspect my-container | python3 -c "
import sys, json
data = json.load(sys.stdin)
health = data[0]['State']['Health']
print('Status:', health['Status'])
print('--- Last 3 checks ---')
for h in health['Log'][-3:]:
    print(h['Start'][:19], 'ExitCode:', h['ExitCode'])
    if h['Output']:
        print(' ', h['Output'][:100])
"

💡개념

재시작 정책 4종 — 운영 상황별 올바른 선택

restart: always를 설정했는데 배포 중 점검을 위해 docker stop으로 컨테이너를 내렸습니다. 서버를 재부팅했더니 컨테이너가 다시 올라와 있습니다. 유지보수 의도가 무시된 것입니다. unless-stopped는 수동 stop 상태를 기억해서 재부팅 후에도 내린 채로 둡니다. 반대로 배치 작업은 정상 완료(exit 0) 후 재시작이 불필요하고, 오류(exit 1) 시에만 재시도해야 합니다. 이 ConceptBlock에서는 4가지 재시작 정책의 실제 동작 차이와 상황별 선택 가이드를 다룹니다.

재시작 정책 결정 기준

4가지 재시작 정책 비교

Docker
# no (기본값): 재시작 안 함
docker run --restart no myapp

# always: 항상 재시작 (수동 docker stop 후 Docker 재시작해도 시작됨)
docker run --restart always myapp

# unless-stopped: 수동으로 stop한 경우만 제외하고 재시작 (권장)
docker run --restart unless-stopped myapp

# on-failure: 오류 종료(exit code ≠ 0)일 때만 재시작
docker run --restart on-failure myapp

# on-failure:N: 최대 N번 시도 후 포기
docker run --restart on-failure:3 myapp

always vs unless-stopped — 실무에서 중요한 차이

가장 많이 혼동하는 부분입니다.

시나리오: 유지보수를 위해 docker stop myapp 실행 후
          서버를 재부팅했을 때 어떻게 되는가?

always:          재부팅 후 컨테이너 자동 시작됨 ← 의도치 않은 재시작!
unless-stopped:  재부팅 후도 중지 상태 유지됨 ✓ (stop한 의도를 기억)
Docker
# 예시
docker run -d --name myapp --restart always myapp
docker stop myapp          # 유지보수를 위해 중지
sudo reboot                # 서버 재부팅
# 재부팅 후: myapp 컨테이너가 자동 시작됨 (always)

docker run -d --name myapp --restart unless-stopped myapp
docker stop myapp          # 유지보수를 위해 중지
sudo reboot                # 서버 재부팅
# 재부팅 후: myapp 컨테이너 중지 상태 유지 (unless-stopped) ✓

권장 사항: 대부분의 운영 서비스는 unless-stopped 사용

정책 선택 가이드

상황권장 정책
개발 중, 테스트 실행no (기본값)
일반 운영 서비스 (웹 서버, API 등)unless-stopped
절대 중단되면 안 되는 인프라 (Docker 재시작 포함)always
배치 작업, 오류 시만 재시도 필요on-failure:3
실행 완료 후 종료가 정상인 작업no

재시작 횟수와 딜레이

Docker는 재시작이 반복될수록 딜레이를 늘립니다 (exponential backoff).

로컬 터미널
# 재시작 횟수 확인
docker inspect myapp --format '{{.RestartCount}}'
# 5 → 5번 재시작됨

# 현재 재시작 정책 확인
docker inspect myapp --format '{{.HostConfig.RestartPolicy}}'
# {unless-stopped 0}

# 실행 중인 컨테이너 재시작 정책 변경 (재시작 필요 없음)
docker update --restart unless-stopped myapp

재시작 딜레이: 100ms → 200ms → 400ms → 800ms → ... (최대 1분) on-failure:3 설정 시 3번 모두 실패하면 더 이상 재시작하지 않습니다.


핵심 요약

개념명령/설정설명
헬스체크 주기--interval=30s헬스체크 실행 간격 (기본 30초)
응답 대기 시간--timeout=10s명령어 응답 대기 시간 초과 시 실패 처리
연속 실패 허용--retries=3unhealthy 판정 전 허용 실패 횟수
시작 유예 기간--start-period=60s이 기간 동안 실패는 retries에 미포함
컨테이너 상태starting / healthy / unhealthyHEALTHCHECK 결과에 따른 3가지 상태
항상 재시작restart: always종료 코드 무관, 데몬 재시작 시에도 적용
수동 중지 존중restart: unless-stoppeddocker stop 후 데몬 재시작 시 비적용
실패 시만 재시작restart: on-failure:3비정상 종료 시 최대 3회 재시작
서비스 준비 대기condition: service_healthydepends_on과 조합, healthy 상태 확인 후 시작
PostgreSQL 헬스체크pg_isready -U user -d dbPostgreSQL 전용 연결 준비 확인 도구
Redis 헬스체크redis-cli pingPONG 응답 확인
HTTP 헬스체크curl -f URL 또는 wget --spider URLHTTP 200 응답 확인

지식 확인

퀴즈 — 5문제

Q1

Dockerfile의 HEALTHCHECK 지시어에서 `--start-period` 옵션의 역할은 무엇인가요?

Q2

컨테이너가 `unhealthy` 상태가 되는 조건은 무엇인가요?

Q3

docker-compose에서 `restart: on-failure:3`이 의미하는 바는 무엇인가요?

Q4

Docker Compose에서 `depends_on`에 `condition: service_healthy`를 사용하려면 의존 서비스에 무엇이 반드시 정의되어 있어야 하나요?

Q5

`restart: always`와 `restart: unless-stopped`의 차이점은 무엇인가요?

0 / 5 답변

🧪 실습으로 확인하기

Docker Compose 멀티 서비스 구성

초급

docker-compose.yml로 nginx + 앱 컨테이너를 함께 정의하고, 서비스 간 통신과 볼륨 마운트를 구성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

docker고급 · 60
[Docker] 루트 권한 탈피 및 최소 권한 원칙을 적용한 이미지 보안
Docker 트랙 계속
networking입문 · 45
[Network] OSI 7계층과 TCP/IP 4계층 모델 실무적 관점 분석
Networking 트랙 시작점