infra
Platform

모듈 맵

[Docker] 복잡한 멀티 서비스 환경의 유기적 연동과 배포

0 / 27 완료

펼치기
0 / 27 완료0%

도커 & 컨테이너 · 11 / 27

[Docker] 복잡한 멀티 서비스 환경의 유기적 연동과 배포

docker-compose.yml로 멀티 컨테이너 서비스를 선언하고 한 번에 실행합니다

🚨INCIDENT ALERT
HIGH

새 팀원이 로컬 개발 환경을 띄우기 위해 nginx, MariaDB, Redis를 각각 docker run으로 실행했습니다. 포트 하나를 잘못 매핑했고, DB 볼륨 이름도 달라서 다른 팀원과 전혀 다른 상태가 만들어졌습니다.

여러 컨테이너로 구성된 서비스는 명령어를 사람이 기억해서 맞추기 어렵습니다. Docker Compose는 서비스, 네트워크, 볼륨, 환경변수를 YAML 파일에 선언해 팀 전체가 같은 환경을 한 번에 올리게 해줍니다.

다중 컨테이너 오케스트레이션 (Docker Compose)

웹 애플리케이션은 대부분 여러 컨테이너로 구성됩니다. 웹 서버, 데이터베이스, 캐시 서버 등을 각각 docker run 명령으로 실행하면 실수가 생기기 쉽고 반복 작업도 번거롭습니다. Docker Compose는 이 모든 구성을 YAML 파일 하나로 선언하고 단 한 번의 명령으로 실행하게 해줍니다.


이번 챕터에서 배울 것

여러 컨테이너로 구성된 애플리케이션(예: 앱 서버 + DB + 캐시)을 단일 파일로 정의하고, 명령어 하나로 올리고 내리는 방법을 익힙니다.

  • 1docker-compose.yml의 services / networks / volumes 세 섹션 역할을 설명할 수 있다
  • 2docker compose up / down / ps / logs / exec 핵심 명령어를 다룰 수 있다
  • 3depends_on과 healthcheck으로 서비스 시작 순서를 보장할 수 있다
  • 4.env 파일과 environment 키로 환경 변수를 관리할 수 있다
  • 5개발/스테이징/프로덕션 환경별 override 파일 패턴을 구성할 수 있다
실습 환경 준비

Docker Compose V2는 Docker Desktop에 기본 포함됩니다. Linux 서버라면 sudo apt-get install docker-compose-plugin으로 설치하세요.

Docker Compose V2 설치 확인 (docker compose 서브커맨드 방식)
docker compose version
실습용 디렉토리 생성
mkdir -p ~/compose-lab && cd ~/compose-lab
실습용 이미지 미리 pull
docker pull nginx:alpine && docker pull redis:7-alpine && docker pull postgres:16-alpine
💡개념

IaC 개념과 docker-compose.yml 핵심 구조

새 팀원이 프로젝트를 시작하려면 nginx, PostgreSQL, Redis를 각각 설치하고, 포트를 설정하고, 환경 변수를 맞추고, 네트워크를 연결해야 합니다. 한 번에 성공하면 다행이지만 대부분 뭔가를 빠뜨려서 30분을 헤맵니다. Docker Compose는 이 설정 전체를 docker-compose.yml 파일 하나에 선언하고, docker compose up -d 한 줄로 전체 환경을 올립니다. 이 파일을 Git에 올려두면 팀원 모두가 완전히 동일한 환경에서 작업하고, 빌드 서버와 개발 서버의 차이로 인한 "내 컴퓨터에서는 되는데요" 문제가 사라집니다. 이 ConceptBlock에서는 docker-compose.yml의 세 가지 최상위 섹션(services, networks, volumes)과 각 옵션의 역할을 다룹니다.

IaC 구조와 docker-compose.yml 핵심 구조 — nginx·PostgreSQL·Redis를 따로 설치·설정하던 작업을 docker-compose.yml 하나에 services·networks·volumes로 선언하고 docker compose up -d 한 줄로 전체 환경을 재현. 설정이 코드(IaC)로 남아 새 팀원도 동일 환경을 즉시 구성확대

Infrastructure as Code (IaC)

Docker Compose는 Infrastructure as Code(IaC) 의 좋은 예시입니다. 인프라 구성을 코드(YAML 파일)로 선언하면 다음과 같은 이점이 있습니다.

  • 재현 가능성: 어느 환경에서든 같은 파일로 동일한 환경을 구성
  • 버전 관리: Git으로 인프라 변경 이력 추적
  • 팀 공유: 팀원 모두가 같은 개발 환경을 사용
  • 문서화: 파일 자체가 아키텍처 문서 역할

docker-compose.yml 핵심 구조

YAML 파일에서 services, networks, volumes 세 최상위 키로 전체 애플리케이션 스택을 선언합니다. 아래 예시에서 구조 전체를 한눈에 파악하세요.

YAML
version: "3.9"          # Compose 파일 스펙 버전

services:               # 실행할 컨테이너들 정의
  web:                  # 서비스 이름 (컨테이너 이름 기반)
    image: nginx:alpine # 사용할 이미지
    ports:
      - "80:80"         # 호스트포트:컨테이너포트
    volumes:
      - ./html:/usr/share/nginx/html  # 마운트
    networks:
      - frontend        # 연결할 네트워크
    depends_on:
      - db              # 시작 순서 의존성
    environment:
      - APP_ENV=production
    restart: always     # 재시작 정책

  db:
    image: mariadb:10.6
    volumes:
      - db_data:/var/lib/mysql  # 이름 있는 볼륨
    env_file:
      - .env            # 환경변수 파일 참조
    networks:
      - backend

networks:               # 네트워크 정의
  frontend:
  backend:

volumes:                # 볼륨 정의
  db_data:

주요 최상위 키 설명

각 최상위 키가 담당하는 역할을 파악하면 YAML 파일 전체 구조가 명확해집니다.

