테마 전환

Docker Compose 프로덕션 배포: 상태 확인, 재시작 정책, 로그 관리

Easton editorial illustration: observability control panel

서버 경고 문자가 휴대전화에 연달아 뜹니다. 터미널을 열어 보니 디스크 사용량이 99%입니다. 컨테이너 로그가 50GB를 차지하고 있었습니다.

이게 최악의 상황도 아닙니다. 작년 한 프로젝트에서는 API 컨테이너 상태가 “running”으로 표시됐지만 데이터베이스 연결은 이미 끊겨 모든 요청이 500을 반환했습니다. 문제를 찾는 데 꼬박 세 시간이 걸렸습니다. Last9의 연구 데이터에 따르면 이처럼 컨테이너가 겉으로만 실행되는 문제를 조사하는 데 한 번당 평균 3.2시간이 낭비됩니다.

많은 팀이 처음 프로덕션 환경에 Docker Compose를 배포할 때 포트 매핑과 볼륨 마운트만 간단히 설정한 채 컨테이너를 운영 환경에 올립니다. 상태 확인도, 로그 로테이션도 설정하지 않고 재시작 정책에는 별생각 없이 restart: always를 씁니다. 그 결과 컨테이너는 실행 중인 것처럼 보이지만 실제로는 이미 멈춰 있고, 로그 파일은 걷잡을 수 없이 커져 디스크를 가득 채우며, 충돌한 서비스가 무한히 재시작해 CPU와 메모리를 모두 소모합니다.

이 글에서는 프로덕션 환경의 세 가지 핵심 구성인 상태 확인, 재시작 정책, 로그 관리를 하나씩 자세히 설명합니다. 설정 예시뿐 아니라 자주 사용하는 서비스의 상태 확인 명령, 장애 진단 절차, 바로 복사해 사용할 수 있는 완전한 docker-compose.yml 템플릿도 제공합니다.

상태 확인 — 컨테이너가 실제로 살아 있게 만들기

컨테이너 상태가 running이라고 해서 애플리케이션이 실제로 작동한다는 뜻은 아닙니다. 데이터베이스에 연결할 수 없거나, 포트가 열려 있지 않거나, 프로세스가 멈춘 상황을 Docker가 저절로 알아내지는 못합니다. 상태 확인은 컨테이너에 일종의 “심박수 모니터”를 달아 애플리케이션이 정상적으로 응답하는지 주기적으로 검사하는 기능입니다.

설정 문법

docker-compose.yml의 healthcheck 설정은 다음과 같습니다.

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 10s      # 10초마다 한 번 검사
  timeout: 5s        # 각 검사는 최대 5초 대기
  retries: 5         # 5회 연속 실패해야 unhealthy로 표시
  start_period: 30s  # 컨테이너 시작 후 30초의 준비 시간 부여

이 매개변수들은 서로 알맞게 조합해야 합니다. timeoutinterval보다 길면 안 됩니다. 그렇지 않으면 검사가 끝나기 전에 다음 검사가 시작됩니다. start_period도 빼면 안 됩니다. 데이터베이스와 같은 서비스는 시작이 느리므로 준비 시간이 너무 짧으면 상태 확인이 컨테이너 장애로 잘못 판단할 수 있습니다.

자주 사용하는 서비스의 상태 확인 명령

서비스마다 검사 방법이 다릅니다. 자주 사용하는 몇 가지 예시는 다음과 같습니다.

PostgreSQL

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d mydb"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

pg_isready는 PostgreSQL에 내장된 검사 도구로, 데이터베이스가 연결을 받을 준비가 되었는지 확인하는 데 사용합니다.

MySQL / MariaDB

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

비밀번호에는 $$ 이스케이프를 사용해야 합니다. 그렇지 않으면 YAML이 $를 변수 참조로 처리합니다.

Redis

healthcheck:
  test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
  interval: 10s
  timeout: 3s
  retries: 3

Redis의 ping 명령은 PONG을 반환합니다. grep으로 필터링해 결과가 올바른지 확인합니다.

Web Server(HTTP 검사)

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

-f 옵션을 사용하면 HTTP 상태 코드가 2xx가 아닐 때 curl이 0이 아닌 종료 코드를 반환하므로 상태 확인이 실패합니다.

주의할 점: Alpine 같은 경량 이미지에는 curl이 없을 수 있습니다. curl을 설치하거나(apk add curl) wget으로 바꾸면 됩니다.

