infra
Platform

모듈 맵

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

0 / 27 완료

펼치기
0 / 27 완료0%

Docker · 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 핵심 구조

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

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

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 후 컨테이너는 사라지고 볼륨은 유지되는 차이를 설명할 수 있는가?

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로 관리하면 이런 충돌을 피할 수 있습니다.


💼
실무 맥락
현업 패턴

현업에서 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 핵심 명령어 완전 정리

시작과 중지

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 파일 패턴

왜 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를 함께 사용하거나, 애플리케이션에 재연결 로직을 구현하는 것이 실무의 정석입니다.

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

지식 확인

퀴즈 — 5문제

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보다 먼저 완전히 켜져서 연결 오류가 발생할 때 가장 권장되는 해결 방법은?

0 / 5 답변

🧪 실습으로 확인하기

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

중급

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

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

이것도 배워보세요

docker중급 · 45
[Docker] CPU/메모리 리밋 설정으로 서버 먹통 방지하는 cgroups
Docker 트랙 계속
networking입문 · 45
[Network] OSI 7계층과 TCP/IP 4계층 모델 실무적 관점 분석
Networking 트랙 시작점