도커 중급 강좌 #3 docker compose 기초 — web + db 한 파일로
지금까지의 명령들은 모두 컨테이너 한 개를 다뤘습니다. 하지만 실제 앱은 web + db, 또는 web + db + cache + worker처럼 여러 컨테이너의 묶음입니다. 이걸 한 파일로 정의하고 한 명령으로 실행하는 도구가 Docker Compose 입니다.
도커 중급 강좌 시리즈에서 이번 글의 위치:
- #1 멀티스테이지 빌드와 이미지 슬리밍
- #2 빌드 캐시 — 레이어 순서 최적화
- #3 docker compose 기초 — web + db ← 이번 글
- #4 compose 심화 — depends_on, healthcheck, profiles
- #5 환경변수와 secrets 관리
- #6 로깅과 디버깅
docker-compose와 docker compose
#
처음 보면 헷갈리는 부분부터 보겠습니다. 두 표기가 있습니다.
docker-compose(하이픈) — 옛 v1, Python으로 만들어진 별도 바이너리. deprecated.docker compose(공백) — 현재 v2, Go로 만들어져 도커에 통합. 이게 표준.
이 글에서는 docker compose (v2)로 통일합니다. Docker Desktop에는 기본 포함, 리눅스에선 docker-compose-plugin 패키지가 있습니다.
docker compose version
# Docker Compose version v2.30.x왜 Compose가 필요한가 #
docker run으로 하던 일을 그대로 옮겨보면 곧 한계가 보입니다.
docker network create myapp-net
docker volume create pgdata
docker run -d --name pg \
--network myapp-net \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
docker run -d --name web \
--network myapp-net \
-p 8000:8000 \
-e DB_HOST=pg \
-e DB_PASSWORD=secret \
myapp:latest이 정도가 “최소” 셋업입니다. 환경이 더 늘면 셸 스크립트로 옮기는데, 그래도 어딘가에 변경이 생기면 처음부터 다시 띄우게 되고, 동료에게 셋업을 알려주려면 README가 길어집니다.
같은 일을 Compose로 적으면:
services:
pg:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: myapp:latest
ports:
- "8000:8000"
environment:
DB_HOST: pg
DB_PASSWORD: secret
depends_on:
- pg
volumes:
pgdata:docker compose up 한 명령으로 전체 띄우기. 변경이 있으면 같은 명령으로 변경된 부분만 다시 만듭니다. 동료에게는 “리포지토리 클론하고 docker compose up” 한 줄로 끝납니다.
파일 이름과 위치 #
표준 파일 이름은 compose.yaml(또는 compose.yml)입니다. 옛날에 보던 docker-compose.yml도 여전히 동작하지만, 새 프로젝트라면 compose.yaml을 권장.
compose.yaml # 1순위
compose.yml # 2
docker-compose.yaml # 3
docker-compose.yml # 4 (옛 관습)명령은 파일이 있는 디렉터리에서 그냥 docker compose up — 따로 경로 지정이 없으면 위 순서로 찾습니다. 파일이 다른 위치에 있다면 -f로:
docker compose -f infra/compose.yaml up
docker compose -f compose.yaml -f compose.dev.yaml up # 여러 개 (override)compose.yaml의 큰 그림 #
최상위 키들 한 표.
| 최상위 키 | 의미 |
|---|---|
services | 컨테이너들 (가장 많이 내용을 채우는 영역) |
volumes | 이름 있는 볼륨 |
networks | 이름 있는 네트워크 |
secrets | secret (#5에서) |
configs | 컨테이너에 주입할 설정 파일 |
옛날 파일에서 자주 보는 version: "3.8" 같은 줄은 이제 안 씁니다. Compose v2가 무시합니다. 새 파일에선 빼는 게 깔끔.
실전 예 — Django + Postgres + Redis #
좀 더 현실적인 예를 짜봅니다. 장고 실전 시리즈와 비슷한 구성입니다.
services:
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./:/app
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://app:secret@pg:5432/app
REDIS_URL: redis://redis:6379/0
DEBUG: "1"
depends_on:
- pg
- redis
pg:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432" # 호스트 DB 클라이언트로 접근하려면
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
volumes:
pgdata:
redisdata:각 항목을 풀어 보면:
build: .— 현재 디렉터리의Dockerfile로 빌드. 이미지를 직접 만들 때image: postgres:16— 이미지를 받아 쓸 때command:— 이미지의 기본 CMD를 오버라이드volumes:— bind mount(./:/app)와 named volume(pgdata:/var/...)이 한 항목 안에 섞여 있습니다.:앞이 절대경로/상대경로면 bind, 그냥 이름이면 named.ports:—docker run -p와 같은 형태."127.0.0.1:5432:5432"처럼 호스트 IP도 가능.environment:—-e KEY=val에 해당하는 항목. 매핑 또는 리스트로 적을 수 있음depends_on:— 시작 순서 (#4에서 깊이)
Compose가 자동으로 해주는 것 #
docker run으로 일일이 하던 작업을 Compose가 알아서 해줍니다.
자동 네트워크 #
compose up 첫 실행 시 프로젝트 이름의 기본 네트워크를 만들고 모든 서비스를 그 네트워크에 붙입니다.
docker network ls
# NAME DRIVER SCOPE
# myapp_default bridge local같은 네트워크 안의 서비스는 서비스 이름으로 서로 부를 수 있습니다. 위 예에서 web 컨테이너의 환경변수 DATABASE_URL: postgres://app:secret@pg:5432/app — 호스트가 pg 인데, 이게 그대로 동작합니다. (기초 #4의 사용자 정의 bridge 네트워크 + DNS와 같은 원리.)
프로젝트 네임스페이싱 #
Compose는 모든 리소스에 프로젝트 이름을 prefix로 붙입니다.
docker ps
# NAMES
# myapp-web-1
# myapp-pg-1
# myapp-redis-1기본 프로젝트 이름은 디렉터리 이름. 변경하려면 -p:
docker compose -p myproj up
# myproj-web-1, myproj-pg-1 ...같은 머신에서 같은 compose 파일을 다른 이름으로 두 벌 띄울 수 있습니다. 테스트용 / 개발용 환경을 동시에 굴릴 때 유용합니다.
일상 명령군 #
docker compose up # 포그라운드 + 모든 로그를 한곳에서
docker compose up -d # 백그라운드 (detached)
docker compose up --build # 빌드부터 다시 (코드 변경 후)
docker compose up web # 특정 서비스만
docker compose up --remove-orphans # compose.yaml에서 빠진 옛 서비스도 정리docker compose down # 컨테이너 + 네트워크 제거
docker compose down -v # 위 + 볼륨까지 (데이터 사라짐)
docker compose down --rmi local # 위 + 빌드된 이미지까지docker compose ps # 이 프로젝트의 컨테이너만
docker compose ps -a # 종료된 것까지docker compose logs # 모든 서비스 로그를 한곳에서
docker compose logs -f # follow
docker compose logs -f web # 특정 서비스만
docker compose logs --since 10m # 최근 10분docker compose exec web bash # 떠 있는 web 안에서 bash
docker compose run --rm web python manage.py migrate # 새 컨테이너에서 일회성 명령exec와 run의 차이는 기초 #3의 docker exec vs docker run 차이와 같습니다. 마이그레이션 / 시드 같은 일회성 작업은 run --rm, 떠 있는 컨테이너 디버깅은 exec.
docker compose restart web # 한 서비스 재시작
docker compose stop # 멈추되 컨테이너는 보존 (down과 다름)
docker compose start # 멈춘 것 다시 시작up vs start vs restart
#
세 명령이 비슷해 보이지만 의미가 다릅니다.
up— 컨테이너를 만들거나(없으면), 변경을 반영해 다시 만든다(있어도 정의가 바뀌었으면)start— 멈춘(기존) 컨테이너를 다시 띄운다. 정의 변경은 반영 안 함restart—stop후start. 정의 변경 반영 안 함
compose.yaml을 고친 뒤에는 항상 up입니다.
build: 깊이
#
서비스에서 이미지를 직접 빌드할 때:
services:
web:
build:
context: . # 빌드 컨텍스트 (Dockerfile이 있는 곳)
dockerfile: Dockerfile.dev # 기본이 아닌 다른 파일을 쓸 때
args:
APP_VERSION: "1.0.0"
target: dev # 멀티스테이지의 특정 스테이지
cache_from:
- myapp:cache
image: myapp:dev # 빌드 결과에 이 이름을 붙임image:와 build:를 같이 적으면, 빌드한 결과물에 그 이름을 붙입니다. 그러면 docker compose push로 그 이미지를 레지스트리에 올릴 수도 있습니다.
volumes: 정리
#
서비스의 volumes:는 세 형태가 자주 보입니다.
services:
web:
volumes:
- ./:/app # bind (상대경로)
- /Users/me/data:/app/data # bind (절대경로)
- pgdata:/var/lib/postgresql/data # named (최상위 volumes에서 정의)
- logs:/app/logs:ro # named, read-only
- type: bind
source: ./config
target: /etc/myapp
read_only: true # 긴 형식
volumes:
pgdata:
logs:짧은 형식이 일상에선 충분하고, 읽기 전용 / 권한 / 옵션이 더 필요할 때 긴 형식으로 펼쳐 적습니다.
networks: — 사용자 정의
#
기본 네트워크 외에 사용자 정의 네트워크를 만들 수도 있습니다. 한 프로젝트 안에서 일부 서비스만 같은 네트워크에 두고 싶을 때.
services:
web:
networks:
- frontend
- backend
pg:
networks:
- backend
nginx:
networks:
- frontend
networks:
frontend:
backend:pg는 backend에만 있어 nginx가 직접 부를 수 없습니다. 보안 격리에 유용한 패턴입니다. 다만 일반 앱에선 기본 네트워크 한 개로도 충분합니다.
자주 만나는 함정 #
- 로그가 안 보임 — 앱이
127.0.0.1에 바인딩했거나, 파일에 로그를 쓰는 중.0.0.0.0바인딩 + stdout 출력으로. - DB가 다 떴는데 web이 connection refused —
depends_on은 컨테이너 시작 순서만 본다. DB가 준비됐는지는 안 본다. (#4의 healthcheck로 푼다.) - bind mount 한 코드를 컨테이너가 못 봄 — Docker Desktop의 파일 공유 설정에서 해당 디렉터리가 빠져 있을 수 있음.
Settings → Resources → File Sharing점검. compose down으로 DB 데이터가 사라짐 —-v를 같이 쓰면 named volume까지 지워집니다. 평상시엔-v없이.build:했는데 변경이 반영 안 됨 —up --build또는up -d --build로.
한 사이클 #
새 프로젝트의 일상:
# 처음
docker compose up -d --build
# 코드 고쳤음 (bind mount 라면 자동 반영, dev 서버라면 자동 재시작)
docker compose logs -f web
# 마이그레이션
docker compose run --rm web python manage.py migrate
# DB 셸 진입
docker compose exec pg psql -U app
# 하루 끝
docker compose down
# 처음부터 다시 (DB 데이터까지 날리고)
docker compose down -v
docker compose up -d --build이 정도가 손에 붙으면 한 프로젝트의 인프라 셋업이 한 파일 + 몇 줄 명령으로 정리됩니다.
정리 #
이번 글에서 잡은 그림:
docker compose(v2)가 표준.docker-compose(v1)는 deprecatedcompose.yaml한 파일에services/volumes/networks를 정의 — 한 명령으로 띄우기- 서비스끼리는 서비스 이름이 DNS처럼 동작 (자동 네트워크 + 사용자 정의 bridge)
- **
build:+image:**로 빌드와 태그를 한곳에서 - 일상 명령:
up -d,down,logs -f,exec,run --rm up은 변경 반영,start는 단순 시작 —compose.yaml을 고쳤으면 항상up
다음 글(#4 compose 심화 — depends_on, healthcheck, profiles)에서는 이 골격에 운영 도구를 얹습니다. healthcheck로 “DB가 진짜 준비되었는가” 를 보고, depends_on의 condition으로 의미 있는 시작 순서를 잡고, **profiles**로 dev / test / prod를 한 파일 안에서 분기하는 방법입니다.