test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"]

시작 순서 제어

데이터베이스가 준비되기 전에 API 컨테이너가 시작되어 연결 실패, 오류, 충돌로 이어지는 상황을 저는 여러 번 보았습니다. depends_oncondition: service_healthy를 함께 사용하면 해결할 수 있습니다.

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

  api:
    build: ./api
    depends_on:
      postgres:
        condition: service_healthy  # postgres 상태 확인 통과 후 시작

이렇게 하면 Docker Compose는 postgres의 상태 확인이 healthy를 반환할 때까지 기다린 뒤 api 컨테이너를 시작합니다. 더 이상 “데이터베이스가 준비되기 전에 API가 연결하는” 난처한 상황이 생기지 않습니다.

재시작 정책 — 실패 후 안정적으로 복구하기

컨테이너가 충돌하면 어떻게 해야 할까요? 자동 재시작은 좋은 방법처럼 보입니다. 하지만 충돌의 근본 원인이 해결되지 않았다면 재시작은 무한 반복될 뿐입니다. CPU와 메모리를 낭비하고 실제 장애까지 가립니다.

설정 문법

재시작 정책은 deploy 블록에서 설정합니다.

deploy:
  restart_policy:
    condition: on-failure   # 실패한 경우에만 재시작
    delay: 5s               # 재시작 전 5초 대기
    max_attempts: 3         # 최대 3회 재시작 시도
    window: 120s            # 120초 동안 정상 실행되어야 실제 복구로 간주

condition에는 세 가지 옵션이 있습니다.

  • none: 재시작하지 않으며 컨테이너가 중단된 상태로 둡니다.
  • on-failure: 컨테이너가 비정상적으로 종료될 때(종료 코드가 0이 아닐 때)만 재시작합니다.
  • any: 상황과 관계없이 항상 재시작합니다.

프로덕션 환경 권장 사항

프로덕션 환경에서는 always가 아니라 on-failure를 사용하는 것이 좋습니다.

이유는 무엇일까요? restart: always는 상황을 가리지 않고 컨테이너를 다시 시작합니다. 애플리케이션 코드 버그로 충돌해도 재시작하고, 데이터베이스 연결 실패로 프로세스가 종료되어도 재시작하며, 설정 파일 오류로 시작에 실패해도 또 재시작합니다. 결국 충돌 루프가 발생해 로그가 쉴 새 없이 쌓이고 CPU가 반복해서 소모됩니다.

on-failuremax_attempts를 더하면 다릅니다. 최대 세 번만 재시작하고 계속 실패하면 중지합니다. 운영 담당자는 컨테이너가 결국 중단되었다는 사실을 확인하고 실제 원인을 조사할 수 있습니다.

매개변수 조정

delay는 재시작 간격입니다. 너무 짧으면 컨테이너가 완전히 정리되기도 전에 다시 시작될 수 있고, 너무 길면 복구 시간이 늘어납니다. 일반적으로 5~10초가 적절합니다.

window는 놓치기 쉬운 매개변수입니다. 재시작 후 얼마 동안 다시 실패하지 않아야 재시작 성공으로 간주할지를 정의합니다. 예를 들어 window: 120s로 설정하면 컨테이너가 재시작된 뒤 120초 안에 다시 중단될 경우 max_attempts 카운트가 초기화되지 않습니다. 이렇게 하면 “재시작에 성공한 지 1초 만에 다시 충돌하는” 상황을 성공으로 잘못 판단하지 않습니다.

상태 확인과 재시작 정책의 조합

상태 확인과 재시작 정책은 서로 독립적으로 작동하는 것이 아니라 함께 연동됩니다.

  1. 상태 확인이 retries 횟수만큼 연속 실패 → 컨테이너가 unhealthy로 표시됨
  2. restart_policy가 설정되어 있으면 Docker가 컨테이너 재시작을 시도
  3. 재시작 후 상태 확인 카운트가 처음부터 다시 시작
  4. 재시작 후 상태 확인이 통과하면 컨테이너가 정상 복구되고, 계속 실패하면 max_attempts를 모두 사용할 때까지 재시작을 반복

이 흐름은 장애에 “자동 복구” 능력을 부여하면서 무한 재시작의 위험도 제한합니다.

로그 관리 — 디스크 용량 고갈 방지

