서론: Docker Compose의 필요성

실제 애플리케이션은 대부분 여러 서비스로 구성됩니다. 웹 서버, 데이터베이스, 캐시, 메시지 큐 등 다양한 컴포넌트가 함께 작동해야 합니다. 이런 멀티 컨테이너 환경을 개별 docker run 명령으로 관리하는 것은 번거롭고 오류가 발생하기 쉽습니다.

Docker Compose는 이 문제를 해결하는 도구로, YAML 파일 하나로 여러 컨테이너를 정의하고 한 번의 명령으로 전체 애플리케이션 스택을 관리할 수 있게 해줍니다.

1. Docker Compose란?

1.1 Docker Compose의 정의

Docker Compose는 멀티 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다. docker-compose.yml 파일에 애플리케이션의 서비스, 네트워크, 볼륨을 선언적으로 정의합니다.

Docker Compose의 주요 특징:

  • 선언적 정의: YAML 파일로 전체 애플리케이션 스택을 정의
  • 단일 명령 관리: docker compose up으로 모든 서비스 시작
  • 환경 격리: 프로젝트별로 독립적인 네트워크 생성
  • 변수 지원: 환경 변수와 .env 파일로 유연한 설정
  • 개발 편의성: 로컬 개발 환경 구성에 특히 유용

1.2 Docker Compose V2

Docker Desktop과 최신 Docker Engine에는 Compose V2가 기본 포함됩니다. V2는 Go로 재작성되어 docker compose (하이픈 없음) 형식으로 사용합니다.

# V1 (레거시)
docker-compose up

# V2 (권장)
docker compose up

2. docker-compose.yml 구조

2.1 기본 구조

# docker-compose.yml
version: "3.9"  # Compose 파일 버전 (선택적)

services:       # 서비스(컨테이너) 정의
  web:
    image: nginx:alpine
    ports:
      - "80:80"

  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret

volumes:        # 볼륨 정의 (선택적)
  db-data:

networks:       # 네트워크 정의 (선택적)
  backend:

2.2 최소 구성 예시

# 가장 간단한 docker-compose.yml
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

3. 서비스 정의 상세

3.1 image - 이미지 지정

services:
  web:
    # Docker Hub 이미지
    image: nginx:alpine

  db:
    # 레지스트리 이미지
    image: registry.example.com/mydb:1.0

  app:
    # 특정 다이제스트
    image: myapp@sha256:abc123...

3.2 build - 이미지 빌드

services:
  app:
    # 간단한 빌드
    build: .

  api:
    # 상세 빌드 옵션
    build:
      context: ./api
      dockerfile: Dockerfile.prod
      args:
        VERSION: "2.0"
        BUILD_ENV: production
      target: production  # 멀티스테이지 빌드 타겟

  frontend:
    # 빌드 후 이미지 태그 지정
    build: ./frontend
    image: myapp/frontend:latest

3.3 ports - 포트 매핑

services:
  web:
    image: nginx
    ports:
      # 호스트:컨테이너
      - "80:80"
      - "443:443"

      # 호스트 포트 자동 할당
      - "80"

      # 특정 IP에만 바인딩
      - "127.0.0.1:3000:3000"

      # UDP 포트
      - "53:53/udp"

      # 긴 형식
      - target: 80
        published: 8080
        protocol: tcp
        mode: host

3.4 volumes - 볼륨 마운트

services:
  db:
    image: postgres:15
    volumes:
      # 이름 있는 볼륨 (Named Volume)
      - db-data:/var/lib/postgresql/data

      # 바인드 마운트 (호스트 경로)
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

      # 읽기 전용 마운트
      - ./config:/etc/app/config:ro

      # 긴 형식
      - type: volume
        source: db-data
        target: /var/lib/postgresql/data
        volume:
          nocopy: true

volumes:
  db-data:  # 볼륨 선언
  cache-data:
    driver: local

3.5 environment - 환경 변수

services:
  app:
    image: myapp
    environment:
      # Map 형식
      NODE_ENV: production
      DEBUG: "false"
      DATABASE_URL: postgres://user:pass@db:5432/mydb

  db:
    image: postgres
    environment:
      # Array 형식
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=myapp

  worker:
    image: myworker
    # 파일에서 환경 변수 로드
    env_file:
      - .env
      - .env.local