역할
versionCompose 파일 형식 버전 지정 (3.x 권장)
services실행할 컨테이너 서비스 목록
networks서비스 간 통신에 사용할 네트워크 정의
volumes데이터를 영속적으로 보관할 볼륨 정의

YAML 문법 핵심 규칙

YAML
# 들여쓰기: 스페이스 2개 또는 4개 (탭 불가!)
services:
  web:          # 2칸 들여쓰기
    image: nginx  # 4칸 들여쓰기

# 목록(리스트): 하이픈(-)으로 표현
ports:
  - "80:80"
  - "443:443"

# 키-값 쌍: 콜론(:) + 스페이스
image: nginx:alpine   # 콜론 뒤 반드시 스페이스!
컨테이너 실행 방식 선택 기준
로컬 개발 환경에서 여러 서비스를 한 번에 올려야 할 때Docker ComposeYAML 한 파일로 전체 스택 선언, docker compose up 한 줄로 실행
단일 컨테이너를 빠르게 테스트하거나 일회성 실행이 필요할 때docker run 직접 실행명령어 하나로 즉시 실행, 설정 파일 없이 빠른 검증 가능
프로덕션 환경에서 수십 개 이상 컨테이너를 스케일아웃해야 할 때Kubernetes자동 스케일링, 자가 치유, 롤링 배포 등 운영 자동화 지원
소규모 팀에서 간단한 멀티 호스트 클러스터가 필요할 때Docker SwarmCompose 파일 재사용 가능, K8s보다 설정 단순, docker stack deploy로 배포
CI/CD 파이프라인에서 빌드 및 테스트 환경을 격리해야 할 때Docker Compose (CI 모드)--abort-on-container-exit 옵션으로 테스트 종료 시 자동 정리

💡개념

docker compose up 한 번이 여러 컨테이너를 순서대로 띄우기까지 — 6단계

docker compose up -d 한 줄. Enter를 누르면 잠깐 뒤에 여러 컨테이너가 모두 떠 있습니다. 이 짧은 순간에 Compose는 compose 파일 검증 → 네트워크·볼륨 생성 → depends_on 순서로 컨테이너 생성·시작 → 헬스체크 대기 → 서비스명 DNS 연결을 정해진 순서로 처리합니다. 이 흐름을 알면 "왜 여기서 멈추지", "왜 web이 db를 못 찾지"를 단계로 좁혀 진단할 수 있습니다.

TEXT
docker compose up -d
   │
   ① compose 파일 파싱·검증          (services·networks·volumes 병합, 변수 치환)
   │
   ② 네트워크 생성                   (프로젝트명_default 브리지 — 없으면 새로 만듦)
   │
   ③ 볼륨 생성                       (db_data 등 named volume — 이미 있으면 재사용)
   │
   ④ depends_on 순서로 컨테이너 생성  (의존 대상부터: db → web)
   │
   ⑤ 시작 + healthcheck 대기         (condition: service_healthy면 healthy까지 대기)
   │
   ⑥ 서비스명 DNS로 상호 통신 준비    (web이 db:3306 이름으로 접속)
   ▼
모든 서비스 Up — docker compose ps로 확인

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① 파싱·검증compose 파일과 override를 병합하고 ${VAR}.env·환경변수로 치환. YAML 문법·필수 키 검사들여쓰기·탭 오류나 미정의 변수 → yaml: ... 또는 variable is not set 경고로 즉시 중단
② 네트워크프로젝트 전용 브리지 네트워크(프로젝트명_default)를 만들고 각 서비스를 연결external 네트워크가 없거나 서브넷 충돌 → network ... not found·Pool overlaps
③ 볼륨named volume을 생성(이미 있으면 그대로 마운트). 프로젝트명이 접두어로 붙음프로젝트명(디렉토리명)이 바뀌면 새 빈 볼륨이 붙어 데이터가 사라진 듯 보임
④ 생성 순서depends_on 그래프를 위상정렬해 의존 대상부터 컨테이너를 생성. 이미지 없으면 pull 후 진행순환 의존 → circular dependency. pull 실패 → pull access denied
⑤ 시작·헬스컨테이너 시작. condition: service_healthy면 대상이 healthy 될 때까지 다음 서비스를 대기healthcheck가 계속 실패 → 의존 서비스가 waiting에서 안 넘어감. depends_on만 있으면 준비 전 시작돼 앱이 연결 오류
⑥ DNS·통신같은 네트워크 안에서 서비스명이 DNS 이름이 됨(db→컨테이너 IP). 포트는 컨테이너 내부 포트로 접속다른 네트워크에 있거나 서비스명 오타 → could not resolve host·이름 해석 실패

"compose up 성공"은 여섯 단계가 모두 통과했다는 뜻입니다 — 파일이 유효하고(①), 네트워크·볼륨이 준비됐고(②③), 의존 순서대로 떠서(④) 헬스체크를 통과했고(⑤), 서비스끼리 이름으로 찾을 수 있다(⑥). 문제가 생기면 docker compose up(포그라운드) 로그가 어느 단계에서 멈췄는지 보여주고, docker compose config로 ①의 병합·치환 결과를, docker compose ps로 ⑤의 각 서비스 상태(Up·health)를 확인해 좁힙니다. depends_on이 보장하는 것은 ④ '시작 순서'까지이고, ⑤ '준비 완료'는 healthcheck + condition이 담당한다는 구분이 핵심입니다.


💡개념

서비스 시작 순서와 환경변수 관리

