테마 전환

Docker Compose 프로덕션 배포 3요소: 헬스체크, 재시작 정책, 리소스 제한

Easton editorial illustration: input-process-output transport line

서버 알림 문자가 사람을 침대에서 벌떡 일어나게 만들었습니다.

컴퓨터를 열어 보니 컨테이너 상태는 running이었고, 초록색 작은 표시도 꽤 건강해 보였습니다. 그런데 서비스를 접속하면? 502 Bad Gateway.

데이터베이스 컨테이너가 아직 시작을 마치지 않았는데 애플리케이션 컨테이너가 성급하게 연결을 시도한 겁니다. 연결에 실패했고 서비스는 죽었습니다. 컨테이너는 여전히 “실행 중”이었지만 서비스는 이미 멈춘 뒤였습니다.

이것이 제가 처음 Docker Compose를 프로덕션 환경에 배포했을 때 겪은 실제 경험입니다.

돌아가는 것과 안정적으로 돌아가는 것은 완전히 다른 문제입니다. docker-compose up으로 한 번에 시작할 수 있다고 해서, 한밤중에 메모리가 터지거나 프로세스가 충돌할 때도 버틴다는 뜻은 아닙니다.

Docker Compose 프로덕션 배포 3요소인 헬스체크, 재시작 정책, 리소스 제한은 바로 이 구멍을 메우기 위한 설정입니다. 이 글에서는 시행착오를 거치며 정리한 설정 방법과 바로 복사해 쓸 수 있는 YAML 템플릿을 공유합니다. 컨테이너를 “일단 실행됨”에서 “안정적으로 실행됨”으로 바꾸는 데 도움이 될 것입니다.

1. 헬스체크(healthcheck): 컨테이너가 실제로 사용 가능한지 판단하기

docker ps에 표시되는 running 상태는 컨테이너 프로세스가 아직 살아 있다는 뜻일 뿐입니다. 정상적으로 서비스를 제공할 수 있는지는 알려주지 않습니다.

헬스체크는 Docker가 컨테이너를 주기적으로 “검진”하게 하는 설정입니다. HTTP 요청을 보내거나, 데이터베이스에 연결하거나, 스크립트를 실행해 서비스가 정말 살아 있는지 확인합니다.

헬스체크는 어떻게 동작하나요?

Docker는 사용자가 설정한 간격에 맞춰 컨테이너 안으로 검사 명령을 보냅니다. 명령이 0을 반환하면 healthy, 0이 아니면 unhealthy입니다. 몇 번 연속 실패하면 컨테이너가 unhealthy로 표시됩니다.

중요한 점은 헬스체크 실패가 자동 재시작을 직접 트리거하지 않는다는 것입니다. 헬스체크는 상태를 드러내며 “이 서비스에 문제가 있다”고 알려줄 뿐입니다. 자동 복구까지 원한다면 depends_on의 조건부 시작과 재시작 정책을 함께 써야 합니다.

꼭 이해해야 할 네 가지 매개변수

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
  interval: 30s      # 얼마마다 검사할지
  timeout: 10s       # 단일 검사 제한 시간
  retries: 3         # 몇 번 연속 실패하면 unhealthy로 표시할지
  start_period: 60s  # 시작 유예 시간, 이 시간의 실패는 retries에 포함하지 않음

저는 한때 start_period 매개변수를 무시한 적이 있습니다. 결과는 이랬습니다. 애플리케이션 시작이 느리고 데이터베이스 연결에 40초가 필요한데, 헬스체크는 10초 만에 시작됩니다. 3번 연속 실패하자 곧바로 unhealthy로 표시됐습니다. start_period: 60s를 추가한 뒤에는 애플리케이션이 초기화할 시간을 충분히 확보할 수 있었습니다.

자주 쓰는 헬스체크 명령

Web 서비스:

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

PostgreSQL:

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

Redis:

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

depends_on과 함께 의존 서비스 시작 제어하기

이 부분이 헬스체크의 가장 실용적인 쓰임새입니다. 의존 서비스가 실제로 준비된 뒤에 서비스를 시작하게 만들 수 있습니다.

services:
  app:
    depends_on:
      db:
        condition: service_healthy  # 데이터베이스 헬스체크 통과를 기다림
      redis:
        condition: service_healthy  # Redis 헬스체크 통과를 기다림

예전에는 depends_on: [db, redis]를 사용했습니다. 그 결과 애플리케이션 컨테이너는 시작됐지만 데이터베이스는 아직 초기화 중이었습니다. 연결할 수 없으니 바로 오류를 내고 종료됐습니다. condition: service_healthy로 바꾼 뒤에는 애플리케이션이 데이터베이스가 pg_isready에 응답할 때까지 얌전히 기다린 다음 시작합니다. 훨씬 조용해졌습니다.