3.6 command와 entrypoint

services:
  app:
    image: node:20
    # 기본 명령 덮어쓰기
    command: npm run dev

  worker:
    image: python:3.11
    # 배열 형식
    command: ["python", "-m", "celery", "worker"]

  custom:
    image: alpine
    # 엔트리포인트 덮어쓰기
    entrypoint: /custom-entrypoint.sh
    command: ["--config", "/etc/app/config.yml"]

4. 의존성 관리 (depends_on)

4.1 기본 의존성

services:
  web:
    image: nginx
    depends_on:
      - api
      - db

  api:
    image: myapi
    depends_on:
      - db
      - redis

  db:
    image: postgres:15

  redis:
    image: redis:alpine

4.2 조건부 의존성 (service_healthy)

services:
  web:
    image: nginx
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:15
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

중요: depends_on은 시작 순서만 보장하며, 서비스가 "준비됨" 상태인지는 보장하지 않습니다. 프로덕션에서는 healthcheck와 함께 condition: service_healthy를 사용하세요.

5. 네트워크 설정

5.1 기본 네트워크 동작

Docker Compose는 프로젝트에 대해 기본 네트워크를 자동 생성합니다. 같은 네트워크의 서비스는 서비스 이름으로 서로 통신할 수 있습니다.

services:
  web:
    image: nginx
    # db 서비스에 "db" 호스트명으로 접근 가능

  db:
    image: postgres
    # web 서비스에 "web" 호스트명으로 접근 가능

5.2 커스텀 네트워크

services:
  frontend:
    image: nginx
    networks:
      - frontend-net

  api:
    image: myapi
    networks:
      - frontend-net
      - backend-net

  db:
    image: postgres
    networks:
      - backend-net

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge
    internal: true  # 외부 접근 차단

5.3 네트워크 별칭과 고정 IP

services:
  db:
    image: postgres
    networks:
      backend:
        aliases:
          - database
          - postgres-primary
        ipv4_address: 172.28.0.10

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

6. 실전 예제: WordPress + MySQL

# wordpress/docker-compose.yml
services:
  wordpress:
    image: wordpress:latest
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress_password
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wordpress-data:/var/www/html
    depends_on:
      db:
        condition: service_healthy
    networks:
      - wordpress-net

  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress_password
      MYSQL_ROOT_PASSWORD: root_password
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - wordpress-net

volumes:
  wordpress-data:
  db-data:

networks:
  wordpress-net:
    driver: bridge

7. 실전 예제: Node.js + MongoDB + Redis

# nodejs-stack/docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      MONGODB_URI: mongodb://mongo:27017/myapp
      REDIS_URL: redis://redis:6379
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      mongo:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-net
    command: npm run dev

  mongo:
    image: mongo:7
    restart: unless-stopped
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: secret
      MONGO_INITDB_DATABASE: myapp
    volumes:
      - mongo-data:/data/db
      - ./mongo-init.js:/docker-entrypoint-initdb.d/init.js:ro
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - app-net

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - app-net

  mongo-express:
    image: mongo-express
    restart: unless-stopped
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: admin
      ME_CONFIG_MONGODB_ADMINPASSWORD: secret
      ME_CONFIG_MONGODB_URL: mongodb://admin:secret@mongo:27017/
      ME_CONFIG_BASICAUTH: "false"
    depends_on:
      - mongo
    networks:
      - app-net

volumes:
  mongo-data:
  redis-data:

networks:
  app-net:
    driver: bridge

8. Docker Compose 명령어

8.1 기본 명령어

# 서비스 시작 (백그라운드)
docker compose up -d

# 서비스 시작 (포그라운드, 로그 출력)
docker compose up

# 이미지 빌드 후 시작
docker compose up --build

# 특정 서비스만 시작
docker compose up -d web db

# 서비스 중지 및 제거
docker compose down

# 볼륨도 함께 제거
docker compose down -v

# 이미지도 함께 제거
docker compose down --rmi all

8.2 상태 확인 명령어