docker compose up을 실행했는데 웹 앱이 시작 직후에 죽습니다. 로그를 보면 "cannot connect to database"입니다. depends_on: - db를 설정했는데도 왜 그럴까요? depends_on은 컨테이너가 시작되는 순서만 보장하고, 서비스가 실제로 요청을 받을 준비가 됐는지는 확인하지 않습니다. PostgreSQL 컨테이너가 켜졌어도 초기화가 끝나기 전에 앱이 접속을 시도하면 실패합니다. 환경변수도 마찬가지입니다. .env 파일, environment: 블록, 시스템 환경변수 중 어느 것이 우선인지 모르면 개발 PC에서는 잘 되는데 CI에서만 환경변수가 비어있는 문제를 반복적으로 만납니다. 이 ConceptBlock에서는 depends_on의 실제 동작과 healthcheck 기반 대기 패턴, 그리고 환경변수 주입 우선순위를 다룹니다.

서비스 시작 순서와 환경변수 관리 — depends_on은 컨테이너 시작 순서만 보장하고 DB가 연결 받을 준비가 됐는지는 확인 안 해 앱이 "cannot connect to database"로 죽음. condition: service_healthy로 준비 완료를 기다리고, 환경변수는 environment·env_file·.env로 주입확대

depends_on의 동작 원리와 한계

depends_on은 컨테이너 시작 순서를 제어하지만, 컨테이너가 '준비됐는지'는 확인하지 않습니다. 이것이 실무에서 자주 발생하는 함정입니다.

YAML
services:
  web:
    image: wordpress
    depends_on:
      - db          # db 컨테이너가 시작되면 web을 시작
                    # ⚠️ db가 실제로 연결 받을 준비가 됐는지는 미확인!
  db:
    image: mariadb

문제 상황: MariaDB는 컨테이너가 시작된 후에도 초기화가 완료될 때까지 수 초~수십 초가 걸립니다. 이 사이에 WordPress가 DB에 연결을 시도하면 실패합니다.

완전한 해결책: healthcheck + condition

YAML
services:
  web:
    image: wordpress
    depends_on:
      db:
        condition: service_healthy  # DB가 healthy 상태일 때만 시작
    restart: always

  db:
    image: mariadb:10.6
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s     # 10초마다 체크
      timeout: 5s       # 5초 내 응답 없으면 실패
      retries: 5        # 5회 실패 시 unhealthy 판정
      start_period: 30s # 시작 후 30초간은 실패해도 무시

환경변수 관리 전략

방법 1: environment 키 (인라인) — docker-compose.yml에 직접 환경변수를 나열합니다.

YAML
services:
  db:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword  # ⚠️ 파일에 노출 위험
      MYSQL_DATABASE: mydb
      MYSQL_USER: user

방법 2: env_file (외부 파일 분리 — 권장) — 환경변수를 별도 파일로 분리해 버전 관리에서 제외합니다.

YAML
services:
  db:
    image: mariadb
    env_file:
      - .env  # .gitignore에 추가하여 민감 정보 보호

.env 파일 내용:

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part3/exam_11 && cd /tmp/docker/part3/exam_11

MYSQL_ROOT_PASSWORD=supersecret
MYSQL_DATABASE=myapp_db
MYSQL_USER=appuser
MYSQL_PASSWORD=apppassword

방법 3: 호스트 환경변수 참조 — 호스트의 환경변수를 컨테이너로 그대로 전달합니다.

YAML
environment:
  - DB_PASSWORD=${DB_PASSWORD}  # 호스트의 환경변수에서 주입

포트/볼륨/네트워크 선언 패턴

YAML
services:
  app:
    # 포트: "호스트:컨테이너" 형식
    ports:
      - "8080:80"         # 호스트 8080 → 컨테이너 80
      - "127.0.0.1:3000:3000"  # 루프백만 바인딩 (보안 강화)

    # 볼륨: 바인드 마운트와 이름 있는 볼륨
    volumes:
      - ./config:/app/config   # 바인드 마운트 (상대경로)
      - app_data:/app/data     # 이름 있는 볼륨

    # 네트워크: 여러 네트워크에 동시 연결 가능
    networks:
      - frontend
      - backend

WordPress + MariaDB 연동 실습

Docker Compose를 사용해 WordPress와 MariaDB를 연동하는 완전한 실습을 진행합니다.

실습 전 프로젝트 디렉토리와 예제 파일을 먼저 준비합니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part2/compose-app/{backend,frontend,nginx}

# 백엔드 앱 파일 생성
cat > /tmp/docker/part2/compose-app/backend/app.py << 'EOF'
from flask import Flask, jsonify
import os, redis

app = Flask(__name__)
cache = redis.Redis(host=os.getenv('REDIS_HOST', 'redis'), port=6379)

@app.route('/')
def index():
    count = cache.incr('hits')
    return jsonify({'message': 'Hello!', 'hits': int(count)})

@app.route('/health')
def health():
    return jsonify({'status': 'ok'})
EOF

cat > /tmp/docker/part2/compose-app/backend/requirements.txt << 'EOF'
flask==3.0.0
redis==5.0.0
EOF

cat > /tmp/docker/part2/compose-app/backend/Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
EOF

이제 WordPress + MariaDB 실습을 진행합니다.

1단계: 작업 디렉토리 생성 — Compose 프로젝트 디렉토리를 만듭니다.

로컬 터미널
mkdir -p /tmp/docker/part2/compose-app/wordpress-demo && cd /tmp/docker/part2/compose-app/wordpress-demo

2단계: docker-compose.yml 작성 — 서비스, 볼륨, 네트워크를 정의합니다.

YAML
# /tmp/docker/part2/compose-app/wordpress-demo/docker-compose.yml
version: "3.9"

services:
  db:
    image: mariadb:10.6
    container_name: wp_database
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass123
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppass123
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - wp_network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass123"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  wordpress:
    image: wordpress:latest
    container_name: wp_app
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppass123
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
    networks:
      - wp_network
    depends_on:
      db:
        condition: service_healthy

networks:
  wp_network:
    driver: bridge

volumes:
  db_data:
  wp_data:

3단계: 백그라운드로 실행-d 옵션으로 백그라운드 실행합니다.

