Compose (멀티, YAML, 의존성)
이전 글에서 Docker로 컨테이너 한 개를 띄우는 흐름까지 다뤘다면, 이번 글은 그 다음 단계인 멀티 컨테이너 구성을 다룬다. 실제 서비스 한 개를 운영하려면 보통 웹 서버, 백엔드 API, 데이터베이스, 캐시 같은 여러 컨테이너가 함께 동작해야 하는데, 이걸 docker run 명령 여러 줄로 관리하는 일은 곧 한계에 부딪힌다. 이 문제를 정확히 풀어 주는 도구가 Docker Compose이며, 단일 YAML 파일 한 장으로 멀티 컨테이너 환경을 코드처럼 정의하고 한 줄 명령으로 띄우거나 내릴 수 있게 만든다(출처: Docker Compose 공식 문서). 제가 처음 학교 졸업 프로젝트에서 백엔드+DB+Redis 세 컨테이너를 docker run 세 줄로 관리하다 자정에 환경이 꼬여 한 시간을 날려 본 후로는 Compose 없이는 멀티 컨테이너에 손을 대지 않게 되었다.

Compose가 해결하는 문제, 멀티 컨테이너의 어려움
Docker Compose의 등장 배경은 매우 단순하다. 컨테이너 한 개라면 docker run 한 줄로 충분하지만, 둘 이상이 되는 순간 같은 환경을 똑같이 재현하기가 급격히 어려워진다는 점이다. 백엔드 API 컨테이너에 -p 옵션으로 포트를 매핑하고, 환경 변수에 DB 주소를 넣고, 또 다른 명령으로 DB 컨테이너를 띄우고, 같은 네트워크에 묶고, 시작 순서를 맞추는 작업을 한 번 해 보면 이 흐름이 사람이 매번 손으로 할 일이 아니라는 점을 손끝으로 받아들이게 된다.
Compose는 이 모든 정보를 docker-compose.yml이라는 한 장의 선언형 파일에 담는다. 여기서 선언형이란 "어떤 명령을 어떤 순서로 실행하라"가 아니라 "최종 상태가 어떠해야 한다"를 기술하는 방식을 의미하며, 이 흐름은 인프라를 코드로 다루는 IaC(Infrastructure as Code) 사상의 가장 기본 단위이기도 하다. 이 한 장만 있으면 새 팀원이 들어와도 docker compose up 한 줄로 동일한 환경이 재현되며, 운영 환경과 로컬 환경의 차이를 거의 0에 가깝게 줄일 수 있다.
또한 Compose는 단순한 일괄 실행 도구를 넘어 의존성 관리, 가상 네트워크, 볼륨 공유, 헬스체크, 재시작 정책 같은 운영 필수 기능을 표준 문법으로 제공한다. 솔직히 처음에는 "그냥 셸 스크립트로 묶으면 되지 굳이 새 도구를?"이라는 의문이 있었는데, Compose가 표준화한 부분은 단순한 실행이 아니라 "여러 컨테이너가 한 묶음으로 함께 살아가는 라이프사이클"이라는 점을 알고 나서야 그 가치에 비로소 납득이 갔다.
docker-compose.yml 작성법
Compose 파일은 YAML 문법을 따르며, 최상위 키는 보통 services·networks·volumes 셋이다. services 아래에 띄우고 싶은 컨테이너를 키 단위로 나열하고, 각 서비스는 image 또는 build, ports, environment, depends_on, volumes 같은 속성을 가진다. 가장 단순한 예시는 다음과 같다.
services:
web:
build: ./backend
ports:
- "8000:8000"
environment:
DB_HOST: db
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
이 한 장으로 백엔드 API와 PostgreSQL 데이터베이스가 한 묶음으로 관리된다. docker compose up -d를 실행하면 db 컨테이너가 먼저 뜨고 web 컨테이너가 그 뒤에 올라온다. 여기서 depends_on이란 한 서비스가 다른 서비스를 먼저 시작하도록 순서를 강제하는 의존성 선언으로, 컨테이너가 떴는지 여부만 보지 그 안의 애플리케이션이 실제로 준비되었는지까지는 보지 않는다는 한계가 있다. 그래서 실제 운영에서는 healthcheck 옵션을 함께 써서 "DB가 정말 받을 준비가 됐는가"까지 확인한 뒤에 다음 컨테이너를 띄우는 흐름이 권장된다(출처: Docker Compose Spec).
또 한 가지 중요한 사실은 Compose가 같은 파일 안의 서비스들을 자동으로 같은 가상 네트워크에 묶어 준다는 점이다. 즉 web 컨테이너 안에서 http://db:5432처럼 서비스 이름을 호스트명으로 쓰면 그대로 DB 컨테이너에 닿는다. IP 주소를 외우거나 환경 변수에 박지 않아도 되며, 이 자동 DNS 해상도는 Compose의 가장 편리한 기능 중 하나다. 제 경험상 Compose 입문에서 가장 자주 막히는 지점이 바로 "host.docker.internal과 서비스 이름을 헷갈리는 일"인데, 같은 Compose 파일 안에서는 무조건 서비스 이름을 쓴다고 한 줄로 외워두면 사고가 거의 사라진다.
의존성·네트워크·볼륨 실전 예제
실전에서 가장 자주 쓰는 패턴은 백엔드 API + 데이터베이스 + 캐시 + 리버스 프록시 4개 묶음이다. 다음 예제는 Nginx를 프록시로 두고 그 뒤에 FastAPI 백엔드, PostgreSQL DB, Redis 캐시를 묶은 구조이다.
services:
proxy:
image: nginx:alpine
ports: ["80:80"]
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on: [api]
api:
build: ./api
environment:
DATABASE_URL: postgresql://app:pw@db/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: pw
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 5
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
pgdata:
여기서 핵심 포인트는 세 가지다. 첫째, depends_on에 condition: service_healthy를 붙여 DB의 헬스체크가 통과한 뒤에야 API가 뜨도록 만들었다. 헬스체크란 컨테이너가 정상적으로 응답하는지 주기적으로 점검하는 자가 진단 절차로, 컨테이너가 떠 있는 것과 실제로 동작하는 것의 차이를 메워 준다. 둘째, volumes에 이름이 지정된 볼륨(pgdata)을 두어 DB 데이터가 컨테이너를 지웠다 새로 띄워도 그대로 보존되도록 했다. 셋째, proxy는 호스트 측 80번 포트를 외부에 열어 두지만 api·db·cache는 외부 포트를 열지 않아 외부에서 직접 접근할 수 없도록 격리했다. 이 한 장의 파일이 보여 주는 운영 모범은 시간이 지나도 잘 변하지 않는다.
운영 명령은 단순하다. docker compose up -d로 묶음을 백그라운드로 띄우고, docker compose logs -f api로 한 서비스의 로그만 따라가며, docker compose down으로 한 번에 정리한다. 솔직히 이건 예상 밖이었는데, 제가 처음 이 4개 묶음을 신규 노트북에서 띄워 봤을 때 docker compose up 한 줄에서 1분도 안 걸려 모든 서비스가 정상 응답을 시작하는 모습을 본 후로는 "환경 셋업 가이드 문서"라는 길고 지치는 산출물이 거의 사라졌다. 다음 글에서는 이 묶음을 한 단계 더 키워, 여러 호스트에 걸쳐 컨테이너를 분산하는 Kubernetes 기본을 이어서 다룬다.
메타 디스크립션: Docker Compose가 멀티 컨테이너 구성을 어떻게 단순화하는지, docker-compose.yml 작성법과 의존성·네트워크·볼륨 실전 예제까지 한 번에 정리합니다.