목. 8월 14th, 2025

G: 안녕하세요, 개발자 여러분! 👋 복잡한 마이크로서비스 아키텍처나 여러 컨테이너로 구성된 애플리케이션을 개발하고 계신가요? 매번 docker run 명령어를 여러 개 입력하거나 수동으로 네트워크를 설정하는 데 지치셨다면, 바로 Docker Compose가 여러분의 구세주가 될 수 있습니다!

Docker Compose는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다. YAML 파일을 사용하여 애플리케이션의 모든 서비스를 구성하면, 단일 명령어로 전체 애플리케이션을 빌드하고 실행하며 관리할 수 있습니다. 마치 오케스트라의 지휘자처럼, Docker Compose는 여러분의 컨테이너들을 일사천리로 움직이게 합니다.

이 글에서는 개발자가 Docker Compose를 더욱 효율적으로 활용할 수 있도록, YAML 파일 작성 팁과 함께 실용적인 베스트 프랙티스들을 자세히 알아보겠습니다.


1. Docker Compose YAML 기본 구조 이해하기 🏗️

Docker Compose 파일은 .yaml 또는 .yml 확장자를 가지며, 다음과 같은 주요 섹션으로 구성됩니다.

# docker-compose.yml
version: '3.8' # Docker Compose 파일 형식 버전
services:     # 애플리케이션을 구성하는 서비스들 정의
  webapp:
    # ... 서비스 설정 ...
  database:
    # ... 서비스 설정 ...
networks:     # 컨테이너 간 통신을 위한 네트워크 정의 (선택 사항)
  app_network:
    driver: bridge
volumes:      # 데이터 영속성을 위한 볼륨 정의 (선택 사항)
  db_data:
  • version: Docker Compose 파일 형식을 지정합니다. 최신 기능과 문법을 사용하려면 최신 버전을 사용하는 것이 좋습니다 (예: '3.8').
  • services: 이 섹션은 애플리케이션을 구성하는 핵심 부분입니다. 각 서비스는 독립적인 컨테이너로 실행되며, 웹 서버, 데이터베이스, 백엔드 API 등 개별 구성 요소를 나타냅니다.
  • networks: 컨테이너들이 서로 통신할 수 있도록 네트워크를 정의합니다. 명시적으로 정의하지 않으면 기본 네트워크가 생성됩니다.
  • volumes: 컨테이너의 데이터를 영구적으로 저장하기 위한 볼륨을 정의합니다. 컨테이너가 삭제되어도 데이터가 유지될 수 있도록 합니다.

💡 팁: YAML 파일은 들여쓰기가 매우 중요합니다! 항상 스페이스 2칸 또는 4칸을 사용하여 정확한 구조를 유지하세요. 탭은 사용하지 않는 것이 좋습니다.


2. 핵심 서비스 지시어 활용 팁 🚀

services 아래의 각 서비스는 다양한 지시어를 통해 컨테이너의 동작을 세밀하게 제어할 수 있습니다.

2.1. image vs. build 🛠️

  • image: 이미 빌드되어 Docker Hub나 다른 레지스트리에 저장된 이미지를 사용할 때 지정합니다. 간편하게 컨테이너를 실행할 수 있습니다.
    services:
      database:
        image: postgres:14-alpine # PostgreSQL 14 버전 이미지 사용
  • build: 여러분의 소스 코드로부터 Docker 이미지를 직접 빌드할 때 사용합니다. Dockerfile이 있는 경로를 지정합니다.
    services:
      webapp:
        build: ./web_app # 현재 디렉토리의 'web_app' 폴더에 있는 Dockerfile로 빌드
        context: .      # Dockerfile이 있는 디렉토리 (기본값)
        dockerfile: Dockerfile.dev # 사용할 Dockerfile 이름 (기본값 Dockerfile)

    💡 팁: 개발 환경에서는 build를 사용하여 코드 변경 시 바로 반영되도록 하고, 배포 환경에서는 미리 빌드된 image를 사용하는 것이 일반적입니다.

2.2. ports 🌐

호스트 머신과 컨테이너 간의 포트 매핑을 설정합니다. HOST_PORT:CONTAINER_PORT 형식으로 지정합니다.

services:
  webapp:
    image: my_webapp_image
    ports:
      - "80:80"    # 호스트의 80번 포트를 컨테이너의 80번 포트에 연결
      - "443:443"  # HTTPS 포트
      - "8080"     # 호스트의 임의의 사용 가능한 포트를 컨테이너의 8080번에 연결

⚠️ 주의: 8080처럼 컨테이너 포트만 지정하면, Docker가 호스트에 임의의 사용 가능한 포트를 할당합니다. docker-compose ps 명령으로 확인해야 합니다.

2.3. volumes 💾