로컬 터미널
docker-compose up -d
# Creating network "wordpress-demo_wp_network" with driver "bridge"
# Creating volume "wordpress-demo_db_data" with default driver
# Creating volume "wordpress-demo_wp_data" with default driver
# Creating wp_database ... done
# Creating wp_app      ... done

4단계: 실행 상태 확인 — 모든 컨테이너가 Up 상태인지 확인합니다.

로컬 터미널
docker-compose ps
# Name         Command         State    Ports
# wp_database  ...             Up       3306/tcp
# wp_app       ...             Up       0.0.0.0:8080->80/tcp

# DB의 헬스체크 상태 확인
docker inspect wp_database --format '{{.State.Health.Status}}'
# healthy

5단계: 로그 확인 — 서비스별 로그를 실시간으로 확인합니다.

로컬 터미널
# 전체 서비스 로그
docker-compose logs

# wordpress 서비스만 실시간 로그
docker-compose logs -f wordpress

# 최근 50줄만
docker-compose logs --tail=50 db

6단계: http://localhost:8080 접속하여 WordPress 설치 진행

7단계: 정리 — 컨테이너와 볼륨을 함께 삭제합니다.

로컬 터미널
# 컨테이너와 네트워크 삭제 (볼륨 유지)
docker-compose down

# 볼륨까지 완전 삭제
위험 명령어Compose 프로젝트의 이름 있는 볼륨까지 삭제되어 DB 데이터가 사라질 수 있습니다.

Compose 볼륨 포함 삭제

안전한 실행 조건: 데이터 초기화가 목적이고 삭제할 볼륨이 실습용임을 확인한 경우에만 실행합니다.

실행 전 반드시 확인

  • docker-compose ps로 대상 프로젝트를 확인했다
  • docker volume ls에서 삭제될 볼륨 이름을 확인했다
  • 보존해야 할 DB/업로드 데이터가 없음을 확인했다
docker-compose down -v

위 항목을 모두 확인한 후 복사할 수 있습니다

🔍실행 후 확인할 것
  • docker-compose ps 출력에서 wp_database와 wp_app이 모두 Up 상태인가?
  • docker inspect wp_database 결과가 healthy 상태로 표시되는가?
  • 브라우저에서 http://localhost:8080 접속 시 WordPress 설치 화면이 보이는가?
  • docker-compose down 후 컨테이너는 사라지고 볼륨은 유지되는 차이를 설명할 수 있는가?
  • [읽기 순서 — 원칙 A] docker compose ps 출력에서 Up 상태가 아닌 서비스(Exited, Restarting)를 먼저 찾는다. 문제 서비스를 특정한 뒤 `docker compose logs <서비스명>`으로 원인을 파악한다.
  • [수치 판단 기준 — 원칙 B] docker compose up -d 후 30초 내에 모든 서비스가 Up 상태여야 정상이다. 특정 서비스의 restart 횟수가 3회 이상(docker inspect --format "{{.RestartCount}}")이면 구조적 문제이므로 로그를 반드시 확인한다.
  • [조합 해석 — 원칙 C] depends_on만 설정하고 healthcheck가 없으면 컨테이너 시작 순서만 보장되고 서비스 준비 여부는 보장되지 않는다. 웹 서비스가 시작 직후 DB 연결 오류로 재시작을 반복하면 healthcheck + condition: service_healthy 조합을 추가해야 한다.

Docker Compose 핵심 명령어 실습

Compose 환경을 운영하는 데 필요한 핵심 명령어를 익힙니다.

1단계: 간단한 테스트 환경 생성 — 스케일링 실습을 위한 기본 환경을 준비합니다.

로컬 터미널
mkdir -p /tmp/docker/part2/compose-app/compose-test && cd /tmp/docker/part2/compose-app/compose-test

cat > docker-compose.yml << 'EOF'
version: "3.9"
services:
  web:
    image: nginx:alpine
    ports:
      - "8081:80"
  redis:
    image: redis:alpine
EOF

2단계: 실행 및 상태 관리 명령어 — Compose 서비스를 제어하는 주요 명령어입니다.

로컬 터미널
# 백그라운드 실행
docker-compose up -d

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

# 특정 서비스만 재시작
docker-compose restart web

# 서비스 중지 (컨테이너 삭제 안 함)
docker-compose stop redis

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

3단계: 스케일링 — 특정 서비스의 컨테이너 수를 동적으로 늘립니다.

로컬 터미널
# web 서비스를 3개로 스케일 (포트 충돌에 주의)
# 포트 고정 없이 스케일 가능하게 ports 제거 필요
docker-compose up -d --scale web=3
docker-compose ps
# web_1, web_2, web_3 확인

4단계: 이미지 업데이트 — 새 이미지로 재빌드하고 재시작합니다.

로컬 터미널
# 최신 이미지 pull 후 재시작
docker-compose pull
docker-compose up -d

# 특정 서비스만 재빌드
docker-compose up -d --build web

5단계: 서비스 내부 접속 — 실행 중인 컨테이너에 셸로 접속합니다.

로컬 터미널
# 실행 중인 컨테이너에 명령 실행
docker-compose exec web sh
docker-compose exec redis redis-cli ping
# PONG

6단계: 정리 — 실습 환경을 깔끔하게 삭제합니다.


환경별 Compose 파일 분리 (개발/운영)

실무에서는 개발(dev)과 운영(prod) 환경의 설정을 분리하는 패턴을 사용합니다.

1단계: 기본 파일과 오버라이드 파일 구조 이해 — base 파일과 환경별 오버라이드 파일로 분리합니다.

project/ 구조입니다.

  • docker-compose.yml — 공통 설정
  • docker-compose.override.yml — 개발 환경 (자동 적용)
  • docker-compose.prod.yml — 운영 환경

2단계: 기본 파일 작성 — 모든 환경에서 공통으로 쓰는 설정을 작성합니다.