2. 재시작 정책(restart): 컨테이너에 자가 복구 능력 주기

컨테이너가 죽으면 누가 다시 일으켜 세울까요?

수동으로 docker restart를 실행할까요? 새벽 3시에 알림이 울릴 때 한번 해보면 생각이 달라질 겁니다.

재시작 정책은 Docker 데몬이 이 일을 대신하게 하는 설정입니다. 컨테이너가 종료되면 Docker가 자동으로 재시작할지 판단합니다.

네 가지 정책 비교

정책동작적합한 상황
no죽으면 그대로 둠, 재시작하지 않음임시 테스트, CI/CD
always어떤 방식으로 종료되든 재시작핵심 서비스
on-failure비정상 종료일 때만 재시작작업형 컨테이너
unless-stopped수동으로 중지하지 않는 한 항상 재시작프로덕션 우선 선택

프로덕션 우선 선택: unless-stopped

restart: unless-stopped

왜 always가 아니라 unless-stopped를 추천할까요?

차이는 수동 docker stop 이후의 동작에 있습니다.

  • always: 수동으로 중지해도 시스템이나 Docker 서비스가 재시작되면 컨테이너가 다시 자동 시작됩니다
  • unless-stopped: 수동으로 중지하면 정말로 중지 상태를 유지하며 스스로 살아나지 않습니다

컨테이너 하나를 유지보수하려고 수동으로 멈췄는데, 서버 재시작 후 컨테이너가 혼자 다시 올라왔다고 상상해 보세요. 그때 떠오르는 말은 대체로 좋지 않습니다.

on-failure의 재시도 제한

on-failure에는 재시도 횟수를 설정할 수 있습니다.

restart: on-failure:5  # 최대 5회 재시작

컨테이너가 5번 연속 시작에 실패하면 Docker는 포기합니다. 데이터베이스 연결 실패나 설정 오류처럼 외부 원인으로 반복 충돌할 수 있는 상황에 적합합니다. 무한 재시작 루프를 막아줍니다.

어떻게 고르면 될까요? 간단히 정리하면

  • 핵심 서비스(Web, API, 데이터베이스): unless-stopped
  • 백그라운드 작업, 예약 스크립트: on-failure
  • 개발 디버깅, 임시 실행: no

주의할 점: 재시작 정책은 “컨테이너가 종료된 뒤 재시작할지”만 관리합니다. “서비스가 정말 사용 가능한지”는 보지 않습니다. 둘을 연결하려면 헬스체크가 필요합니다.

3. 리소스 제한(deploy.resources): 컨테이너 폭주 막기

이런 상황을 겪어본 적이 있나요? 한 컨테이너에서 메모리 누수가 발생해 서버의 모든 메모리를 먹어치우고, 다른 컨테이너까지 전부 OOM Killer에 의해 죽는 상황입니다.

저는 겪어봤습니다. 꽤 쓰라린 경험이었습니다.

리소스 제한은 각 컨테이너에 “천장”을 만들어 주는 설정입니다. 이 한도를 넘으면 해당 컨테이너를 종료해 다른 서비스를 보호합니다.

limits vs reservations

deploy:
  resources:
    limits:
      cpus: '1.0'      # 최대 CPU 1개 사용
      memory: 512M     # 최대 512MB 메모리 사용
    reservations:
      cpus: '0.5'      # 최소 CPU 0.5개 예약
      memory: 256M     # 최소 256MB 메모리 예약
  • limits: 하드 제한, 넘으면 프로세스를 종료합니다(OOM)
  • reservations: 소프트 제한, 스케줄러에 “이 컨테이너는 최소 이만큼의 리소스가 필요하다”고 알려줍니다

직설적으로 말하면 limits는 “넘으면 안 되는 값”이고, reservations는 “최소한 보장할 값”입니다.

CPU 제한은 어떻게 설정하나요?

cpus: '1.0'   # CPU 코어 1개를 최대치로 사용
cpus: '0.5'   # CPU 50%까지 사용
cpus: '2.0'   # 코어 2개까지 사용

CPU 제한은 소프트 제한입니다. 컨테이너가 초과하면 속도가 제한될 뿐 종료되지는 않습니다. 그래서 조금 넉넉하게 잡는 편이 낫습니다.

메모리 제한은 어떻게 설정하나요?

memory: 512M    # 512MB
memory: 2G      # 2GB

메모리 제한은 하드 제한입니다. 초과하면 컨테이너가 곧바로 OOM Killer에 의해 종료됩니다. 협상은 없습니다.

제 경험상 기준값:

  • Node.js 애플리케이션: 최소 512M, 프로덕션 환경에서는 1G 권장
  • Python 애플리케이션: 256M - 512M
  • PostgreSQL: 연결 수와 데이터 규모에 따라 1G - 4G
  • Redis: 256M - 512M, 캐시로 사용한다면 더 필요할 수 있음