데이터 영속성을 확보하거나 호스트와 컨테이너 간에 파일을 공유할 때 사용합니다.

  • 바인드 마운트 (Bind Mounts): 호스트 머신의 특정 경로를 컨테이너 내의 경로에 연결합니다. 개발 환경에서 소스 코드 동기화에 유용합니다.
    services:
      webapp:
        image: my_webapp_image
        volumes:
          - ./src:/app/src # 호스트의 './src' 폴더를 컨테이너의 '/app/src'에 마운트
          - ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro # 읽기 전용으로 마운트

    💡 팁: 개발 시 소스 코드를 바인드 마운트하면, 호스트에서 코드를 수정해도 컨테이너 내에서 바로 반영되어 개발 속도를 높일 수 있습니다.

  • 명명된 볼륨 (Named Volumes): Docker가 관리하는 볼륨으로, 주로 데이터베이스 파일처럼 영구적인 데이터를 저장할 때 사용합니다.
    services:
      database:
        image: postgres:14-alpine
        volumes:
          - db_data:/var/lib/postgresql/data # 'db_data'라는 명명된 볼륨 사용
    volumes: # 볼륨 정의 섹션
      db_data:
        driver: local # 로컬 드라이버 사용

    💡 팁: 명명된 볼륨은 컨테이너가 삭제되어도 데이터가 보존되기 때문에, 데이터베이스나 캐시처럼 중요한 데이터를 저장하는 데 필수적입니다.

2.4. environment & env_file 🔒

환경 변수를 설정하여 컨테이너 동작을 유연하게 제어합니다.

  • environment: YAML 파일 내부에 직접 환경 변수를 정의합니다.
    services:
      webapp:
        environment:
          - NODE_ENV=development
          - DATABASE_URL=postgres://user:pass@db_host:5432/mydb
  • env_file: .env 파일 등 외부 파일에서 환경 변수를 로드합니다. 민감한 정보(비밀번호, API 키)를 관리하는 데 필수적입니다.
    services:
      webapp:
        env_file:
          - .env.development # 개발 환경 변수 파일
          - .env.secrets     # 민감한 비밀 정보 파일

    ⚠️ 주의: 민감한 정보는 environment에 직접 노출하지 말고, .env 파일을 사용하고 .gitignore에 추가하여 버전 관리에서 제외해야 합니다.

2.5. depends_on 🔗

서비스 간의 의존성을 명시하여 특정 서비스가 시작된 후에 다른 서비스가 시작되도록 합니다.

services:
  webapp:
    build: .
    depends_on:
      - database # webapp은 database가 먼저 시작된 후에 시작
      - cache    # webapp은 cache가 먼저 시작된 후에 시작
  database:
    image: postgres:14-alpine
  cache:
    image: redis:latest

⚠️ 주의: depends_on은 단순히 서비스 시작 순서만 보장하며, 서비스가 준비 완료되었음을 보장하지는 않습니다. 예를 들어, 데이터베이스 컨테이너가 시작되었어도 내부적으로 DB 서버가 완전히 구동되기까지 시간이 걸릴 수 있습니다. 이 문제는 healthcheck로 해결합니다.

2.6. networks 🌉

특정 네트워크에 서비스를 연결합니다. 컨테이너 간의 통신 격리 및 관리에 유용합니다.

services:
  webapp:
    build: .
    networks:
      - frontend_network # 외부에서 접근 가능한 네트워크
      - backend_network  # 내부 서비스 간 통신용 네트워크
  database:
    image: postgres:14-alpine
    networks:
      - backend_network  # 데이터베이스는 백엔드 네트워크에만 연결
networks:
  frontend_network:
    driver: bridge
  backend_network:
    driver: bridge

