Table of contents
- Compose가 하는 일
- 설치 확인
- 최소한의 compose.yaml
- 세 가지 축 — services, networks, volumes
- depends_on과 healthcheck — 시작 순서의 진실
- 네트워크 — 분리와 노출
- 볼륨 — named vs bind
- .env와 변수 치환
- profiles — 상황별로만 켜기
- 환경별 override
- 자주 쓰는 명령
- 여기까지의 상태
Compose가 하는 일
먼저 Compose가 docker compose up 한 번에 무슨 일을 벌이는지 전체 흐름부터 보자.
flowchart LR
CMD["docker compose up"] --> PARSER["compose.yaml 파싱"]
PARSER --> NET["default 네트워크 생성"]
PARSER --> VOL["볼륨 생성"]
PARSER --> BUILD["이미지 빌드 / pull"]
BUILD --> RUN1["web 컨테이너"]
BUILD --> RUN2["db 컨테이너"]
BUILD --> RUN3["cache 컨테이너"]
NET --> RUN1
NET --> RUN2
NET --> RUN3
VOL --> RUN2
RUN2 -.->|"healthcheck OK"| RUN1
핵심은 두 가지다. 첫째, 서비스들은 자동으로 같은 네트워크에 들어가서 서비스 이름으로 서로를 부를 수 있다. 둘째, 의존성과 헬스체크를 선언해두면 시작 순서를 Compose가 대신 맞춰준다.
설치 확인
최신 Docker Desktop이나 Docker Engine에는 Compose v2가 플러그인으로 내장되어 있다. 예전 docker-compose(하이픈)가 아니라 docker compose(공백)를 쓴다.
docker compose version
버전이 찍히면 준비 끝이다. docker-compose 명령을 여전히 쓰는 팀이 있다면 v1이라 EOL된 버전이니 v2로 올리는 걸 권한다.
최소한의 compose.yaml
파일 이름은 compose.yaml이 공식 권장이다. 예전에는 docker-compose.yml이었는데 지금은 둘 다 지원한다. 가장 작은 구성을 보자.
# compose.yaml
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
services 아래에 web이라는 서비스 이름을 하나 정의했다. 이미지는 nginx, 호스트 8080을 컨테이너 80으로 연결했다. 이 파일이 있는 디렉토리에서 기동한다.
docker compose up -d
-d는 detached, 즉 백그라운드 실행이다. 브라우저로 http://localhost:8080을 열면 Nginx 기본 페이지가 뜬다. 내릴 때는 docker compose down이다.
세 가지 축 — services, networks, volumes
Compose 파일은 크게 세 가지 톱 레벨 키로 구성된다. 이 셋만 이해하면 나머지는 디테일이다.
services: # 컨테이너 하나하나의 정의
...
networks: # 서비스끼리 붙일 네트워크
...
volumes: # 데이터 영속화에 쓸 볼륨
...
간단한 웹+DB+캐시 구성을 예로 본다.
services:
web:
image: myapp:latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD", "pg_isready", "-U", "app"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7-alpine
networks:
- backend
networks:
backend:
volumes:
db-data:
가볍게 읽어보면 이야기가 보인다. web은 3000번 포트를 열고, DB와 캐시에 의존한다. db는 postgres 이미지에 볼륨을 붙여서 데이터가 재시작해도 살아남게 했다. 셋 다 backend라는 같은 네트워크에 들어간다.
여기서 주목할 건 environment에 있는 db:5432와 cache:6379다. 호스트 이름 자리에 서비스 이름을 그대로 썼다. Compose가 만들어주는 네트워크 안에서는 서비스 이름이 DNS 이름처럼 동작한다. IP를 알 필요가 없다.
depends_on과 healthcheck — 시작 순서의 진실
처음 Compose를 쓰면 depends_on이 시작 순서를 보장해준다고 착각하기 쉽다. 정확히는 컨테이너가 시작되는 순서만 보장한다. “DB 프로세스가 쿼리를 받을 준비가 됐는지”는 보장하지 않는다.
그래서 condition을 붙인다.
sequenceDiagram
participant Compose
participant DB
participant Web
Compose->>DB: 컨테이너 시작
DB-->>Compose: started
loop 5초 간격 healthcheck
Compose->>DB: pg_isready
DB-->>Compose: ok?
end
DB-->>Compose: healthy
Compose->>Web: 컨테이너 시작 (의존 충족)
service_started: 컨테이너가 시작만 됐으면 통과 (기본값)service_healthy:healthcheck가 통과해야 의존이 충족됨service_completed_successfully: 한 번 실행되고 정상 종료된 작업(마이그레이션 같은 것)에 씀
DB 같이 준비 시간이 필요한 서비스는 반드시 healthcheck를 정의하고 service_healthy로 묶는다. 안 그러면 웹이 먼저 뜨고 DB 연결 실패로 재시작 루프에 빠진다.
네트워크 — 분리와 노출
한 네트워크에 다 넣어도 되지만, 규모가 커지면 분리하는 게 안전하다. 예를 들어 DB는 외부에서 접근 불가능해야 하고, 웹만 리버스 프록시 네트워크에 노출돼야 한다면 이렇게 쓴다.
services:
proxy:
image: traefik:v3
networks:
- edge
web:
image: myapp:latest
networks:
- edge # proxy와 통신
- backend # db와 통신
db:
image: postgres:16-alpine
networks:
- backend # 오직 backend에만
networks:
edge:
backend:
internal: true # 외부 인터넷 접근 차단
internal: true를 쓰면 해당 네트워크는 외부 라우팅이 끊긴다. DB가 인터넷으로 뭔가 당겨올 일은 없으니 기본값으로 걸어두는 게 안전하다.
볼륨 — named vs bind
볼륨은 두 가지 방식이 있다. 쓰임새가 다르다.
services:
db:
image: postgres:16-alpine
volumes:
- db-data:/var/lib/postgresql/data # named volume
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro # bind mount
volumes:
db-data:
- Named volume (
db-data:...): Docker가 관리하는 스토리지. 운영 환경의 데이터 영속화에 쓴다. 위치가/var/lib/docker/volumes/...밑 어딘가라 호스트 FS 구조와 무관 - Bind mount (
./init.sql:...): 호스트 경로를 그대로 마운트. 개발 중 코드 핫리로드, 초기화 스크립트 주입, 설정 파일 공유에 유용.:ro는 read-only
개발 환경에서는 소스 코드를 bind mount로 넣어서 파일 수정이 즉시 반영되게 하고, 데이터는 named volume으로 관리한다.
.env와 변수 치환
비밀번호나 환경별 값을 파일에 박아두면 안 된다. .env 파일을 같은 디렉토리에 놓으면 Compose가 자동으로 읽는다.
# .env
POSTGRES_PASSWORD=secret
APP_PORT=3000
services:
web:
image: myapp:latest
ports:
- "${APP_PORT}:3000"
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
.env는 .gitignore에 반드시 넣는다. 커밋 실수 한 번이면 비밀번호가 원격 리포로 가버린다.
profiles — 상황별로만 켜기
개발자는 앱+DB만 띄우면 되지만, QA 환경에서는 여기에 Kafka, Mailhog 같은 게 더 필요할 수 있다. 서비스마다 profiles를 붙이면 원하는 상황에서만 뜨게 할 수 있다.
services:
web:
image: myapp:latest
db:
image: postgres:16-alpine
mailhog:
image: mailhog/mailhog
profiles: ["dev"]
kafka:
image: confluentinc/cp-kafka:7.6.0
profiles: ["qa"]
기본 기동은 web과 db만 뜬다.
docker compose up -d # web, db만
docker compose --profile dev up -d # + mailhog
docker compose --profile qa up -d # + kafka
옵션 없이 up을 하면 프로파일이 지정된 서비스는 건드리지 않는다. 필요할 때만 불러서 쓰는 구조다.
환경별 override
개발/스테이징/프로덕션에서 같은 기본 설정을 쓰되 일부만 덮어쓰고 싶을 때가 많다. Compose는 compose.yaml + compose.override.yaml 병합을 기본값으로 한다. 여러 파일을 명시적으로 합칠 수도 있다.
# compose.yaml — 공통
services:
web:
image: myapp:latest
environment:
LOG_LEVEL: info
# compose.dev.yaml — 개발용 덮어쓰기
services:
web:
build: .
volumes:
- ./src:/app/src
environment:
LOG_LEVEL: debug
ports:
- "3000:3000"
# compose.prod.yaml — 프로덕션용 덮어쓰기
services:
web:
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
실행할 때 -f로 여러 파일을 나열하면 순서대로 덮어쓴다.
docker compose -f compose.yaml -f compose.dev.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml up -d
같은 서비스 키의 중복된 설정은 뒤 파일이 이긴다. 배열은 기본적으로 덮어쓰기인데, 환경 변수 같은 맵은 병합된다는 점만 기억해두면 충돌이 날 일은 거의 없다.
자주 쓰는 명령
Compose를 쓰다 보면 다음 명령을 수백 번 친다.
# 전체 기동 / 중지
docker compose up -d
docker compose down
# 볼륨까지 같이 삭제 (데이터 날아감 주의)
docker compose down -v
# 특정 서비스만
docker compose up -d web
docker compose restart web
# 로그 보기
docker compose logs -f web
# 컨테이너 안으로
docker compose exec web sh
# 이미지 재빌드하며 기동
docker compose up -d --build
# 현재 정의된 최종 구성 확인 (override 병합 결과)
docker compose config
마지막 config는 자주 까먹는데 디버깅에 매우 유용하다. 여러 파일을 합쳤을 때 최종적으로 뭐가 될지 미리 볼 수 있다.
여기까지의 상태
Compose 파일 하나로 웹 + DB + 캐시를 한 번에 띄우고, 네트워크를 분리하고, 환경별 override까지 구성하는 방법을 훑었다. 실무에서는 여기에 모니터링(Prometheus, Grafana)이나 메시지 브로커(Kafka, RabbitMQ)가 더 붙을 뿐, 뼈대는 똑같다.
다음 편에서는 이미지 크기를 줄이고 빌드 속도를 끌어올리는 멀티스테이지 빌드를 다룬다. 빌드 산출물과 런타임을 분리해서 최종 이미지가 얼마나 작아질 수 있는지 직접 보여준다.

Loading comments...