실전 설정 예시

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.25'
          memory: 256M

이렇게 설정하면 애플리케이션은 CPU 1개와 메모리 1GB까지만 사용할 수 있고, Docker는 최소 CPU 0.25개와 메모리 256MB를 보장합니다.

주의: deploy 설정은 주로 Docker Swarm에서 사용됩니다. 단일 서버 배포에서 docker-compose up을 사용할 때도 리소스 제한은 동작하지만 Docker Compose V2가 필요하거나 docker-compose --compatibility 매개변수를 사용해야 합니다. 더 범용적인 단일 서버 방식은 mem_limit와 cpus(폐기됨)를 쓰거나 deploy를 직접 사용하는 것입니다(Compose V2.20+ 지원).

4. 완성형 설정 템플릿: 바로 복사해 쓸 수 있는 프로덕션급 YAML

세 요소는 각각 따로 써도 유용하지만, 함께 조합할 때 프로덕션급 설정이 됩니다. 아래는 Web 서비스 + PostgreSQL + Redis의 완성형 예시입니다. 바로 복사한 뒤 상황에 맞게 고쳐 쓸 수 있습니다.

전체 예시

version: '3.8'

services:
  # Web 애플리케이션
  app:
    image: myapp:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.25'
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"   # 단일 로그 파일 최대 10MB
        max-file: "3"     # 로그 파일 최대 3개 보관

  # PostgreSQL 데이터베이스
  db:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppassword
      POSTGRES_DB: appdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M
    volumes:
      - pgdata:/var/lib/postgresql/data

  # Redis 캐시
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          memory: 128M

volumes:
  pgdata:

설정 분리 팁

개발 환경과 프로덕션 환경의 설정은 보통 다릅니다. 두 파일로 나눠 관리하세요.

# compose.yaml - 개발 환경
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    restart: "no"  # 개발 중에는 자동 재시작을 원하지 않음
# compose.production.yaml - 프로덕션 환경 오버라이드
version: '3.8'
services:
  app:
    image: myapp:v1.2.3  # 프로덕션에서는 빌드된 이미지를 사용
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 1G

프로덕션 환경 시작:

docker-compose -f compose.yaml -f compose.production.yaml up -d

두 파일은 병합되며, compose.production.yaml의 설정이 compose.yaml의 설정을 덮어씁니다.

로그 관리: 디스크가 가득 차는 문제 방지

Docker의 기본 로그 드라이버는 json-file이고, 로그는 제한 없이 커질 수 있습니다. 제한을 걸지 않으면 몇 달 뒤 디스크가 로그로 가득 찰 수 있습니다.

logging:
  driver: json-file
  options:
    max-size: "10m"   # 단일 파일 최대 10MB
    max-file: "3"     # 최대 3개 파일, 총 30MB

컨테이너마다 최대 30MB 로그만 남기고 자동으로 로테이션합니다. 저는 이 설정을 모든 서비스에 넣어 둡니다. 나중에 직접 로그를 정리하러 들어가는 일을 줄일 수 있습니다.

정리

여기까지 길게 이야기했지만, 3요소의 핵심 흐름은 다음과 같습니다.

헬스체크가 문제를 발견 → 재시작 정책이 자동 복구 → 리소스 제한이 장애 확산을 차단

이 조합을 적용하면 Docker Compose 애플리케이션은 다음을 할 수 있습니다.

  1. 시작 시 의존 서비스가 실제로 준비될 때까지 기다리고, 무작정 달려들지 않습니다
  2. 충돌 후 스스로 다시 올라오므로 한밤중에 일어날 필요가 줄어듭니다
  3. 컨테이너 하나가 폭주해도 서버 전체를 끌어내리지 않습니다

이제 당신의 docker-compose.yml을 확인해 보세요. 어떤 설정이 빠져 있나요? 채워 넣으세요.

아직 Docker Compose로 프로덕션 환경을 배포해 본 적이 없다면, 다음 배포 때 이 세 가지 설정을 함께 가져가세요. 나중의 자신이 고마워할 것입니다.

Docker Compose 프로덕션 배포 3요소 설정하기

Docker Compose에 헬스체크, 재시작 정책, 리소스 제한을 추가해 프로덕션 수준의 안정적인 배포를 구성합니다

