웹·DB·캐시를 각각 docker run으로 띄우면 컨테이너마다 긴 명령어를 외워야 하고, 네트워크를 손으로 만들어 연결해야 한다. 더 흔한 문제는 DB가 아직 안 떴는데 앱이 먼저 떠서 접속 에러로 죽는 것이다. Docker Compose는 스택 전체를 YAML 한 장에 선언하고 한 명령으로 띄운다.
멀티 컨테이너에서 실제로 발목을 잡는 건 서비스 시작 순서다. 헬스체크로 DB가 준비된 다음에 앱을 띄우는 스택을 만들고 docker compose up을 돌려 그 순서가 지켜지는지 로그로 확인한다. 출력은 Docker Compose v5.1.1에서 돌린 결과다.
하나의 스택, 하나의 파일
다음은 nginx(앱), PostgreSQL(DB), Redis(캐시)로 이루어진 스택이다. db 서비스의 healthcheck와, api가 그 헬스 상태를 기다리는 depends_on ... condition: service_healthy가 핵심이다.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
interval: 3s
timeout: 3s
retries: 10
cache:
image: redis:7-alpine
command: redis-server --save "" --appendonly no
api:
image: nginx:1.27-alpine
depends_on:
db:
condition: service_healthy # db가 "건강"해질 때까지 대기
cache:
condition: service_started # cache는 시작만 하면 됨
ports:
- "8088:80"
volumes:
db-data:
서비스 정의에 들어간 옵션의 역할은 다음과 같다.
depends_on+condition: 시작 순서를 보장한다.service_started는 컨테이너가 뜨기만 하면 되고,service_healthy는 헬스체크를 통과해야 다음 단계로 넘어간다.healthcheck: 컨테이너가 떴다고 해서 그 안의 프로세스가 요청을 받을 준비가 된 건 아니다.pg_isready로 Postgres가 연결을 받는지 주기적으로 확인한다.volumes:db-data에 DB 파일을 저장해 컨테이너를 지워도 데이터가 남는다.ports: 호스트의 8088을 컨테이너의 80에 연결한다.
db와 cache에는 포트를 호스트로 노출하지 않았다. Compose가 만든 기본 네트워크 안에서 api가 서비스 이름으로 직접 접근하므로 외부로 열 필요가 없다.
시작 순서 확인
docker compose up -d
이미지를 받은 뒤 컨테이너를 만드는 마지막 로그가 시작 순서를 보여준다.
Container composedemo-cache-1 Started
Container composedemo-db-1 Started
Container composedemo-db-1 Waiting
Container composedemo-db-1 Healthy
Container composedemo-api-1 Starting
Container composedemo-api-1 Started
db가 Started 된 직후 Waiting으로 들어가고, 헬스체크를 통과해 Healthy가 된 뒤에야 api가 Starting한다. 선언한 대로 DB가 준비된 다음에 앱이 뜬 것이다. depends_on만 쓰고 condition을 빼면 Compose는 컨테이너가 뜨는 것까지만 기다리고 헬스는 기다리지 않으므로 Waiting → Healthy 단계가 사라진다.
docker compose ps로 상태를 보면 db가 (healthy)로 표시된다.
NAME IMAGE SERVICE STATUS
composedemo-api-1 nginx:1.27-alpine api Up 11 seconds
composedemo-cache-1 redis:7-alpine cache Up 22 seconds
composedemo-db-1 postgres:16-alpine db Up 22 seconds (healthy)
서비스 이름이 곧 호스트 이름이다
같은 Compose 파일의 서비스들은 자동으로 만들어진 네트워크에 묶이고, 서로를 서비스 이름으로 찾는다. api 컨테이너 안에서 이름을 조회하면 Compose의 내장 DNS가 각 서비스를 컨테이너 IP로 풀어 준다.
docker compose exec api sh -c "getent hosts db; getent hosts cache"
172.18.0.2 db db
172.18.0.3 cache cache
그래서 애플리케이션 설정에는 IP가 아니라 db:5432, cache:6379처럼 서비스 이름을 쓴다. 컨테이너 IP는 재시작마다 바뀌지만 서비스 이름은 변하지 않는다.
호스트로 노출한 포트도 동작한다. api의 80을 8088로 매핑했으므로 호스트에서 바로 접근된다.
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8088/
# HTTP 200
다 쓴 스택은 한 명령으로 정리한다. -v는 볼륨까지 같이 지운다.
docker compose down -v
Container composedemo-api-1 Removed
Container composedemo-db-1 Removed
Volume composedemo_db-data Removed
Network composedemo_default Removed
환경별로 설정 나누기
같은 스택이라도 개발과 운영에서 달라지는 부분이 있다. Compose는 여러 파일을 겹쳐 쓸 수 있어, 공통 설정 위에 환경별 오버라이드를 올린다. 뒤에 지정한 파일이 앞 파일을 덮어쓴다.
# docker-compose.override.yml — 개발용 (소스 마운트, 디버그 포트)
services:
api:
volumes:
- ./src:/app
ports:
- "9229:9229"
environment:
- NODE_ENV=development
# 개발: base + 개발 오버라이드
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# 운영: base + 운영 오버라이드, 백그라운드
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
파일 이름이 docker-compose.override.yml이면 docker compose up만 해도 base에 자동으로 겹쳐진다.
자주 쓰는 명령어
| 명령어 | 설명 |
|---|---|
docker compose up -d | 백그라운드로 스택 시작 |
docker compose up --build | 이미지를 다시 빌드한 뒤 시작 |
docker compose ps | 서비스 상태 확인 (헬스 포함) |
docker compose logs -f api | 특정 서비스 로그 실시간 추적 |
docker compose exec db psql -U postgres -d app | 컨테이너 안에서 명령 실행 |
docker compose config | 최종 병합된 설정 검증·출력 |
docker compose down -v | 스택 중지 후 볼륨까지 삭제 |
설정이 의도대로 병합됐는지 의심스러우면 docker compose config로 최종 결과를 확인한다. 시작 실패는 대부분 의존 서비스가 준비되기 전에 떠서 생기므로, healthcheck + condition: service_healthy 조합으로 해결한다.