앞에서 언급한 새벽 3시의 경고, 즉 디스크 사용량이 99%에 달하고 로그가 50GB를 차지한 상황을 저는 한 번만 겪은 것이 아닙니다. Docker의 기본 로그 드라이버인 json-file은 이전 로그를 자동으로 정리하지 않으므로 파일 크기가 끝없이 늘어납니다. 로그 로테이션을 설정하지 않으면 언젠가는 디스크를 가득 채웁니다.

로그 로테이션 설정

docker-compose.yml에 logging 설정을 추가합니다.

logging:
  driver: "json-file"
  options:
    max-size: "10m"      # 로그 파일 하나의 최대 크기 10MB
    max-file: "3"        # 로그 파일 최대 3개 보관
    compress: "true"     # 이전 로그를 압축해 공간 절약

이렇게 설정하면 컨테이너 로그가 최대 30MB(10MB × 3)를 차지합니다. 10MB를 넘으면 Docker가 새 파일을 만들고, 파일이 3개를 넘으면 가장 오래된 파일을 삭제하거나 압축합니다.

로그 파일은 /var/lib/docker/containers/<container-id>/<container-id>-json.log에 저장됩니다. du 명령으로 실제 사용량을 확인할 수 있습니다.

du -sh /var/lib/docker/containers/*/*-json.log

드라이버 선택

Docker는 json-file, syslog, fluentd, journald, local 등 다양한 로그 드라이버를 지원합니다. 대부분의 환경에서는 json-file이나 local이면 충분합니다.

Docker 공식 문서에 따르면 local 드라이버는 json-file보다 효율적이며 로그 로테이션 기능이 내장되어 있어 max-size/max-file을 직접 설정할 필요가 없습니다. 로그 양이 많다면(예: 하루에 수십 GB) local을 고려할 수 있습니다.

logging:
  driver: "local"

다만 local 드라이버에는 단점이 있습니다. docker logs로 로그 내용을 바로 볼 수 없습니다. 호환하려면 설정에 mode: "non-blocking"을 추가해야 합니다.

중앙 집중식 로그 수집(선택 사항)

단일 서버 배포에는 json-file이나 local이면 충분합니다. 하지만 서버가 수십 대이고 컨테이너가 수백 개라면 여러 위치에 흩어진 로그를 관리하기 어렵습니다. 이때 중앙 집중식 로그 솔루션을 고려할 수 있습니다.

  • Fluentd: 소규모 클러스터에 적합한 경량 로그 수집 도구
  • ELK Stack(Elasticsearch + Logstash + Kibana): 기능은 강력하지만 배포 비용이 높음
  • Loki + Grafana: Prometheus 생태계와 잘 통합되는 클라우드 네이티브 솔루션

이러한 솔루션은 설정이 복잡하므로 이 글의 범위에서는 다루지 않습니다. Fluentd 설정 방식만 간단히 살펴보겠습니다.

logging:
  driver: "fluentd"
  options:
    fluentd-address: "localhost:24224"
    tag: "docker.{{.Name}}"

Fluentd는 로그를 지정된 주소로 전달합니다. 다른 서버에서 로그를 한곳에 모아 분석할 수 있습니다.

전체 설정 템플릿

상태 확인, 재시작 정책, 로그 관리를 조합하면 프로덕션 수준의 docker-compose.yml을 만들 수 있습니다. 아래는 PostgreSQL 데이터베이스, Redis 캐시, API 서비스를 포함한 전체 예시입니다.

version: '3.8'

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        compress: "true"

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://myuser:mypassword@postgres:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
        window: 120s
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"
        compress: "true"

volumes:
  postgres_data:

설정 핵심 설명

시작 순서: api 컨테이너의 depends_on은 postgres와 redis의 상태 확인이 모두 통과할 때까지 기다립니다. 데이터베이스와 캐시가 준비된 뒤 API를 시작하므로 시작 시점의 연결 오류를 방지합니다.

로그 크기 차이: postgres와 redis는 일반적으로 로그 양이 많지 않아 10MB × 3이면 충분합니다. API 서비스는 로그가 더 많을 수 있으므로 50MB × 5로 설정합니다. 일률적으로 적용하지 말고 실제 로그 양에 맞춰 조정하세요.

재시작 지연 차이: postgres는 시작이 느리고 재시작 후 복구에 시간이 필요하므로 delay를 5초로 설정합니다. API는 시작이 빠르지만 상태 확인에 여유를 주도록 delay를 10초로 설정합니다.

시작 준비 시간: postgres의 start_period: 30s는 데이터베이스 초기화에 충분한 시간을 줍니다. Redis는 원래 시작이 빠르므로 redis에는 start_period: 5s, 애플리케이션 시작에는 일반적으로 몇 초면 충분하므로 API에는 start_period: 10s를 사용합니다.

이 템플릿은 바로 복사해 사용할 수 있습니다. 환경 변수와 이미지만 자신의 환경에 맞게 바꾸면 됩니다. 프로젝트에 MongoDB나 MinIO 같은 다른 서비스가 있다면 같은 패턴으로 상태 확인, 재시작 정책, 로그 설정을 추가하세요.

자주 발생하는 문제와 진단 방법

설정을 마치고 배포하더라도 문제가 발생할 수 있습니다. 자주 마주치는 문제와 진단 절차를 살펴보겠습니다.

상태 확인이 계속 실패하는 경우

증상: 컨테이너 상태가 계속 unhealthy이지만 애플리케이션은 정상적으로 작동하는 것처럼 보입니다.

진단 절차:

  1. 먼저 상태 확인 명령에 사용하는 도구가 있는지 확인합니다.

    docker exec <container> which curl
    docker exec <container> which pg_isready

    Alpine 이미지에는 curl이 없는 경우가 많으므로 직접 설치하거나 wget으로 바꿔야 합니다.

  2. 상태 확인 명령을 직접 실행해 출력을 확인합니다.

    docker exec <container> curl -f http://localhost:8080/health

    오류가 반환된다면 상태 확인 엔드포인트 자체에 문제가 있을 수 있습니다.

  3. 상태 확인의 상세 상태를 조회합니다.

    docker inspect --format='{{json .State.Health}}' <container> | jq

    최근 여러 차례의 검사 결과, 실패 원인, 타임스탬프를 확인할 수 있습니다.

컨테이너가 반복해서 재시작되는 경우

증상: 컨테이너가 시작된 지 몇 초 만에 다시 중단되고 로그가 재시작 기록으로 가득합니다.

진단 절차:

  1. 컨테이너 종료 원인을 확인합니다.

    docker inspect --format='{{.State.ExitCode}}' <container>
    docker inspect --format='{{.State.Error}}' <container>

    종료 코드를 보면 대략적인 원인을 알 수 있습니다(1 = 일반 오류, 137 = OOM에 의해 종료, 139 = 세그멘테이션 오류).

  2. 재시작 횟수를 확인합니다.

    docker inspect --format='{{.RestartCount}}' <container>

    횟수가 많다면 max_attempts가 적용되었는지 확인합니다.

  3. 컨테이너 로그에서 구체적인 오류를 찾습니다.

    docker logs --tail 100 <container>

로그로 디스크가 가득 찬 경우

증상: 디스크 공간 경고가 발생하고 /var/lib/docker/containers 디렉터리가 많은 용량을 차지합니다.

진단 절차:

  1. 가장 큰 로그 파일을 찾습니다.

    du -sh /var/lib/docker/containers/*/*-json.log | sort -rh | head -5
  2. 로그 로테이션 설정이 적용되었는지 확인합니다.

    docker inspect --format='{{.HostConfig.LogConfig}}' <container>

    출력에 Config: {}가 표시되면 로그 로테이션이 설정되지 않은 것입니다.

  3. 로그를 직접 정리합니다(임시 방편).

    truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log

    이는 임시 방편일 뿐이며 장기적으로는 로그 로테이션 설정을 추가해야 합니다.