# 실행 중인 서비스 목록
docker compose ps

# 모든 서비스 (중지된 것 포함)
docker compose ps -a

# 서비스 로그 확인
docker compose logs

# 특정 서비스 로그
docker compose logs web

# 실시간 로그 추적
docker compose logs -f

# 최근 100줄만
docker compose logs --tail=100

# 타임스탬프 포함
docker compose logs -t

8.3 서비스 관리 명령어

# 서비스 재시작
docker compose restart
docker compose restart web

# 서비스 중지 (컨테이너 유지)
docker compose stop

# 중지된 서비스 시작
docker compose start

# 서비스 스케일링
docker compose up -d --scale web=3

# 설정 변경 적용
docker compose up -d --force-recreate

8.4 컨테이너 내 명령 실행 (exec)

# 컨테이너에서 명령 실행
docker compose exec web sh
docker compose exec db psql -U postgres

# 특정 사용자로 실행
docker compose exec --user root web sh

# 환경 변수 설정
docker compose exec -e DEBUG=true app npm test

# 새 컨테이너로 실행 (run)
docker compose run --rm app npm test
docker compose run --rm db psql -U postgres

9. 환경별 설정 분리

9.1 .env 파일 사용

# .env 파일
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret123
APP_PORT=3000
NODE_ENV=development
# docker-compose.yml
services:
  app:
    ports:
      - "${APP_PORT}:3000"
    environment:
      NODE_ENV: ${NODE_ENV}

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

9.2 여러 Compose 파일 사용

# docker-compose.yml (기본 설정)
services:
  app:
    build: .
    environment:
      NODE_ENV: production

  db:
    image: postgres:15
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
# docker-compose.override.yml (개발 환경, 자동 적용)
services:
  app:
    build:
      context: .
      target: development
    volumes:
      - .:/app
    environment:
      NODE_ENV: development
    ports:
      - "3000:3000"

  db:
    ports:
      - "5432:5432"
# docker-compose.prod.yml (프로덕션 환경)
services:
  app:
    restart: always
    deploy:
      replicas: 3
    environment:
      NODE_ENV: production

  db:
    restart: always
# 개발 환경 (기본 + override 자동 병합)
docker compose up

# 프로덕션 환경
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# 여러 파일 명시적 지정
docker compose -f docker-compose.yml -f docker-compose.override.yml up

9.3 프로필 사용 (선택적 서비스)

services:
  app:
    image: myapp

  db:
    image: postgres:15

  # 개발 환경에서만 사용
  adminer:
    image: adminer
    profiles:
      - debug
    ports:
      - "8080:8080"

  # 테스트 환경에서만 사용
  test-runner:
    image: myapp-test
    profiles:
      - test
# 기본 서비스만 시작
docker compose up

# debug 프로필 포함
docker compose --profile debug up

# 여러 프로필
docker compose --profile debug --profile test up

10. 유용한 설정 옵션

10.1 restart 정책

services:
  app:
    image: myapp
    restart: "no"           # 재시작 안 함 (기본값)
    # restart: always       # 항상 재시작
    # restart: on-failure   # 실패 시에만 재시작
    # restart: unless-stopped # 수동 중지 전까지 재시작

10.2 리소스 제한

services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M

10.3 로깅 설정

services:
  app:
    image: myapp
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

결론

Docker Compose는 멀티 컨테이너 애플리케이션 관리를 위한 필수 도구입니다. 이 글에서 다룬 내용을 정리하면:

  • docker-compose.yml 구조: services, volumes, networks의 역할과 정의 방법
  • 서비스 정의: image, build, ports, volumes, environment 등 핵심 설정
  • 의존성 관리: depends_on과 healthcheck를 활용한 안정적인 시작 순서
  • 네트워크: 서비스 간 통신과 네트워크 격리
  • 실전 예제: WordPress, Node.js 스택 등 실제 사용 사례
  • 명령어: up, down, ps, logs, exec 등 주요 명령어
  • 환경별 설정: .env 파일, override 파일, 프로필 활용

다음 편에서는 Kubernetes의 기본 개념아키텍처를 다루며, 컨테이너 오케스트레이션의 세계로 나아가겠습니다.