💡 팁: 각 서비스를 필요한 네트워크에만 연결하여 보안을 강화하고 불필요한 통신을 방지할 수 있습니다. 같은 네트워크에 연결된 서비스는 서비스 이름을 사용하여 서로 통신할 수 있습니다 (예: database 서비스 내에서 webapp 서비스에 연결할 때 http://webapp:80).

2.7. restart 🔄

컨테이너 종료 시 재시작 정책을 설정합니다.

  • no: 재시작 안 함 (기본값)
  • always: 항상 재시작
  • on-failure: 비정상 종료 시에만 재시작
  • unless-stopped: 수동으로 중지되기 전까지 항상 재시작
services:
  webapp:
    build: .
    restart: unless-stopped # 컨테이너가 수동으로 중지되지 않는 한 항상 재시작
  database:
    image: postgres:14-alpine
    restart: always # 항상 재시작

💡 팁: 개발 환경에서는 no 또는 on-failure를, 중요한 백엔드 서비스나 데이터베이스는 always 또는 unless-stopped를 고려해볼 수 있습니다.


3. 개발자를 위한 베스트 프랙티스 (심화) ✨

이제 더 견고하고 효율적인 Docker Compose 환경을 구축하기 위한 심화 팁들을 알아봅시다.

3.1. healthcheck 활용하여 서비스 준비 상태 확인 🩺

depends_on은 시작 순서만 보장하므로, 실제 서비스가 요청을 처리할 준비가 되었는지 확인하기 위해 healthcheck를 사용합니다.

services:
  webapp:
    build: .
    ports:
      - "80:80"
    depends_on:
      database:
        condition: service_healthy # database 서비스가 건강해질 때까지 기다림
  database:
    image: postgres:14-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"] # DB 연결 테스트
      interval: 5s    # 5초마다 체크
      timeout: 5s     # 체크당 최대 5초 대기
      retries: 5      # 5회 실패 시 unhealthy
      start_period: 10s # 시작 후 10초 동안은 healthcheck 실패 무시

🚀 중요:healthcheck 덕분에 webappdatabase 컨테이너가 단순히 시작하는 것을 넘어, PostgreSQL 서버가 완전히 구동되어 쿼리를 받을 준비가 될 때까지 기다리게 됩니다. 이는 “컨테이너가 시작되었지만 서비스는 아직 준비되지 않은” 상황에서 발생하는 오류를 방지하는 가장 좋은 방법입니다.

3.2. 다양한 환경 관리: profilesextends 🌍

개발, 테스트, 운영 등 여러 환경에 따라 다른 설정을 사용해야 할 때 유용합니다.

  • profiles: 특정 프로파일이 활성화될 때만 해당 서비스가 실행되도록 합니다.
    # docker-compose.yml
    services:
      webapp:
        build: .
        ports:
          - "80:80"
        profiles: ["dev", "prod"] # dev 또는 prod 프로파일에서 실행
      tests:
        build: .
        command: npm test
        profiles: ["test"] # test 프로파일에서만 실행
      admin_panel:
        build: .
        ports:
          - "8080:80"
        profiles: ["dev"] # 개발 환경에서만 실행

    실행: docker-compose --profile dev up -d 또는 docker-compose --profile test up --build

  • extends: 공통 설정을 가진 베이스 파일을 만들고, 다른 파일에서 이를 확장하여 재사용성을 높입니다.

    # base.yaml (공통 설정)
    services:
      base_service:
        image: my_app_base
        environment:
          APP_ENV: default
        networks:
          - app_network
    networks:
      app_network:
        driver: bridge
    
    # docker-compose.yml (개발 환경)
    services:
      webapp:
        extends:
          file: base.yaml # base.yaml의 base_service를 확장
          service: base_service
        environment:
          APP_ENV: development # APP_ENV를 development로 오버라이드
        volumes:
          - ./src:/app/src
    
    # docker-compose.prod.yml (운영 환경)
    services:
      webapp:
        extends:
          file: base.yaml
          service: base_service
        environment:
          APP_ENV: production
        # 운영 환경에 맞는 추가 설정 (예: 리소스 제한, 로그 드라이버)

    실행: docker-compose -f docker-compose.yml up (개발) 또는 docker-compose -f docker-compose.prod.yml up (운영)

3.3. 리소스 제한 설정 (deploy -> resources) 📈

컨테이너가 사용할 수 있는 CPU, 메모리 등의 리소스를 제한하여 안정성을 높입니다. 특히 개발 환경에서 특정 서비스가 너무 많은 리소스를 차지하는 것을 방지할 수 있습니다.

services:
  webapp:
    build: .
    deploy:
      resources:
        limits: # 최대 사용량
          cpus: '0.5' # 0.5 코어
          memory: 512M # 512MB
        reservations: # 보장되는 최소 사용량
          cpus: '0.25'
          memory: 256M

💡 팁: 이는 Docker Swarm 모드에서 주로 사용되지만, Compose CLI로도 단일 호스트에서 적용 가능합니다. 개발 환경에서는 리소스 부족으로 인한 호스트 불안정을 막을 수 있고, 테스트 환경에서는 실제 운영 환경과 유사한 부하 테스트를 가능하게 합니다.

3.4. latest 태그 지양하기 🚫

image: myapp:latest와 같이 latest 태그를 사용하는 것은 피해야 합니다. latest 태그는 언제든 업데이트될 수 있어, 예상치 못한 변경이나 버그를 유발할 수 있습니다. 항상 특정 버전을 명시하는 것이 좋습니다: image: postgres:14.5-alpine.

3.5. .dockerignore 파일 활용 🧹

build 지시어를 사용할 때 .dockerignore 파일을 Dockerfile과 같은 디렉토리에 두면, 이미지 빌드 시 불필요한 파일(예: .git, node_modules, logs)이 컨텍스트에 포함되는 것을 방지하여 빌드 속도를 높이고 이미지 크기를 줄일 수 있습니다.

# .dockerignore 예시
.git
.vscode
node_modules
npm-debug.log
dist/
tmp/
*.log

3.6. 컨테이너 간 통신 시 서비스 이름 사용 🗣️

Docker Compose는 기본적으로 서비스 이름을 DNS 호스트 이름으로 등록해줍니다. 따라서 같은 네트워크에 있는 컨테이너들은 localhost 대신 서비스 이름을 사용하여 서로 통신할 수 있습니다.

# docker-compose.yml
services:
  frontend:
    build: ./frontend
    environment:
      API_URL: http://backend:3000 # 'backend' 서비스 이름으로 접근
  backend:
    build: ./backend
    ports:
      - "3000:3000"

💡 팁: 이는 개발 환경에서 매우 편리하며, 나중에 배포 환경으로 전환할 때도 서비스 디스커버리 문제를 줄여줍니다.


4. 흔히 하는 실수와 해결책 ⚠️

Docker Compose를 사용하면서 개발자들이 자주 겪는 문제점과 그 해결책입니다.

4.1. 하드코딩된 비밀번호/민감 정보 ❌

  • 문제: 데이터베이스 비밀번호, API 키 등을 environment 섹션에 직접 작성하는 경우.
  • 해결책: .env 파일을 사용하고, .gitignore에 추가하여 버전 관리 시스템에 올라가지 않도록 합니다. 실제 배포 시에는 Docker Secrets나 Kubernetes Secrets 같은 보안 솔루션을 사용합니다.

4.2. depends_on 오해 ⏳

  • 문제: depends_on이 컨테이너의 서비스 준비 완료를 보장한다고 착각하는 경우.
  • 해결책: 위에서 설명한 healthcheck를 함께 사용하여 서비스의 실제 준비 상태를 확인하고, depends_oncondition: service_healthy를 추가합니다.

4.3. 포트 충돌 🚪💥

  • 문제: 여러 서비스가 동일한 호스트 포트를 사용하려고 하거나, 호스트의 이미 사용 중인 포트를 사용하려 할 때.
  • 해결책: ports 매핑에서 각 서비스에 고유한 호스트 포트를 할당하거나, 이미 사용 중인 포트가 없는지 확인합니다. docker-compose ps 명령으로 현재 실행 중인 포트 매핑을 확인할 수 있습니다.

4.4. 볼륨 마운트 경로 오류 🗺️

  • 문제: 호스트 또는 컨테이너의 볼륨 경로를 잘못 지정하여 파일이 동기화되지 않거나 데이터가 저장되지 않는 경우.
  • 해결책: 절대 경로를 사용하거나, ./를 사용하여 현재 docker-compose.yml 파일 기준의 상대 경로를 명확히 지정합니다. 컨테이너 내부의 경로도 정확한지 확인합니다 (예: Nginx는 /etc/nginx/conf.d, PostgreSQL은 /var/lib/postgresql/data).

4.5. 로그 확인 소홀 🧐

  • 문제: 컨테이너가 제대로 실행되지 않는데 로그를 확인하지 않아 문제 원인을 찾기 어려운 경우.
  • 해결책: docker-compose logs [서비스_이름] 명령을 사용하여 특정 서비스의 로그를 확인하고, -f 옵션으로 실시간 로그를 추적합니다. 문제 발생 시 가장 먼저 확인해야 할 부분입니다.

5. 마무리하며: 개발 워크플로우의 혁신 🌟

Docker Compose는 개발자가 다중 컨테이너 애플리케이션을 쉽고 효율적으로 관리할 수 있도록 돕는 강력한 도구입니다. 위에 제시된 팁과 베스트 프랙티스들을 적용하면, 다음과 같은 이점을 누릴 수 있습니다.

  • 일관된 개발 환경: 팀원 간의 환경 차이로 인한 “내 컴퓨터에서는 되는데…” 문제를 해결합니다.
  • 빠른 온보딩: 새로운 팀원이 프로젝트에 합류했을 때, 단일 명령어로 개발 환경을 즉시 구축할 수 있습니다.
  • 효율적인 테스트: 여러 서비스를 쉽게 올리고 내리면서 통합 테스트를 수행할 수 있습니다.
  • 시간 절약: 반복적인 수동 설정을 자동화하여 개발 시간을 절약합니다.

이제 여러분은 Docker Compose YAML 파일을 더욱 견고하고 유연하게 작성할 수 있는 지식을 갖추었습니다. 오늘부터 Docker Compose를 적극적으로 활용하여 여러분의 개발 워크플로우를 한 단계 업그레이드해보세요! 궁금한 점이 있다면 언제든지 검색하고 실험해보는 것을 두려워하지 마세요. Happy Dockering! 🚀

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다