YAML
# docker-compose.yml (공통 설정)
version: "3.9"
services:
  app:
    image: myapp:latest
    environment:
      - APP_NAME=MyApplication
  db:
    image: postgres:14
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:

3단계: 개발 환경 오버라이드 — 개발 환경에서만 필요한 설정을 추가합니다.

YAML
# docker-compose.override.yml (개발용 — 자동 병합)
version: "3.9"
services:
  app:
    build: .          # 이미지 빌드 (개발 중 코드 변경 반영)
    volumes:
      - .:/app        # 소스코드 바인드 마운트 (핫리로드)
    environment:
      - APP_ENV=development
      - DEBUG=true
    ports:
      - "3000:3000"

4단계: 운영 환경 파일 — 운영에서는 리소스 제한과 재시작 정책을 강화합니다.

YAML
# docker-compose.prod.yml (운영용)
version: "3.9"
services:
  app:
    image: myapp:v1.2.0  # 고정 버전 태그 사용
    environment:
      - APP_ENV=production
      - DEBUG=false
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 512M

5단계: 환경별 실행-f 옵션으로 파일을 조합해서 실행합니다.

로컬 터미널
# 개발 환경 (override 자동 적용)
docker-compose up -d

# 운영 환경 (명시적으로 파일 지정)
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

증상

docker-compose up 후 WordPress나 Django 같은 웹 서비스에서 DB 연결 오류가 발생합니다.

wordpress | Error establishing a database connection
# 또는
app | django.db.utils.OperationalError: could not connect to server

원인 분석

로컬 터미널
# 타임라인 분석
docker-compose logs db | head -20
# 2024-01-01 00:00:00 MariaDB 시작 중...
# 2024-01-01 00:00:02 초기화 진행 중...
# 2024-01-01 00:00:08 준비 완료

docker-compose logs wordpress | head -10
# 2024-01-01 00:00:03 DB에 연결 시도... ← DB가 아직 준비 안 된 시점!
# 2024-01-01 00:00:03 연결 실패

depends_on만으로는 컨테이너 프로세스가 시작된 것만 확인하고, DB가 실제로 연결을 받을 준비가 됐는지는 알 수 없습니다.

해결 방법 1: healthcheck + condition (권장)

YAML
services:
  db:
    image: mariadb:10.6
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  wordpress:
    depends_on:
      db:
        condition: service_healthy
    restart: always

해결 방법 2: restart: always + 재연결 로직

애플리케이션 자체에 DB 연결 재시도 로직이 있다면, restart: always만으로도 해결됩니다.

YAML
services:
  app:
    restart: always  # 실패 시 자동 재시작
    depends_on:
      - db

검증 방법 — 헬스체크 설정이 실제로 동작하는지 확인합니다.

로컬 터미널
docker-compose up -d
docker-compose ps
# 서비스가 모두 Up 상태인지, Restarting 상태가 아닌지 확인
docker inspect wp_app --format '{{.RestartCount}}'
# 0 또는 낮은 숫자면 연결 문제 없음

증상

Error response from daemon: driver failed programming external connectivity:
Bind for 0.0.0.0:3306 failed: port is already allocated

원인

이전에 실행한 컨테이너가 완전히 삭제되지 않았거나, 호스트에서 동일 포트를 사용하는 프로세스가 실행 중입니다.

진단 및 해결

1. 이전 Compose 환경 확인 — 포트를 점유 중인 컨테이너가 있는지 확인합니다.

로컬 터미널
docker-compose ps -a
# 중지된 컨테이너가 아직 존재하면 포트를 점유 중일 수 있음

2. 완전히 정리 후 재시작 — 기존 컨테이너를 내리고 깨끗하게 재시작합니다.

로컬 터미널
docker-compose down
docker-compose up -d

3. 호스트 프로세스가 포트 사용 중인 경우 — 호스트에서 포트를 점유한 프로세스를 찾아 종료합니다.

로컬 터미널
# 포트 3306을 사용하는 프로세스 확인
sudo lsof -i :3306
# 또는
sudo ss -tulpn | grep 3306

# 해당 프로세스 종료 (예: 로컬 MySQL)
sudo systemctl stop mysql

4. Compose 파일에서 포트 변경 — 충돌하는 포트를 다른 값으로 변경합니다.

YAML
services:
  db:
    ports:
      - "3307:3306"  # 호스트 포트를 충돌 없는 번호로 변경

예방 방법: 개발 환경에서 로컬에 MySQL/PostgreSQL을 직접 설치하지 않고 모두 Docker로 관리하면 이런 충돌을 피할 수 있습니다.


심화 — 볼륨을 지우지 않았는데 데이터가 사라진 것처럼 보일 때

💡개념

심화: Compose의 프로젝트 네임스페이스 — 볼륨·네트워크는 프로젝트명에 묶인다

docker compose down -v를 한 적도 없는데 DB가 초기화된 것처럼 보이는 사고는, Compose가 리소스 이름을 어떻게 짓는지를 모르면 원인을 찾지 못합니다. Compose는 컨테이너·볼륨·네트워크에 프로젝트명을 접두어로 붙여 격리합니다.

  • 모든 리소스에 프로젝트명이 붙습니다: db_data 볼륨은 실제로 myapp_db_data로, 기본 네트워크는 myapp_default로, 컨테이너는 myapp-db-1로 만들어집니다. 여기서 myapp이 프로젝트명입니다.
  • 프로젝트명 기본값은 디렉토리명입니다: compose 파일이 있는 디렉토리 이름을 소문자로 바꾼 값이 기본 프로젝트명이 됩니다. 그래서 디렉토리를 rename하거나, 다른 경로에 다시 clone하거나, -p를 다르게 주면 프로젝트명이 바뀝니다.
  • 프로젝트명이 바뀌면 볼륨을 못 찾습니다: 같은 volumes: db_data 정의라도 접두어가 달라지면 Compose는 기존 볼륨을 찾지 못하고 새 빈 볼륨을 만들어 붙입니다. 옛 데이터는 삭제된 것이 아니라 옛 이름 볼륨에 그대로 남아 있습니다.
  • down도 프로젝트 단위입니다: 다른 프로젝트명으로 띄운 컨테이너는 지금 프로젝트의 down으로 내려가지 않고 orphan으로 남습니다.