빠른 진단 명령 모음

문제가 생겼을 때 다음 명령을 사용하면 빠르게 원인을 찾을 수 있습니다.

# 모든 컨테이너의 상태 확인 결과 보기
docker ps --format "table {{.Names}}\t{{.Status}}"

# 특정 컨테이너의 상태 확인 이력 보기
docker inspect --format='{{json .State.Health}}' <container>

# 컨테이너 종료 코드와 재시작 횟수 보기
docker inspect --format='ExitCode: {{.State.ExitCode}}, RestartCount: {{.RestartCount}}' <container>

# 로그 파일 크기 보기
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh

# 컨테이너의 최근 로그 100줄 보기
docker logs --tail 100 <container>

마무리

프로덕션 환경에 Docker Compose를 배포할 때 이 세 가지 구성은 선택 사항이 아니라 필수입니다. 상태 확인은 컨테이너가 단순히 “살아 있는 것처럼 보이는” 데 그치지 않게 하고, 재시작 정책은 무한 반복을 제한하면서 장애를 자동으로 복구할 기회를 주며, 로그 관리는 디스크 용량 고갈을 방지합니다.

3.2시간
컨테이너 응답 중단 문제의 평균 조사 시간

핵심 설정 체크리스트:

  • 상태 확인: test + interval + timeout + retries + start_period
  • 재시작 정책: condition: on-failure + max_attempts: 3
  • 로그 로테이션: max-size: 10m + max-file: 3 + compress: true