⏱️ Estimated time: 15 min

  1. 1

    Step 1: 헬스체크 설정 추가

    각 서비스에 healthcheck 설정을 추가합니다:

    • test: 검사 명령(curl, pg_isready, redis-cli ping 등)
    • interval: 검사 간격(10-30s 권장)
    • timeout: 제한 시간(5-10s 권장)
    • retries: 실패 횟수(3-5회 권장)
    • start_period: 시작 유예 시간(애플리케이션 시작 시간에 따라 30-60s 설정)
  2. 2

    Step 2: 의존 서비스 조건부 시작 설정

    depends_on의 condition 매개변수를 사용합니다:

    • depends_on: [db]를 depends_on: db: condition: service_healthy로 변경
    • 의존 서비스의 헬스체크가 설정되어 있는지 확인
    • 애플리케이션 서비스는 의존 서비스가 실제로 사용 가능해진 뒤 시작됩니다
  3. 3

    Step 3: 재시작 정책 설정

    서비스 유형에 따라 재시작 정책을 선택합니다:

    • 핵심 서비스(Web/API/데이터베이스): restart: unless-stopped
    • 백그라운드 작업/예약 스크립트: restart: on-failure:5
    • 개발 디버깅: restart: "no"
    • always 사용은 피합니다(수동 중지 후 예기치 않게 다시 시작될 수 있음)
  4. 4

    Step 4: 리소스 제한 설정

    deploy.resources에서 limits와 reservations를 설정합니다:

    • limits: 하드 제한, 초과하면 OOM Killer에 의해 종료됩니다
    • reservations: 소프트 제한, Docker가 보장하는 최소 리소스입니다
    • Node.js 애플리케이션: limits.memory는 최소 512M 권장
    • 데이터베이스는 연결 수와 데이터 규모에 따라 1G-4G로 설정
  5. 5

    Step 5: 로그 로테이션 설정 추가

    로그 파일이 디스크를 가득 채우지 않도록 막습니다:

    • logging.driver: json-file(기본 드라이버)
    • logging.options.max-size: "10m"(단일 파일 최대 10MB)
    • logging.options.max-file: "3"(파일 3개 보관)
    • 전체 로그 상한은 30MB이며 자동으로 로테이션됩니다

FAQ

헬스체크가 실패하면 컨테이너가 자동으로 재시작되나요?
아닙니다. 헬스체크는 컨테이너를 unhealthy로 표시할 뿐 자동 재시작을 직접 트리거하지 않습니다. unless-stopped 같은 재시작 정책과 depends_on의 service_healthy 조건을 함께 사용해야 합니다. 컨테이너 프로세스가 충돌해 종료될 때 재시작 정책이 개입합니다.
unless-stopped와 always는 무엇이 다른가요?
핵심 차이는 수동 중지 후 동작입니다. always 정책에서는 docker stop을 수동으로 실행한 뒤에도 서버나 Docker 서비스가 재시작되면 컨테이너가 자동으로 시작됩니다. unless-stopped 정책에서는 수동으로 중지한 컨테이너가 그대로 중지 상태를 유지하며 스스로 살아나지 않습니다. 프로덕션 환경에서는 unless-stopped를 권장합니다.
리소스 제한에서 limits와 reservations는 무엇이 다른가요?
limits는 하드 제한입니다. 컨테이너의 메모리 사용량이 limits.memory를 넘으면 OOM Killer에 의해 종료됩니다. reservations는 소프트 제한으로, Docker 스케줄러에 이 컨테이너가 최소한 이만큼의 리소스를 필요로 한다고 알려줍니다. CPU 제한은 소프트 제한이고 초과 시 속도가 제한되며, 메모리 제한은 하드 제한이고 초과 시 종료됩니다.
start_period 매개변수는 어떤 역할을 하나요?
start_period는 시작 유예 시간입니다. 이 시간 안에 발생한 헬스체크 실패는 retries에 포함되지 않습니다. 시작이 느린 애플리케이션, 예를 들어 데이터베이스 연결에 40초가 필요한 서비스라면 start_period: 60s를 설정해 막 시작한 애플리케이션이 unhealthy로 오판되는 일을 피할 수 있습니다.
개발 환경과 프로덕션 환경에서 서로 다른 설정을 어떻게 사용하나요?
두 설정 파일로 분리합니다:

• compose.yaml: 개발 환경 설정(build: ., restart: "no")
• compose.production.yaml: 프로덕션 환경 오버라이드(image: xxx, restart: unless-stopped)
• 시작 명령: docker-compose -f compose.yaml -f compose.production.yaml up -d
• 뒤의 파일이 앞의 파일 설정을 덮어써 설정을 분리할 수 있습니다
단일 서버 배포에서도 deploy.resources를 사용할 수 있나요?
사용할 수 있습니다. deploy 설정은 주로 Docker Swarm용이지만 Docker Compose V2.20+는 단일 서버 사용도 지원합니다. 오래된 버전을 사용한다면 --compatibility 매개변수를 추가하거나, 이미 폐기된 mem_limit/cpus 매개변수를 사용해야 합니다. 최신 Docker Compose로 업그레이드하는 것을 권장합니다.

2분 읽기 · 게시일: 2026년 4월 24일 · 수정일: 2026년 10월 21일

댓글

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

Easton BlogEaston Blog