프로젝트명을 최상위 name 키나 COMPOSE_PROJECT_NAME으로 고정해 두면, 경로가 바뀌어도 항상 같은 볼륨·네트워크에 붙습니다.

상황: 볼륨을 지우는 명령을 실행한 적이 없는데, docker compose up 후 WordPress가 다시 설치 화면을 띄우거나 DB가 비어 있습니다. 어제 저장한 데이터가 통째로 안 보입니다.

원인: 프로젝트 폴더를 다른 이름으로 옮겼거나 다른 경로에서 실행해 프로젝트명이 바뀌었습니다. 그 결과 볼륨 접두어가 달라져 Compose가 새 빈 볼륨(newname_db_data)을 만들어 마운트했습니다. 옛 데이터는 여전히 옛 볼륨(oldname_db_data)에 살아 있습니다 — 삭제가 아니라 다른 볼륨을 본 것입니다.

진단: docker volume ls로 접두어(프로젝트명)만 다른 비슷한 볼륨이 쌍으로 있는지 확인합니다. docker compose ls와 docker compose config로 지금 프로젝트명이 무엇으로 잡혔는지, 그리고 실행 디렉토리 이름이 바뀌지 않았는지 봅니다.

해결: 최상위 name 키(또는 COMPOSE_PROJECT_NAME, -p)로 프로젝트명을 원래 값으로 고정한 뒤 다시 올립니다. 이미 갈라진 옛 볼륨의 데이터를 되살리려면, 옛 볼륨을 external: true로 참조하거나 옛 볼륨의 내용을 새 볼륨으로 복사합니다. 재발 방지로 처음부터 name을 박아 두면 경로가 바뀌어도 안전합니다.


💼
실무 맥락
현업 패턴

현업에서 Docker Compose는 단순한 개발 도구를 넘어 중소 규모 서비스의 실제 배포 방식으로도 사용됩니다. 이 플랫폼의 e-commerce 예시처럼 여러 서비스를 효율적으로 관리하는 실무 패턴을 살펴봅니다.

실무 수준의 Compose 파일 구조

YAML
version: "3.9"

# 공통 설정 재사용 (앵커와 별칭)
x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

x-restart-policy: &restart-policy
  restart: unless-stopped

services:
  # API 게이트웨이
  gateway:
    <<: *restart-policy
    image: mycompany/gateway:${VERSION:-latest}
    ports:
      - "8080:8080"
    environment:
      JWT_SECRET: ${JWT_SECRET}
      RATE_LIMIT: 100
    logging: *default-logging
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - frontend
      - backend

  # 인증 서비스
  auth:
    <<: *restart-policy
    image: mycompany/auth:${VERSION:-latest}
    env_file: .env.auth
    logging: *default-logging
    networks:
      - backend

  # 데이터베이스
  postgres:
    <<: *restart-policy
    image: postgres:14-alpine
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d  # 초기화 SQL
    env_file: .env.db
    logging: *default-logging
    networks:
      - data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  frontend:
  backend:
  data:

volumes:
  pg_data:

운영팀이 자주 사용하는 명령 패킷

로컬 터미널
# 서비스 롤링 업데이트 (다운타임 최소화)
docker-compose up -d --no-deps --build auth

# 특정 서비스 로그 타임스탬프 포함 확인
docker-compose logs -f --timestamps gateway

# 전체 환경 상태 빠른 점검
docker-compose ps && docker stats --no-stream

# 환경변수 확인 (민감 정보 포함되므로 주의)
docker-compose config

Kubernetes로 이전하기 전 단계나 트래픽이 많지 않은 내부 서비스에는 Docker Compose가 운영 비용 대비 충분히 효율적인 선택입니다.


💡개념

docker compose 핵심 명령어 완전 정리

docker run에서 docker compose로 넘어가면 명령어 체계가 달라집니다. docker stop은 컨테이너를 중지하지만 docker compose stop은 서비스 단위로 중지하고, docker compose down은 컨테이너와 네트워크를 삭제합니다. exec는 실행 중인 컨테이너 안에서 명령을 실행하고, run은 새 임시 컨테이너를 별도로 만들어서 실행합니다. 이 차이를 모르면 마이그레이션을 실행할 때 실행 중인 서비스에 영향을 줄지, 별도 컨테이너에서 실행할지 혼동합니다. 팀이 Compose를 처음 도입할 때 가장 많이 헷갈리는 명령어들을 실무 사용 맥락과 함께 정리합니다. 이 ConceptBlock에서는 Compose 환경을 운영하는 데 필요한 핵심 명령어 전체를 다룹니다.

docker compose 핵심 명령어 — up -d(전체 기동)·stop(서비스 단위 중지)·down(컨테이너+네트워크 삭제)·exec(실행 중 컨테이너에서 명령)·run(새 임시 컨테이너)·logs·ps. docker 단일 컨테이너 명령과 달리 서비스 단위로 동작하는 차이를 정리확대

시작과 중지

Docker
# 백그라운드로 전체 서비스 실행
docker compose up -d

# 이미지 재빌드 후 실행 (소스코드 변경 시)
docker compose up -d --build

# 특정 서비스만 실행
docker compose up -d db redis

# 서비스 중지 + 컨테이너/네트워크 삭제
docker compose down

# 볼륨까지 삭제 (DB 데이터 포함 — 주의!)

# 컨테이너만 중지 (삭제 안 함)
docker compose stop

# 중지된 컨테이너 재시작
docker compose start

상태 확인

