[도커 30강] 17강. 개발용 compose와 운영용 compose 분리
하나의 compose.yaml로 개발부터 운영까지 모두 해결하려고 하면, 초반에는 편해 보여도 규모가 커질수록 문제가 터집니다. 개발에서는 코드 핫리로드, 디버거 포트, 테스트용 도구 컨테이너가 필요하지만 운영에서는 오히려 이런 요소가 공격면을 넓히고 장애를 유발합니다. 반대로 운영 기준만 엄격하게 적용하면 개발 속도가 급격히 떨어집니다.
이번 강의의 핵심은 단순히 파일을 두 개로 나누는 기술이 아니라, 환경별 의도를 Compose 구조에 명시하는 습관을 만드는 것입니다. 즉, "개발은 빠르게 실험하고", "운영은 안전하고 예측 가능하게"라는 목적을 파일 구조에서 바로 읽히게 만들겠습니다. 실무에서 자주 쓰는 compose.yaml + compose.override.yaml 패턴과 compose.prod.yaml 패턴을 비교하고, 팀 규칙으로 고정하는 방법까지 정리하겠습니다.
핵심 개념
- 개발용 compose는 생산성 최적화가 목적입니다. 바인드 마운트, 핫리로드, 디버깅 포트 노출, 상세 로그를 허용합니다.
- 운영용 compose는 안정성/보안/재현성이 목적입니다. 고정 이미지 태그, 읽기 전용 파일시스템, 최소 포트 노출, 자원 제한을 우선합니다.
- 하나의 베이스 파일에 공통 요소를 두고, 개발/운영에서 오버레이 파일을 분리하면 중복은 줄이고 의도는 분명해집니다.
- "dev에서 되던 게 prod에서 안 된다"는 문제를 줄이려면 완전 분리가 아니라 공통 기반 + 환경별 차이 최소화 전략이 중요합니다.
- Compose 병합 결과를 항상
docker compose config로 확인해야 실제 적용값을 오해하지 않습니다.
기본 사용
예제 1) 베이스 + 개발 오버레이 + 운영 오버레이 파일 구조 만들기
먼저 공통 설정은 compose.yaml에 두고, 개발용은 compose.dev.yaml, 운영용은 compose.prod.yaml로 분리합니다.
mkdir -p ~/docker100/lesson17/app
cd ~/docker100/lesson17
cat > compose.yaml <<'YAML'
services:
app:
build:
context: .
dockerfile: Dockerfile
image: myapp:base
environment:
APP_ENV: base
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD:-apppass}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb -h 127.0.0.1 || exit 1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
volumes:
db_data:
YAML
cat > compose.dev.yaml <<'YAML'
services:
app:
image: myapp:dev
environment:
APP_ENV: development
LOG_LEVEL: debug
volumes:
- ./app:/app
command: ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=8000", "--reload"]
ports:
- "8000:8000"
db:
ports:
- "5432:5432"
YAML
cat > compose.prod.yaml <<'YAML'
services:
app:
image: my-registry.example.com/myapp:1.0.0
environment:
APP_ENV: production
LOG_LEVEL: info
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.00"
memory: 1024M
ports:
- "80:8000"
db:
restart: unless-stopped
ports: []
YAML
설명:
compose.yaml은 환경 공통의 "기본 뼈대"입니다. DB 헬스체크, 의존성 같은 정책을 공통으로 둡니다.compose.dev.yaml은 개발 편의 요소(바인드 마운트, reload, 디버그 로그, DB 포트 개방)를 추가합니다.compose.prod.yaml은 운영 안전장치(read_only, tmpfs, restart, 자원 제한, 포트 최소화)를 정의합니다.
예제 2) 같은 프로젝트를 개발 모드와 운영 모드로 각각 실행하기
Compose는 -f 옵션 순서대로 파일을 병합합니다. 뒤에 오는 파일이 앞 파일을 덮어씁니다.
cd ~/docker100/lesson17
# 개발 모드 실행
docker compose -f compose.yaml -f compose.dev.yaml up -d --build
docker compose -f compose.yaml -f compose.dev.yaml ps
docker compose -f compose.yaml -f compose.dev.yaml logs --tail=80 app
# 운영 모드 시뮬레이션 실행
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml ps
docker compose -f compose.yaml -f compose.prod.yaml logs --tail=80 app
설명:
- 같은 베이스 위에서 환경별 오버레이만 바꿔 실행하므로 구조가 단순합니다.
- 개발 모드에서는 코드 수정 즉시 반영과 디버깅이 쉬워집니다.
- 운영 모드에서는 의도치 않은 포트 노출이나 쓰기 권한 남용 같은 위험을 줄일 수 있습니다.
예제 3) 병합 결과 검증으로 "실제 적용값" 확인하기
파일을 분리한 뒤 가장 많이 하는 실수는 "내가 생각한 값"과 "실제 병합된 값"이 다른데도 모르고 넘어가는 것입니다. 반드시 병합 결과를 텍스트로 확인해야 합니다.
cd ~/docker100/lesson17
# 개발 병합 결과 확인
docker compose -f compose.yaml -f compose.dev.yaml config > /tmp/lesson17-dev-merged.yaml
sed -n '1,220p' /tmp/lesson17-dev-merged.yaml
# 운영 병합 결과 확인
docker compose -f compose.yaml -f compose.prod.yaml config > /tmp/lesson17-prod-merged.yaml
sed -n '1,220p' /tmp/lesson17-prod-merged.yaml
# 차이 비교
diff -u /tmp/lesson17-dev-merged.yaml /tmp/lesson17-prod-merged.yaml | sed -n '1,200p'
설명:
- 병합 결과를 파일로 남기면 PR 리뷰에서 환경 차이를 명확히 토론할 수 있습니다.
- 특히
ports,environment,volumes,restart,read_only항목은 항상 눈으로 검증하세요. - 운영에서 민감한 옵션이 dev 설정으로 덮어써지는 사고를 사전에 차단할 수 있습니다.
예제 4) 팀 협업용 실행 규칙을 스크립트로 고정하기
사람이 매번 긴 명령을 치면 실수가 납니다. Makefile이나 스크립트로 진입점을 표준화하면 신입/기존 구성원 모두 같은 방식으로 실행할 수 있습니다.
cd ~/docker100/lesson17
cat > Makefile <<'MAKE'
.PHONY: dev-up dev-down prod-up prod-down config-dev config-prod
dev-up:
docker compose -f compose.yaml -f compose.dev.yaml up -d --build
dev-down:
docker compose -f compose.yaml -f compose.dev.yaml down
prod-up:
docker compose -f compose.yaml -f compose.prod.yaml up -d
prod-down:
docker compose -f compose.yaml -f compose.prod.yaml down
config-dev:
docker compose -f compose.yaml -f compose.dev.yaml config
config-prod:
docker compose -f compose.yaml -f compose.prod.yaml config
MAKE
make dev-up
make config-prod | sed -n '1,120p'
설명:
- 실행 규칙을 코드로 남기면 구두 지시보다 정확합니다.
- CI에서도 같은 명령을 재사용할 수 있어 재현성이 올라갑니다.
- 운영 실수(예: dev 파일로 운영 배포)를 자동화 단계에서 차단하기 쉬워집니다.
자주 하는 실수
실수 1) dev/prod를 완전히 다른 프로젝트처럼 분리
- 원인: 각 환경 파일을 독립적으로 복사/수정하며 관리.
- 문제: 시간이 지나면 설정이 서로 멀어져 "한쪽만 성공" 상태가 됩니다.
- 해결: 공통 베이스를 두고 차이만 오버레이로 관리합니다.
실수 2) 운영에서도 바인드 마운트를 그대로 사용
- 원인: 개발에서 편했던 패턴을 운영에 그대로 적용.
- 문제: 호스트 파일 변경에 의해 동작이 흔들리고, 배포 재현성이 무너집니다.
- 해결: 운영은 고정 이미지 기반으로 배포하고, 필요 시 read_only + tmpfs 조합을 검토합니다.
실수 3) 운영 DB 포트를 외부에 불필요하게 노출
- 원인: 테스트 편의를 위해 열어둔
5432:5432를 그대로 유지. - 문제: 공격면 확대, 오접속 위험, 보안 감사 이슈.
- 해결: 운영에서는 내부 네트워크 통신만 허용하고 외부 포트는 기본 비공개로 둡니다.
실수 4) 병합 결과를 확인하지 않고 "아마 맞겠지"로 진행
- 원인: Compose 병합 규칙(리스트 병합, 값 덮어쓰기)에 대한 오해.
- 문제: 환경변수/포트/커맨드가 예상과 다르게 적용되어 장애 발생.
- 해결: 배포 전
docker compose config를 필수 검증 단계로 강제합니다.
실무 패턴
실무에서는 다음 패턴이 가장 안정적으로 작동합니다.
- Base는 최소 공통분모만 유지
- 서비스 이름, 네트워크, 볼륨, 핵심 헬스체크처럼 환경과 무관한 요소만 둡니다.
- Dev는 생산성 기능 집중
- 바인드 마운트, hot reload, debug 로그, 테스트 도구 컨테이너를 허용합니다.
- 대신 "개발 전용"임을 파일 이름과 문서에서 분명히 표기합니다.
- Prod는 재현성과 방어 설정 집중
- immutable image tag(예:
1.0.0, git sha)를 사용합니다. - restart 정책, read_only, 자원 제한, 최소 포트 노출을 기본값으로 둡니다.
- 검증 자동화
- CI에서
docker compose -f compose.yaml -f compose.prod.yaml config를 실행해 문법/병합 검증. - 보안 점검 스크립트로 "운영에서 금지된 옵션"(예: 디버그 포트, 과도한 권한)을 탐지.
- 문서화 + 명령 표준화
README와Makefile에 dev/prod 실행 명령을 고정.- "운영 배포는 오직 prod 조합만" 같은 규칙을 PR 템플릿에 포함.
이 패턴의 장점은 팀 규모가 커져도 품질이 크게 흔들리지 않는다는 점입니다. 새로운 팀원이 들어와도 make dev-up, make config-prod 같은 표준 진입점으로 빠르게 적응할 수 있고, 리뷰어는 병합 결과 중심으로 확인하면 되므로 코드 리뷰 품질도 좋아집니다.
오늘의 결론
한 줄 요약: Compose 분리는 파일 개수의 문제가 아니라, 개발 생산성과 운영 안정성을 동시에 지키기 위한 의도 분리 전략이다.
연습문제
- 현재 프로젝트에서
compose.dev.yaml에만 필요한 설정 3가지(예: 바인드 마운트, 디버그 포트, reload)를 정의하고, 왜 운영에선 제외해야 하는지 설명해보세요. compose.prod.yaml에read_only,restart, 자원 제한을 적용한 뒤docker compose config결과를 확인하고, 실제로 어떤 값이 병합되는지 정리해보세요.- 팀 공용
Makefile에dev-up,prod-up,config-prod타깃을 추가하고, 잘못된 파일 조합으로 배포하지 않도록 리뷰 체크리스트를 작성해보세요.
이전 강의 정답
16강 연습문제 해설:
start_period를 0s로 두면 DB 초기화가 끝나기 전의 실패가 즉시 누적되어 unhealthy 판정이 빨라질 수 있습니다. 특히 디스크가 느리거나 초기 마이그레이션이 긴 환경에서 재현이 잘 됩니다.- 앱 재시도 횟수를 3회로 낮추면 의존 서비스가 조금만 늦어도 앱이 조기 종료될 가능성이 큽니다. 운영에서는 서비스 평균 기동 시간과 변동 폭을 측정해
attempts/delay를 데이터 기반으로 정해야 합니다. - Redis healthcheck를 의도적으로 깨면 app이
service_healthy조건을 만족하지 못해 시작 대기 상태로 남습니다. CI에서는 compose 기동 후 health 상태를 검사해 unhealthy 서비스가 있으면 즉시 실패 처리하는 단계가 필요합니다.
실습 환경/재현 정보
- OS: macOS 15+ (Apple Silicon) 또는 Ubuntu 22.04+
- Docker: Engine/Desktop 25.x 이상
- Compose: v2.x (
docker compose version) - 사용 파일:
compose.yaml,compose.dev.yaml,compose.prod.yaml,Makefile - 실행 순서:
- 베이스/오버레이 파일 작성
- dev 조합으로 기동 및 로그 확인
- prod 조합으로 기동 및 포트/권한 정책 확인
docker compose config로 병합 결과 검증- diff로 dev/prod 차이 리뷰
- 재현 체크:
- dev에서 코드 변경 즉시 반영되는가?
- prod에서 불필요 포트/바인드 마운트가 제거됐는가?
- 공통 헬스체크/의존성 정책이 양쪽에 일관되게 적용되는가?
- 팀원이 동일 명령으로 같은 결과를 재현할 수 있는가?