Docker Compose로 멀티 컨테이너 앱 운영하기: 서비스·네트워크·볼륨, 헬스체크, 프로덕션 설정
이 글의 핵심
depends_on을 적었는데도 DB가 준비되기 전에 앱이 먼저 떠서 연결 에러가 나는 것은 Compose를 쓰며 가장 흔히 겪는 문제입니다. 이 글은 구성 파일 로딩부터 기동까지의 라이프사이클, 변수 보간과 .env 우선순위, include와 extends, 리소스 제한과 재시작·로깅 설정처럼 개발 환경을 넘어 운영으로 갈 때 필요한 부분을 정리합니다.
이 글의 핵심
Docker Compose로 멀티 컨테이너 앱을 관리하는 글입니다. 서비스 정의, 네트워크, 볼륨, 환경 변수, 프로덕션 설정뿐 아니라 오케스트레이션 라이프사이클, 네트워크·볼륨 심화, 변수 보간, 멀티 파일 병합·상속, 프로덕션 패턴까지 실전 예제로 정리했습니다.
개발 환경을 Compose로 맞춰 두면 새 팀원은
docker compose up한 번으로 DB·캐시까지 같은 버전을 받습니다. 대신 처음 옮길 때는 앱 설정에 남은localhost,depends_on만 믿고 생기는 기동 레이스,down -v로 날린 데이터 같은 함정을 한 번씩 밟게 되는데, 이 글은 그 지점들을 14장에 모아 두었습니다.
들어가며: “개발 환경 설정이 복잡해요”
실무에서 마주치는 문제들
PostgreSQL, Redis 설치가 번거로워요
각각 설치하고 설정해야 합니다. Docker Compose는 한 번에 실행합니다.
팀원마다 환경이 달라요
버전, 설정이 다릅니다. Docker Compose로 일관성을 유지합니다.
여러 컨테이너를 수동으로 실행해요
docker run을 여러 번 실행합니다. Docker Compose는 한 명령으로 실행합니다.
Docker Compose가 해결하는 것
핵심 특징
Docker Compose는 멀티 컨테이너 Docker 앱을 정의하고 실행하는 도구입니다. 주요 장점:
- 간단한 설정: YAML 파일 하나
- 한 번에 실행: docker compose up
- 네트워크 자동 생성: 컨테이너 간 통신
- 볼륨 관리: 데이터 영속성
- 환경 변수: .env 파일 지원
Compose 설치 확인
Docker Desktop(Windows·macOS)에는 Compose V2가 이미 들어 있으므로 Linux에서만 플러그인을 따로 설치하면 됩니다. 설치 후 docker compose version으로 확인합니다.
# Docker Desktop (Windows, macOS)
# Docker Compose 포함됨
# Linux
sudo apt install docker-compose-plugin
# 확인
docker compose version
docker-compose.yml 구조와 기본 명령어
docker-compose.yml
Compose V2에서는 파일 맨 위의 version: 키가 더 이상 쓰이지 않습니다(넣으면 obsolete 경고만 나옴). 아래 예제들도 version 없이 씁니다.
services:
web:
image: nginx:latest
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
명령어
# 시작
docker compose up
# 백그라운드 실행
docker compose up -d
# 중지
docker compose down
# 로그 확인
docker compose logs
# 특정 서비스 로그
docker compose logs web
# 재시작
docker compose restart
React·API·DB를 묶는 풀스택 구성
설정 파일 예시입니다.
# docker-compose.yml
services:
# Frontend (React)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
# 브라우저가 호스트에서 호출하는 주소라 localhost가 맞음 (컨테이너끼리 부를 땐 서비스 이름, 14장 참고)
- REACT_APP_API_URL=http://localhost:8000
depends_on:
- backend
# Backend (FastAPI)
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/mydb
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
volumes:
- ./backend:/app
# Database (PostgreSQL)
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
# Cache (Redis)
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
# Nginx (Reverse Proxy)
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- frontend
- backend
volumes:
postgres-data:
redis-data:
networks:
default:
name: myapp-network
.env 파일로 환경 변수 넘기기
.env 파일
# .env
POSTGRES_USER=postgres
POSTGRES_PASSWORD=secret
POSTGRES_DB=mydb
REDIS_PASSWORD=redis-secret
API_PORT=8000
FRONTEND_PORT=3000
사용
# docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
커스텀 네트워크로 서비스 분리
커스텀 네트워크
services:
frontend:
networks:
- frontend-network
backend:
networks:
- frontend-network
- backend-network
db:
networks:
- backend-network
networks:
frontend-network:
backend-network:
healthcheck로 기동 순서 맞추기
설정 파일 예시입니다.
services:
backend:
image: myapp/backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
docker-compose.prod.yml로 프로덕션 설정 분리
docker-compose.prod.yml
services:
backend:
image: myapp/backend:1.0.0
restart: always
environment:
- NODE_ENV=production
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
db:
image: postgres:16
restart: always
volumes:
- /data/postgres:/var/lib/postgresql/data
# 프로덕션 실행
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
구성 로딩부터 기동까지: depends_on의 함정
docker compose up 한 번이 내부에서 하는 일을 단계로 나누면, 장애 분석·재현·최적화가 훨씬 쉬워집니다. Compose V2(플러그인)는 Compose 규격 파일을 읽어 프로젝트(project) 단위로 리소스를 묶습니다. 프로젝트 이름은 기본적으로 상위 디렉터리 이름이며, COMPOSE_PROJECT_NAME 또는 -p로 바꿀 수 있습니다. 컨테이너·네트워크·볼륨 이름에 접두사로 붙어 동일 호스트에서 여러 스택을 격리합니다.
구성 로딩부터 기동까지의 흐름
- 설정 병합·검증:
-f로 지정한 파일들을 순서대로 병합하고(후행 파일이 동일 키를 덮어씀),include가 있으면 지정된 파일을 끌어옵니다. 스키마 검증 후 보간(interpolation) 이 적용됩니다. - 이미지 확보:
pull_policy(예:missing,always)와 로컬 이미지 유무에 따라 레지스트리에서 가져오거나 빌드합니다.build가 있으면 Dockerfile 컨텍스트를 빌드해 태그를 만듭니다. - 네트워크·볼륨 생성: 선언된 네트워크·볼륨이 없으면 생성합니다. 기본 네트워크는 프로젝트 스코프의 브리지 네트워크입니다.
- 컨테이너 생성·연결: 각 서비스의 컨테이너를 만들고 DNS 이름(서비스명)으로 서로를 해석할 수 있게 붙입니다.
- 시작 순서와 의존성:
depends_on은 컨테이너 기동 순서만 보장합니다. DB가 “준비됐는지”는 기본적으로 보장하지 않으므로, 애플리케이션 쪽 재시도·헬스체크·엔트리포인트 대기 스크립트가 필요합니다. Compose 규격에서는depends_on에condition: service_healthy등을 두어 헬스체크 통과 후 기동하도록 할 수 있습니다(이미지·설정에 헬스체크 정의가 있어야 함). - 실행 중: 재시작 정책(
restart), 시그널, 로그 드라이버가 적용됩니다.docker compose down은 기본적으로 네트워크를 제거하며, 볼륨은-v를 줄 때만 삭제합니다.
depends_on의 함정과 해법
백엔드가 DB보다 먼저 요청을 보내 연결 실패가 나는 경우가 흔합니다. 운영에서는 (1) 백엔드 코드에서 연결 재시도, (2) healthcheck + depends_on 조건, (3) 초기화 전용 init 컨테이너 또는 엔트리포인트에서 pg_isready 등으로 대기 —를 조합합니다. 개발 편의만으로 depends_on만 믿으면 레이스가 남습니다.
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
interval: 5s
timeout: 3s
retries: 10
api:
depends_on:
db:
condition: service_healthy
네트워크 분리와 이름 붙은 볼륨 vs 바인드 마운트
네트워크: 기본 브리지와 분리
같은 Compose 프로젝트의 서비스는 기본적으로 한 브리지 네트워크에 붙으며, 서비스 이름이 DNS 이름이 됩니다. frontend에서 http://backend:8000처럼 호출하는 패턴이 이 때문입니다. networks로 여러 네트워크를 정의하면 서비스별로 어느 세그먼트에 붙일지 나눌 수 있어, DB는 백엔드 전용 네트워크에만 두는 식의 최소 노출이 가능합니다.
networks:
edge:
app:
internal: true # 외부 라우팅 차단(동일 브리지 내 통신만)
services:
proxy:
networks: [edge]
api:
networks: [edge, app]
db:
networks: [app]
internal: true는 해당 네트워크를 인터넷/호스트와 격리하는 용도로 사용됩니다(드라이버·환경에 따라 동작을 항상 확인하세요).
별칭(alias) 으로 레거시 호스트명을 흡수할 수 있습니다.
services:
api:
networks:
app:
aliases:
- legacy-api
볼륨: 이름 붙은 볼륨 vs 바인드 마운트
- 이름 붙은 볼륨(
volumes:아래 선언): Docker가 저장 위치를 관리합니다. 백업은docker run --volumes-from또는 볼륨을 마운트한 임시 컨테이너로 복사하는 방식이 일반적입니다. - 바인드 마운트(
./src:/app): 호스트 경로를 그대로 공유합니다. 개발 시 핫 리로드에 좋지만, 경로·권한·OS 차이(Windows/macOS 볼륨 성능) 이슈가 생깁니다. - 익명/임시 볼륨: 디버깅·캐시에 쓰이기도 하며, 스택 제거 시 정리 정책을 반드시 확인해야 합니다.
프로덕션 DB 데이터는 이름 붙은 볼륨 또는 명시적 호스트 경로로 두며, 백업·복구 절차를 문서화하는 것이 안전합니다. down -v는 이름 붙은 볼륨까지 삭제할 수 있으므로 운영 환경에서 주의합니다.
volumes:
pgdata:
driver: local
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data
이름 붙은 볼륨을 파일로 떠 두는 가장 단순한 방법은 볼륨을 마운트한 임시 컨테이너로 tar를 만드는 것입니다.
docker run --rm -v pgdata:/data -v "$(pwd)":/backup alpine tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata:/data -v "$(pwd)":/backup alpine tar xzf /backup/pgdata.tar.gz -C /data # 복원
다만 DB가 돌아가는 중에 데이터 디렉터리를 tar로 뜨면 일관성이 깨진 백업이 나올 수 있습니다. 파일을 쓰는 도중의 상태가 섞이기 때문입니다. DB 볼륨은 컨테이너를 멈춘 뒤 떠 두거나, docker compose exec db pg_dump -U app appdb > backup.sql처럼 DB 자체의 백업 도구를 쓰는 편이 안전합니다. 캐시·빌드 산출물처럼 사라져도 되는 쓰기 영역은 tmpfs: [/tmp]로 메모리에 두면 디스크 I/O와 정리 부담이 줄어듭니다.
환경 변수 보간(Interpolation) 규칙
Compose 파일의 ${VAR} 형태는 클라이언트 측에서 먼저 치환됩니다. 컨테이너 안의 쉘이 해석하는 것과 단계가 다릅니다.
문법과 이스케이프
${VAR}: 셸 또는.env에서 값을 가져옵니다.${VAR:-default}: 없으면 기본값.${VAR:?err}: 없으면 오류로 실패(필수 변수에 유용).$$: Compose 처리 후 컨테이너에$하나로 전달(예:$$HOME).
.env 파일의 위치와 우선순위
- 프로젝트 디렉터리의
.env는 보간에 자주 사용됩니다.--env-file로 다른 파일을 지정할 수도 있습니다. - 서비스의
environment:와env_file:은 컨테이너 환경을 구성하며, YAML 보간 단계와는 별개입니다. “Compose가 읽는 변수”와 “앱이 받는 변수”를 혼동하지 않는 것이 중요합니다.
services:
api:
environment:
DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
env_file:
- .env.local
비밀은 Git에 넣지 말고, CI/CD·시크릿 스토어에서 .env를 주입하거나 런타임에만 마운트하세요.
여러 Compose 파일 병합과 include
여러 -f 파일 병합
가장 흔한 패턴은 베이스 + 오버레이입니다. 뒤에 온 파일이 앞의 값을 덮어씁니다.
docker compose -f docker-compose.yml -f docker-compose.override.yml up
docker-compose.override.yml은 같은 디렉터리에 두면 일부 버전에서 자동 로드되던 관행이 있었으나, 명시적 -f로 관리하는 편이 재현성이 좋습니다.
include (Compose 규격)
공통 조각을 여러 스택에서 재사용하려면 Compose 파일의 include 로 다른 파일을 끌어올 수 있습니다(환경·경로 기준은 사용 중인 Compose 버전 문서를 확인하세요).
include:
- path: ./compose/infra.yml
팀 규모가 커질수록 공통 네트워크·로깅·레이블을 include로 모아 중복을 줄입니다.
COMPOSE_FILE 환경 변수
여러 파일을 한 번에 지정하기 어려울 때 COMPOSE_FILE에 경로를 나열해 기본 병합 세트를 팀에 공유할 수 있습니다. Unix 계열은 :로, Windows는 ;로 구분하는 것이 일반적입니다(환경·Compose 버전에 따라 다를 수 있으니 공식 문서를 확인하세요).
extends에 대해
과거 Compose 파일 형식의 extends 는 재사용을 위해 쓰였으나, 최신 Compose 규격에서는 include와 다중 파일 병합으로 대체하는 추세입니다. 레거시 프로젝트를 유지보수할 때만 확인하면 됩니다.
리소스 제한·프로파일·시크릿 파일로 운영하기
단일 호스트·소규모 배포에서 Compose는 여전히 유효합니다. 다만 Swarm/Kubernetes와 달리 스케줄링·오토스케일·롤링 업데이트가 플랫폼 차원에서 제공되지 않으므로, 프로세스 감독·배포 절차·백업을 외부(시스템d, 모니터링, 스크립트)로 보완합니다.
리소스 제한과 deploy 키 주의
deploy: 아래 resources 등은 Docker Swarm의 docker stack deploy 에서 의미가 분명한 항목이 많습니다. 일반 docker compose up만 쓰는 경우에는 무시되거나 기대와 다르게 동작할 수 있어, 단일 호스트 제한은 서비스 수준의 mem_limit, cpus 등(엔진·Compose 버전에 맞는 키)과 호스트 모니터링을 병행하는 편이 안전합니다. 실제로 어떤 키가 적용되는지는 docker compose config로 렌더링된 결과와 docker inspect로 검증하세요.
재시작·로깅·읽기 전용
services:
api:
image: myorg/api:1.2.3
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
user: "1000:1000"
read_only: true는 루트 파일시스템 쓰기를 막습니다. 앱이 /tmp 등에 쓰면 tmpfs 또는 볼륨을 추가합니다.
프로파일(profile)로 dev/prod 분리
profiles:로 기본 기동에서 제외할 수 있어, 로컬 전용 도구(메일 목업, Adminer)를 프로덕션에서 실수로 올리는 일을 줄입니다.
services:
adminer:
image: adminer
profiles: ["tools"]
docker compose --profile tools up -d
헬스체크·롤아웃
무중단에 가깝게 가려면 새 버전 컨테이너를 띄운 뒤 헬스체크 통과 후 트래픽을 전환하는 식의 블루그린/프록시 뒤 교체가 필요합니다. Compose 단독으로는 Kubernetes 수준의 롤링 업데이트가 없으므로, 리버스 프록시(Nginx, Traefik) 와 배포 스크립트를 조합하는 경우가 많습니다.
보안 요약
- 루트 컨테이너 지양, 최소 권한 사용자.
- 시크릿을 이미지에 넣지 않기; 런타임 주입과 로테이션.
- 불필요한 포트 노출 금지(가능하면 리버스 프록시 한 곳만 공개).
시크릿: 환경 변수 대신 파일로
비밀번호를 environment:에 넣으면 docker inspect나 프로세스 환경 변수로 그대로 보입니다. Compose의 secrets는 Swarm 없이도 동작하며, 지정한 파일을 컨테이너 안 /run/secrets/<이름>에 읽기 전용으로 마운트합니다. 공식 PostgreSQL·MySQL 이미지는 *_FILE 환경 변수로 이 경로를 받는 규칙을 지원합니다.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt # 이 파일은 .gitignore에
localhost 접속·볼륨 삭제처럼 처음 막히는 실수
| 증상 | 원인 | 해결 |
|---|---|---|
앱이 DB에 connection refused | 접속 주소를 localhost로 둠. 컨테이너 안의 localhost는 자기 자신 | 서비스 이름으로 접속(postgres://db:5432/...). 같은 Compose 파일의 서비스는 기본 네트워크에서 이름으로 찾을 수 있음 |
| 일부 서비스끼리만 통신 안 됨 | 한쪽만 커스텀 네트워크에 넣고 다른 쪽은 기본 네트워크에 둠 | 통신해야 하는 서비스를 같은 네트워크에 두기 |
| 재시작했더니 DB 데이터가 사라짐 | docker compose down -v가 이름 붙은 볼륨까지 삭제 | 평소엔 down만, 볼륨 삭제는 의도할 때만 |
.env를 고쳤는데 반영이 안 됨 | 이미 떠 있는 컨테이너는 값을 다시 읽지 않음, 또는 .env 위치가 프로젝트 디렉터리가 아님 | docker compose up -d로 재생성, docker compose config로 보간 결과 확인 |
port is already allocated | 호스트 포트를 다른 프로세스·다른 스택이 사용 중 | ports: ["8080:80"]처럼 호스트 쪽 포트 변경 |
이 중 localhost 문제가 압도적으로 많습니다. 로컬에서 DB를 직접 띄우고 개발하다가 앱만 컨테이너로 옮기면, 설정 파일에 남아 있던 localhost가 그대로 따라오기 때문입니다. 환경 변수로 DB 호스트를 받게 해 두면 로컬 실행(localhost)과 Compose 실행(db)을 설정만으로 전환할 수 있습니다.
배포를 CI로 자동화한다면, 이미지 빌드·푸시는 CI에서 하고 서버에서는 docker compose pull && docker compose up -d만 실행하는 구조가 가장 단순합니다. 서버에서 직접 빌드하면 빌드 캐시·빌드 도구가 운영 서버에 쌓이고, 롤백할 때 같은 이미지를 다시 받을 수 없습니다.
Compose 설정 요약과 점검 목록
핵심 요약
- Docker Compose: 멀티 컨테이너 앱 관리
- 간단한 설정: YAML 파일
- 라이프사이클: 병합·보간 → 이미지·네트워크·볼륨 → 컨테이너 생성·기동 →
depends_on은 순서만(준비 여부는 별도) - 네트워크: 프로젝트 브리지·서비스 DNS·
internal/별칭으로 세그먼트 분리 - 볼륨: 이름 붙은 볼륨 vs 바인드·
down -v주의 - 환경 변수: 보간(
${VAR:-})·.envvs 컨테이너env_file - 멀티 파일:
-f후행 우선·include·COMPOSE_FILE - 헬스체크: 재시작·
depends_on조건과 함께 쓸 때 의미가 큼 - 프로덕션:
restart, 로그 로테이션,read_only, 비루트, 프로파일,deploy는 Swarm 여부 확인
도입 점검 목록
- docker-compose.yml 작성
- 서비스 정의
- 네트워크 설정
- 볼륨 설정
- 환경 변수·보간 규칙 정리
- 헬스체크·준비 완료 대기 전략
- 멀티 파일 병합·공통
include여부 검토 - 프로덕션 설정(리소스·로그·보안·프로파일)
같이 보면 좋은 글
- Kubernetes 핵심 오브젝트: Pod, Deployment, Service, ConfigMap·Secret, Ingress, 헬스체크, HPA
- GitHub Actions CI/CD 가이드
- Terraform 입문: HCL과 모듈, AWS·Azure·GCP 프로바이더, State 백엔드, Workspace 환경 분리
자주 묻는 질문 (FAQ)
Q. Docker Compose vs Kubernetes, 어떤 게 나은가요?
A. Docker Compose는 단일 서버용입니다. Kubernetes는 클러스터용입니다. 소규모는 Docker Compose, 대규모는 Kubernetes를 권장합니다.
Q. Swarm vs Compose, 차이가 뭔가요?
A. Swarm은 오케스트레이션 도구입니다. Compose는 개발 도구입니다. 프로덕션은 Swarm이나 Kubernetes를 사용하세요.
Q. 볼륨 데이터는 어디에 저장되나요?
A. Docker가 관리하는 디렉터리에 저장됩니다. docker volume inspect <volume_name>으로 확인할 수 있습니다.
Q. depends_on만으로 DB 준비를 보장할 수 있나요?
A. 아니요. 컨테이너 시작 순서만 맞춥니다. 앱에서 재시도하거나, healthcheck + depends_on 조건, 또는 준비 대기 스크립트를 쓰세요.
Q. deploy.resources를 넣었는데 docker compose up에서 제한이 안 걸리는 것 같아요.
A. deploy는 Swarm stack deploy 에 맞춘 항목이 많습니다. 단일 호스트 Compose에서는 docker compose config와 docker inspect로 실제 적용 여부를 확인하며, 필요하면 서비스 수준 제한 키·호스트 cgroup 모니터링을 병행하세요.