Docker
# 서비스 상태 목록
docker compose ps
# NAME            IMAGE     COMMAND     STATUS     PORTS
# myapp-web-1     myapp     "node ..."  Up 5 min   0.0.0.0:3000->3000/tcp
# myapp-db-1      postgres  "docker-…"  Up 5 min   5432/tcp

# 전체 서비스 로그 (최신 순)
docker compose logs

# 특정 서비스 실시간 로그
docker compose logs -f web

# 마지막 100줄
docker compose logs --tail 100 db

# 여러 서비스 동시에
docker compose logs -f web db

실행 중인 서비스 작업

Docker
# 실행 중인 서비스 컨테이너에 셸 접속
docker compose exec web sh
docker compose exec db psql -U postgres

# 명령어 실행
docker compose exec web ls /app

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

# 서비스 스케일 조정 (임시)
docker compose up -d --scale web=3

빌드와 이미지

Docker
# 모든 서비스 이미지 빌드
docker compose build

# 특정 서비스만 캐시 없이 재빌드
docker compose build --no-cache web

# 최신 이미지 pull
docker compose pull

일회성 실행 (임시 컨테이너)

Docker
# 실행 중인 서비스와 같은 환경에서 임시 컨테이너 실행
# 종료 후 자동 삭제 (--rm)
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py createsuperuser
docker compose run --rm web npm run build

docker compose rundocker compose exec와 다릅니다:

  • exec: 이미 실행 중인 컨테이너 안에서 실행
  • run: 해당 서비스의 새 임시 컨테이너를 별도로 생성해서 실행

💡개념

환경별 override 파일 패턴 — 개발/스테이징/프로덕션 분리

개발 환경에서는 소스코드를 bind mount로 실시간 반영해야 하고, 프로덕션에서는 이미지를 그대로 써야 합니다. 개발에서는 디버그 포트를 열어두지만 운영에서는 닫아야 합니다. 이 차이를 하나의 compose.yml에 환경변수 분기로 처리하면 파일이 복잡해지고 실수가 늘어납니다. override 파일은 공통 설정을 compose.yml에 두고 환경별 차이만 별도 파일로 분리합니다. docker compose upcompose.ymlcompose.override.yml을 자동으로 합쳐서 실행합니다. 프로덕션은 compose.ymlcompose.prod.yml을 명시적으로 지정합니다. 이 패턴을 이해하면 "개발에서는 볼륨 마운트 때문에 코드가 바로 반영되는데 운영에서는 왜 반영이 안 되지?"라는 혼란도 없어집니다. 이 ConceptBlock에서는 환경별 Compose override 패턴과 그 적용 방법을 다룹니다.

환경별 override 파일 패턴 — 공통 설정은 compose.yml에 두고, 개발(소스 bind mount·디버그 포트)과 운영(이미지 그대로·포트 닫음)의 차이만 compose.override.yml·compose.prod.yml로 분리. 한 파일에 환경 분기를 욱여넣어 복잡해지는 것을 피함확대

왜 override 파일을 쓰는가

환경마다 다른 설정(포트 노출, 볼륨 마운트, 리소스 제한 등)을 하나의 compose.yml에 if/else로 다루면 복잡해집니다. base + override 패턴으로 환경별 차이만 선언합니다.

디렉토리 구조:
  compose.yml              ← 공통 기반 (모든 환경 공유)
  compose.override.yml     ← 개발용 (docker compose up 시 자동 적용)
  compose.prod.yml         ← 프로덕션용 (명시적으로 지정)
  compose.staging.yml      ← 스테이징용

compose.yml — 공통 기반

YAML
services:
  app:
    image: myapp:${APP_VERSION:-latest}
    environment:
      - DB_HOST=db
      - DB_PORT=5432

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=appuser

volumes:
  db-data:

compose.override.yml — 개발 환경 (자동 적용)

YAML
# docker compose up 시 compose.yml + 이 파일이 자동 병합됨
services:
  app:
    build: .                    # 로컬 소스로 빌드
    volumes:
      - .:/app                  # 소스코드 핫 리로드
      - /app/node_modules       # node_modules는 컨테이너 것 유지
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DEBUG=true

  db:
    ports:
      - "5432:5432"             # 개발 시 DB 직접 접근
    environment:
      - POSTGRES_PASSWORD=devpassword

compose.prod.yml — 프로덕션 환경

YAML
services:
  app:
    restart: always
    ports:
      - "80:3000"
    environment:
      - NODE_ENV=production
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

  db:
    restart: always
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password

실행 방법

Docker
# 개발: compose.yml + compose.override.yml 자동 병합
docker compose up -d

# 프로덕션: 명시적으로 파일 지정 (override.yml 적용 안 됨)
docker compose -f compose.yml -f compose.prod.yml up -d

# 스테이징
docker compose -f compose.yml -f compose.staging.yml up -d

# 현재 어떻게 병합되는지 확인
docker compose config          # 개발 (자동 병합 결과)
docker compose -f compose.yml -f compose.prod.yml config  # 프로덕션

💡개념

Compose Profiles — 선택적 서비스 실행

Compose 파일에 Prometheus, Grafana, mailhog, Redis Commander까지 넣으면 개발할 때마다 모니터링 스택이 함께 뜹니다. 개발 시에는 mailhog만 필요하고, 성능 테스트 때는 k6와 Grafana가 필요하고, 운영 배포에는 둘 다 필요 없습니다. 이런 선택적 서비스를 profiles로 그룹화하면, 기본 docker compose up에서는 포함되지 않고 --profile dev처럼 필요할 때만 켤 수 있습니다. 하나의 Compose 파일로 개발, 테스트, 디버그 환경을 관리하되 불필요한 컨테이너가 뜨지 않게 하는 실무 패턴입니다. 이 ConceptBlock에서는 Compose profiles 문법과 실전 활용 패턴을 다룹니다.