3단계 실행 계획:

  1. 현재 사용 중인 docker-compose.yml에 이 세 가지 설정이 있는지 확인하세요. 없다면 적어도 상태 확인과 로그 로테이션을 추가합니다.
  2. 위의 전체 템플릿으로 테스트 서비스를 배포하고 상태 확인과 로그 로테이션이 제대로 작동하는지 관찰합니다.
  3. 진단 명령을 기록해 두세요. 다음번 새벽 3시에 경고를 받더라도 문제를 빠르게 찾을 수 있습니다.

컨테이너를 아무런 보호 없이 프로덕션에서 실행하지 마세요. 이 세 가지 “보호막”을 제대로 설정하면 문제가 생겨도 자동으로 복구하고, 신속하게 원인을 조사하며, 디스크 용량 고갈을 피할 수 있습니다.

FAQ

상태 확인의 interval과 timeout은 어떻게 설정하는 것이 적절한가요?
interval은 10~30초, timeout은 3~10초를 권장합니다. 핵심은 timeout이 interval보다 길어서는 안 된다는 점입니다. 그렇지 않으면 이전 검사가 끝나기도 전에 다음 검사가 시작됩니다. 데이터베이스 서비스에는 초기화 시간을 확보하도록 start_period를 비교적 길게(30~60초) 설정하는 것이 좋습니다.
재시작 정책은 restart: always와 on-failure 중 무엇이 더 좋은가요?
프로덕션 환경에서는 on-failure와 max_attempts 조합을 권장합니다. always는 설정 오류나 코드 버그를 포함해 어떤 상황에서도 재시작하므로 충돌 루프를 일으킬 수 있습니다. on-failure는 비정상 종료 시에만 재시작하며, max_attempts로 횟수를 제한하면 운영 담당자가 문제를 발견할 수 있습니다.
로그 파일 크기와 보관 개수는 어느 정도가 적절한가요?
일반적인 서비스에는 max-size: 10m + max-file: 3을 권장하며 총 30MB입니다. API처럼 로그 양이 많은 서비스는 50m × 5로 설정할 수 있습니다. 실제 로그 양에 맞춰 조정하고 compress: true를 활성화해 공간을 절약하는 것이 중요합니다.
컨테이너 상태 확인은 계속 실패하지만 애플리케이션은 정상적으로 실행될 때는 어떻게 해야 하나요?
먼저 상태 확인 명령에 사용하는 도구(예: curl, pg_isready)가 있는지 확인합니다. Alpine 이미지에는 도구가 없는 경우가 많습니다. 그런 다음 검사 명령을 직접 실행해 출력을 확인하고, 마지막으로 docker inspect에서 상태 확인 이력을 살펴 구체적인 실패 원인을 찾습니다.
depends_on의 condition: service_healthy는 어떤 역할을 하나요?
종속 서비스의 상태 확인이 통과한 후에만 현재 컨테이너를 시작하도록 합니다. 단순한 depends_on보다 안정적이며, 데이터베이스가 준비되기 전에 API가 연결을 시도해 시작에 실패하는 상황을 방지합니다. 이 기능을 사용하려면 종속 서비스에 healthcheck가 설정되어 있어야 합니다.
컨테이너 로그가 디스크 공간을 얼마나 차지하는지 빠르게 확인하려면 어떻게 하나요?
du -sh /var/lib/docker/containers/*/*-json.log | sort -rh 명령을 사용합니다. 특정 컨테이너의 로그가 유난히 크다면 해당 컨테이너의 로그 로테이션 설정이 적용되었는지 확인합니다.

3분 읽기 · 게시일: 2026년 4월 12일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog