도커 중급 강좌 #3 docker compose 기초 — web + db 한 파일로

7 분 소요

지금까지의 명령들은 모두 컨테이너 한 개를 다뤘습니다. 하지만 실제 앱은 web + db, 또는 web + db + cache + worker처럼 여러 컨테이너의 묶음입니다. 이걸 한 파일로 정의하고 한 명령으로 실행하는 도구가 Docker Compose 입니다.

도커 중급 강좌 시리즈에서 이번 글의 위치:

docker-composedocker 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으로 하던 일을 그대로 옮겨보면 곧 한계가 보입니다.

run으로 web + db 띄우기
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로 적으면:

compose.yaml
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이름 있는 네트워크
secretssecret (#5에서)
configs컨테이너에 주입할 설정 파일

옛날 파일에서 자주 보는 version: "3.8" 같은 줄은 이제 안 씁니다. Compose v2가 무시합니다. 새 파일에선 빼는 게 깔끔.

실전 예 — Django + Postgres + Redis #

좀 더 현실적인 예를 짜봅니다. 장고 실전 시리즈와 비슷한 구성입니다.

compose.yaml
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 파일을 다른 이름으로 두 벌 띄울 수 있습니다. 테스트용 / 개발용 환경을 동시에 굴릴 때 유용합니다.

일상 명령군 #

up — 띄우기
docker compose up                # 포그라운드 + 모든 로그를 한곳에서
docker compose up -d             # 백그라운드 (detached)
docker compose up --build        # 빌드부터 다시 (코드 변경 후)
docker compose up web            # 특정 서비스만
docker compose up --remove-orphans   # compose.yaml에서 빠진 옛 서비스도 정리
down — 내리기
docker compose down              # 컨테이너 + 네트워크 제거
docker compose down -v           # 위 + 볼륨까지 (데이터 사라짐)
docker compose down --rmi local  # 위 + 빌드된 이미지까지
ps — 상태
docker compose ps                # 이 프로젝트의 컨테이너만
docker compose ps -a             # 종료된 것까지
logs — 로그
docker compose logs              # 모든 서비스 로그를 한곳에서
docker compose logs -f           # follow
docker compose logs -f web       # 특정 서비스만
docker compose logs --since 10m  # 최근 10분
exec / run — 안에 들어가기
docker compose exec web bash     # 떠 있는 web 안에서 bash
docker compose run --rm web python manage.py migrate   # 새 컨테이너에서 일회성 명령

execrun의 차이는 기초 #3docker exec vs docker run 차이와 같습니다. 마이그레이션 / 시드 같은 일회성 작업은 run --rm, 떠 있는 컨테이너 디버깅은 exec.

restart / stop / start
docker compose restart web       # 한 서비스 재시작
docker compose stop              # 멈추되 컨테이너는 보존 (down과 다름)
docker compose start             # 멈춘 것 다시 시작

up vs start vs restart #

세 명령이 비슷해 보이지만 의미가 다릅니다.

  • up — 컨테이너를 만들거나(없으면), 변경을 반영해 다시 만든다(있어도 정의가 바뀌었으면)
  • start — 멈춘(기존) 컨테이너를 다시 띄운다. 정의 변경은 반영 안 함
  • restartstopstart. 정의 변경 반영 안 함

compose.yaml을 고친 뒤에는 항상 up입니다.

build: 깊이 #

서비스에서 이미지를 직접 빌드할 때:

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:는 세 형태가 자주 보입니다.

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:

pgbackend에만 있어 nginx가 직접 부를 수 없습니다. 보안 격리에 유용한 패턴입니다. 다만 일반 앱에선 기본 네트워크 한 개로도 충분합니다.

자주 만나는 함정 #

  • 로그가 안 보임 — 앱이 127.0.0.1에 바인딩했거나, 파일에 로그를 쓰는 중. 0.0.0.0 바인딩 + stdout 출력으로.
  • DB가 다 떴는데 web이 connection refuseddepends_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)는 deprecated
  • compose.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를 한 파일 안에서 분기하는 방법입니다.

X