Compose Profiles — 선택적 서비스 실행확대

profiles란?

항상 필요하지 않은 서비스(디버그 UI, 모니터링, 테스트 도구)를 profiles로 그룹화하면, 기본 실행 시 포함되지 않고 필요할 때만 활성화할 수 있습니다.

YAML
services:
  # profiles 없음 → 항상 실행
  app:
    image: myapp

  db:
    image: postgres:16-alpine

  # profiles: ["debug"] → --profile debug 시만 실행
  adminer:
    image: adminer
    profiles: ["debug"]
    ports:
      - "8080:8080"

  # profiles: ["monitoring"] → --profile monitoring 시만 실행
  prometheus:
    image: prom/prometheus
    profiles: ["monitoring"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana
    profiles: ["monitoring"]
    ports:
      - "3001:3000"

profiles 활성화 방법

Docker
# 기본 실행: profiles 없는 서비스만 (app + db)
docker compose up -d

# debug profile 활성화: + adminer
docker compose --profile debug up -d

# monitoring profile 활성화: + prometheus + grafana
docker compose --profile monitoring up -d

# 여러 profile 동시 활성화
docker compose --profile debug --profile monitoring up -d

# 환경변수로 활성화
COMPOSE_PROFILES=debug,monitoring docker compose up -d

실전 활용 예시

YAML
services:
  app:
    image: myapp

  db:
    image: postgres:16-alpine

  # 개발 중에만 필요한 서비스들
  mailhog:
    image: mailhog/mailhog     # 이메일 캡처 도구
    profiles: ["dev"]
    ports:
      - "8025:8025"

  redis-commander:
    image: rediscommander/redis-commander  # Redis GUI
    profiles: ["dev"]
    ports:
      - "8081:8081"

  # 성능 테스트 시에만 필요
  k6:
    image: grafana/k6
    profiles: ["perf"]
    volumes:
      - ./tests:/tests
    command: run /tests/load-test.js
Docker
# 개발 환경: 이메일 캡처 + Redis GUI 포함
docker compose --profile dev up -d

# 성능 테스트: k6 실행
docker compose --profile perf run --rm k6

정리

Docker Compose의 핵심을 요약합니다.

작업명령
백그라운드 실행docker-compose up -d
상태 확인docker-compose ps
로그 확인docker-compose logs -f [서비스명]
중지 및 삭제docker-compose down
데이터까지 삭제docker-compose down -v
컨테이너 내부 접속docker-compose exec [서비스] sh

depends_on만으로는 DB 준비 상태를 보장하지 못합니다. healthcheckcondition: service_healthy를 함께 사용하거나, 애플리케이션에 재연결 로직을 구현하는 것이 실무의 정석입니다.


명령어·단축키 빠른 참조

이 모듈에서 다룬 docker compose 서브커맨드를 실전 옵션과 함께 모았습니다. 단일 컨테이너용 docker 명령과 달리 서비스 단위로 동작합니다.

명령어/단축키용도자주 쓰는 예
docker compose up -d전체 스택 백그라운드 기동docker compose up -d --build (이미지 재빌드 후 기동)
docker compose down컨테이너+네트워크 삭제(볼륨 유지)docker compose down -v (볼륨까지 삭제 — 주의)
docker compose ps서비스 상태·포트 목록Up 아닌 서비스(Exited/Restarting)부터 확인
docker compose logs서비스 로그 확인docker compose logs -f --tail 100 web
docker compose exec실행 중 서비스에서 명령 실행docker compose exec db psql -U postgres
docker compose run --rm새 임시 컨테이너로 일회성 작업docker compose run --rm web python manage.py migrate
docker compose build서비스 이미지 빌드docker compose build --no-cache web
docker compose pull최신 이미지 내려받기docker compose pull
docker compose restart특정 서비스만 재시작docker compose restart web
docker compose up --scale서비스 인스턴스 수 조정docker compose up -d --scale web=3
docker compose config병합·변수 치환 결과 검증docker compose -f compose.yml -f compose.prod.yml config
-f (파일 겹치기)base+override 환경별 병합docker compose -f compose.yml -f compose.prod.yml up -d
--profile선택적 서비스 그룹만 활성화docker compose --profile monitoring up -d

관련 모듈로 더 깊이:

다음 모듈에서는 Compose 기반 개발 환경을 팀 표준으로 만들고, make dev 한 줄로 누구나 같은 환경을 시작하는 흐름을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

docker-compose.yml에서 `depends_on`을 설정하면 어떤 동작이 보장되나요?

Q2

docker-compose.yml에서 환경변수를 외부 파일로 분리할 때 사용하는 키는 무엇인가요?

Q3

`docker-compose down` 명령과 `docker-compose down -v` 명령의 차이는 무엇인가요?

Q4

Docker Compose에서 services 아래 정의한 네트워크가 없을 때의 기본 동작은?

Q5

WordPress + MariaDB 구성에서 웹이 DB보다 먼저 완전히 켜져서 연결 오류가 발생할 때 가장 권장되는 해결 방법은?

Q6

개발·스테이징·프로덕션마다 포트·볼륨·환경변수가 조금씩 다른데, compose 파일을 환경별로 통째로 복사하지 않고 관리하려면?

Q7

[심화] docker compose up으로 만들어지는 볼륨과 네트워크의 이름은 어떻게 정해지나?

Q8

[심화] down -v를 한 적이 없는데 docker compose up 후 DB가 빈 상태로 초기화된 것처럼 보인다. 원인을 찾는 첫 단계는?

0 / 8 답변

🧪 실습으로 확인하기

nginx 리버스 프록시 직접 구성하기

중급

Docker Compose로 nginx + 백엔드 컨테이너를 띄우고 nginx.conf를 직접 작성해 리버스 프록시를 구성한다. verify.sh로 라우팅이 실제로 동작하는지 자동 검증한다.

50📋 6단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요