Docker Compose와 멀티컨테이너 앱
왜 Compose가 필요한가
실제 서비스는 거의 항상 여러 컨테이너로 구성된다. 웹 서버, 데이터베이스, 캐시, 큐 워커—이것들을 docker run 명령어로 하나씩 실행하면 네트워크 연결, 환경 변수, 볼륨, 실행 순서를 수작업으로 맞춰야 한다. 실수가 잦고 반복이 어렵다.
Docker Compose는 멀티컨테이너 애플리케이션의 정의, 실행, 관리를 하나의 YAML 파일로 처리한다. "인프라를 코드로"라는 아이디어를 단일 호스트 범위에서 실현하는 도구다.
Compose v1 vs v2: 무엇이 달라졌나
| 항목 | Compose v1 (legacy) | Compose v2 |
|---|---|---|
| 언어 | Python | Go |
| 실행 방식 | docker-compose (독립 바이너리) | docker compose (CLI 플러그인) |
| 설치 | pip / 별도 패키지 | Docker Engine에 번들 |
| 파일명 | docker-compose.yml 우선 | compose.yaml 우선 (기존 파일명도 지원) |
version: 필드 | 필수 | 무시됨 (지정해도 경고 없이 무시) |
Compose v2는 Go로 다시 작성되어 의존성 문제 없이 단일 바이너리로 배포된다. 2023년 Compose v1은 공식적으로 지원 종료(EOL)됐다.
Docker v5(2025): 내부 버전 넘버링이 v2에서 v5로 건너뛰었다. 이는 legacy Compose 파일 포맷
version: "2"/version: "3"과의 혼동을 피하기 위한 것이다. 기능적으로는 v2와 동일하며, Go SDK가 추가됐다.
compose.yaml 핵심 구조
image / build / ports
environment / volumes
depends_on / healthcheck
driver: bridge / overlay
서비스간 DNS 이름 해석
컨테이너 수명과 독립
여러 서비스에서 공유 가능
에 in-memory 마운트
configs: 일반 설정파일
기본적인 세 서비스(웹 + DB + 캐시) 구성 예시:
services:
web:
build: ./app
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379/0
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER:-appuser}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "${DB_USER:-appuser}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
networks:
- backend
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
cache:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- cache-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
volumes:
db-data:
cache-data:
networks:
backend:
driver: bridge서비스 의존성과 시작 순서
depends_on의 세 가지 조건
depends_on:
db:
condition: service_healthy # healthcheck가 healthy가 될 때까지 대기
migration:
condition: service_completed_successfully # 종료코드 0으로 끝날 때까지 대기
queue:
condition: service_started # 컨테이너 시작만 확인 (기본값)service_started는 프로세스가 실제로 준비됐는지 보장하지 않는다. 데이터베이스나 외부 서비스에는 반드시 service_healthy를 사용해야 한다.
healthcheck 설정
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s # 얼마마다 검사할지
timeout: 5s # 검사 타임아웃
retries: 5 # 이 횟수 실패 시 unhealthy
start_period: 15s # 시작 후 이 시간 동안은 실패를 retries에 미포함상태 전이: starting → (healthy | unhealthy)
start_period는 느린 DB나 JVM 서버에서 불필요한 unhealthy 판정을 막는 핵심 옵션이다.
환경 변수 관리
${VAR}보간(interpolation) 용도.
컨테이너 안으로 전달 안 됨 예: image: ${REGISTRY}/myapp:${TAG}
파일의 KEY=VALUE를
컨테이너 환경변수로 주입 예: env_file: [.env.prod]
env_file보다 우선.
호스트 변수 참조 가능 예: - DEBUG=${DEBUG:-false}
.env 파일 (compose.yaml과 같은 디렉터리)은 Compose 파일 자체를 매개변수화한다. 예를 들어 이미지 태그나 포트를 환경별로 달리 쓸 때 유용하다:
# .env
REGISTRY=123456789.dkr.ecr.ap-northeast-2.amazonaws.com
TAG=v2.3.1
PORT=8000
DB_PASSWORD=supersecret# compose.yaml
services:
web:
image: ${REGISTRY}/myapp:${TAG:-latest}
ports:
- "${PORT:-8080}:8080"단일 따옴표 주의: '${VAR}'은 보간되지 않고 리터럴 문자열로 처리된다.
우선순위 (높음 → 낮음): CLI -e 플래그 → environment: 속성 → env_file: → 호스트 쉘 환경변수
네트워킹: 서비스 이름이 곧 DNS 이름
Compose는 프로젝트별 브리지 네트워크를 자동으로 생성한다. 네트워크에 함께 있는 서비스들은 서비스 이름으로 서로를 찾을 수 있다.
# web 서비스에서 db에 접근할 때
DATABASE_URL=postgresql://user:pass@db:5432/myapp
# ^^
# 서비스 이름이 DNS 이름이 됨내부 DNS는 Docker 임베디드 DNS 서버(127.0.0.11)가 담당한다. 여러 네트워크를 정의해 서비스 간 접근을 논리적으로 분리할 수 있다:
services:
web:
networks: [frontend, backend]
api:
networks: [backend]
db:
networks: [backend] # web은 db에 직접 접근 불가
networks:
frontend:
backend:볼륨 세 종류
| 종류 | 선언 방법 | 수명 | 컨테이너 간 공유 |
|---|---|---|---|
| Named Volume | db-data:/var/lib/postgresql/data | Docker가 관리, 컨테이너 삭제 후에도 유지 | 가능 |
| Bind Mount | ./src:/app/src | 호스트 경로 직접 연결 | 가능 (같은 호스트 경로) |
| tmpfs | tmpfs: [/tmp] | 컨테이너 수명과 동일 (메모리) | 불가 |
개발 환경에서는 소스 코드를 바인드 마운트로 연결해 파일 변경이 컨테이너에 즉시 반영되도록 한다. 프로덕션에서는 Named Volume을 쓴다.
Override 파일로 환경별 설정 분리
Compose는 여러 파일을 순서대로 머지(deep merge)한다. 이후 파일의 값이 앞 파일을 덮어쓴다.
기본 설정 (이미지, 포트, 네트워크) + compose.override.yaml
자동 머지 (개발용 볼륨, 디버그 포트) = 개발 환경
-f로 명시 (replicas, 리소스 제한) = 프로덕션 환경
# 개발: compose.yaml + compose.override.yaml 자동 머지
docker compose up
# 프로덕션: 명시적 파일 지정
docker compose -f compose.yaml -f compose.prod.yml up -d
# 스테이징
docker compose -f compose.yaml -f compose.staging.yml up -d# compose.override.yaml (개발용, 자동 머지)
services:
web:
volumes:
- ./src:/app/src # 소스 바인드 마운트 추가
environment:
- DEBUG=true
db:
ports:
- "5432:5432" # 개발 시 DB 포트 노출# compose.prod.yml
services:
web:
image: myapp:${TAG} # 빌드 대신 레지스트리 이미지 사용
deploy:
replicas: 3
resources:
limits:
cpus: "1.0"
memory: 512MProfiles: 선택적 서비스 실행
특정 환경에서만 필요한 서비스(디버그 도구, 로드 테스터, DB 어드민 UI 등)는 profiles로 분류한다.
services:
web: # profile 없음 → 항상 실행
image: myapp
adminer: # DB GUI — 개발 때만
image: adminer
profiles: ["debug"]
ports:
- "8080:8080"
k6: # 부하 테스트 도구
image: grafana/k6
profiles: ["load-test"]# 기본 실행 (adminer, k6 제외)
docker compose up
# debug 프로파일 활성화
docker compose --profile debug up
# 환경변수로 지정
COMPOSE_PROFILES=debug,load-test docker compose upCompose Watch: 파일 변경 자동 동기화
Compose v2.22부터 GA된 기능이다. 호스트 파일 변경을 감지해 컨테이너에 자동으로 반영한다. 이미지 전체를 재빌드하지 않아도 된다.
services:
app:
build: .
develop:
watch:
- path: ./src
action: sync # 파일 변경 시 컨테이너로 즉시 동기화
target: /app/src
- path: ./requirements.txt
action: sync+restart # 동기화 후 컨테이너 재시작
- path: ./Dockerfile
action: rebuild # 이미지 전체 재빌드세 가지 action:
| action | 동작 | 적합한 경우 |
|---|---|---|
sync | 파일만 복사, 재시작 없음 | 핫 리로드 지원 앱 (Flask, Next.js dev) |
sync+restart | 동기화 후 컨테이너 재시작 | 설정 파일, 의존성 변경 |
rebuild | 이미지 전체 재빌드 | Dockerfile 변경 |
docker compose up --watch
# 또는
docker compose watch내부적으로 파일 이벤트를 디바운싱하여 빠른 연속 편집 시에도 중복 동기화를 방지한다.
실전 멀티컨테이너 스택 전체 예시
# compose.yaml
services:
web:
build: ./app
ports:
- "${PORT:-8000}:8000"
environment:
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/myapp
- REDIS_URL=redis://cache:6379/0
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
networks:
- backend
develop:
watch:
- path: ./app/src
action: sync
target: /app/src
- path: ./app/requirements.txt
action: sync+restart
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
networks:
- backend
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
cache:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- cache-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
deploy:
resources:
limits:
memory: 256M
adminer:
image: adminer:latest
ports:
- "8001:8080"
depends_on:
db:
condition: service_healthy
networks:
- backend
profiles: ["debug"]
volumes:
db-data:
cache-data:
networks:
backend:
driver: bridge# .env
DB_USER=appuser
DB_PASSWORD=secretpassword
PORT=8000자주 쓰는 Compose 명령어
# 서비스 시작 (백그라운드)
docker compose up -d
# 로그 실시간 확인
docker compose logs -f web
# 특정 서비스만 재시작
docker compose restart web
# 실행 중인 서비스 상태 확인
docker compose ps
# 서비스 안으로 접속
docker compose exec web /bin/sh
# 이미지 재빌드 후 시작
docker compose up -d --build
# 컨테이너 + 볼륨까지 모두 삭제
docker compose down -v
# 개발 모드 (파일 감시)
docker compose up --watchReferences
- Compose application model — Docker Docs
- Compose file reference — Docker Docs
- Control startup and shutdown order in Compose — Docker Docs
- Networking in Compose — Docker Docs
- Environment variables in Compose — Docker Docs
- Use multiple Compose files — Docker Docs
- Using profiles with Compose — Docker Docs
- Use Compose Watch — Docker Docs
- Announcing Docker Compose Watch GA Release — Docker Blog
- History and development of Docker Compose — Docker Docs
- Compose Deploy Specification — Docker Docs
- Manage secrets securely in Docker Compose — Docker Docs