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

서버 알림 문자가 사람을 침대에서 벌떡 일어나게 만들었습니다.
컴퓨터를 열어 보니 컨테이너 상태는 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 애플리케이션은 다음을 할 수 있습니다.
- 시작 시 의존 서비스가 실제로 준비될 때까지 기다리고, 무작정 달려들지 않습니다
- 충돌 후 스스로 다시 올라오므로 한밤중에 일어날 필요가 줄어듭니다
- 컨테이너 하나가 폭주해도 서버 전체를 끌어내리지 않습니다
이제 당신의 docker-compose.yml을 확인해 보세요. 어떤 설정이 빠져 있나요? 채워 넣으세요.
아직 Docker Compose로 프로덕션 환경을 배포해 본 적이 없다면, 다음 배포 때 이 세 가지 설정을 함께 가져가세요. 나중의 자신이 고마워할 것입니다.
Docker Compose 프로덕션 배포 3요소 설정하기
Docker Compose에 헬스체크, 재시작 정책, 리소스 제한을 추가해 프로덕션 수준의 안정적인 배포를 구성합니다
⏱️ Estimated time: 15 min
- 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
Step 2: 의존 서비스 조건부 시작 설정
depends_on의 condition 매개변수를 사용합니다:
• depends_on: [db]를 depends_on: db: condition: service_healthy로 변경
• 의존 서비스의 헬스체크가 설정되어 있는지 확인
• 애플리케이션 서비스는 의존 서비스가 실제로 사용 가능해진 뒤 시작됩니다 - 3
Step 3: 재시작 정책 설정
서비스 유형에 따라 재시작 정책을 선택합니다:
• 핵심 서비스(Web/API/데이터베이스): restart: unless-stopped
• 백그라운드 작업/예약 스크립트: restart: on-failure:5
• 개발 디버깅: restart: "no"
• always 사용은 피합니다(수동 중지 후 예기치 않게 다시 시작될 수 있음) - 4
Step 4: 리소스 제한 설정
deploy.resources에서 limits와 reservations를 설정합니다:
• limits: 하드 제한, 초과하면 OOM Killer에 의해 종료됩니다
• reservations: 소프트 제한, Docker가 보장하는 최소 리소스입니다
• Node.js 애플리케이션: limits.memory는 최소 512M 권장
• 데이터베이스는 연결 수와 데이터 규모에 따라 1G-4G로 설정 - 5
Step 5: 로그 로테이션 설정 추가
로그 파일이 디스크를 가득 채우지 않도록 막습니다:
• logging.driver: json-file(기본 드라이버)
• logging.options.max-size: "10m"(단일 파일 최대 10MB)
• logging.options.max-file: "3"(파일 3개 보관)
• 전체 로그 상한은 30MB이며 자동으로 로테이션됩니다
FAQ
헬스체크가 실패하면 컨테이너가 자동으로 재시작되나요?
unless-stopped와 always는 무엇이 다른가요?
리소스 제한에서 limits와 reservations는 무엇이 다른가요?
start_period 매개변수는 어떤 역할을 하나요?
개발 환경과 프로덕션 환경에서 서로 다른 설정을 어떻게 사용하나요?
• 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를 사용할 수 있나요?
2분 읽기 · 게시일: 2026년 4월 24일 · 수정일: 2026년 10월 21일
Docker 실전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Docker Compose 프로덕션 배포: 상태 확인, 재시작 정책, 로그 관리
Docker Compose 프로덕션 환경 실전 가이드입니다. 상태 확인 설정, 재시작 정책, 로그 관리 방법을 자세히 살펴보고 컨테이너 응답 중단부터 자동 복구, 로그로 인한 디스크 용량 고갈까지 방지하는 실용적인 구성을 소개합니다.
38편 중 11편
다음
Docker Compose로 PHP 환경 한 번에 배포하기: DNMP 완전 가이드(Nginx+MySQL+PHP)
Docker Compose를 사용해 DNMP(Docker+Nginx+MySQL+PHP) 개발 환경을 한 번에 배포하는 방법을 단계별로 설명합니다. 10분이면 팀별 환경 차이를 완전히 해결할 수 있으며, 전체 설정 파일과 문제 해결 가이드도 제공합니다.
38편 중 13편



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