Docker Compose 실전 가이드 | 멀티 컨테이너·네트워크·볼륨·프로덕션 배포

Docker Compose 실전 가이드 | 멀티 컨테이너·네트워크·볼륨·프로덕션 배포

이 글의 핵심

Docker Compose로 멀티 컨테이너 애플리케이션을 구성하고 배포하는 완벽 가이드입니다. 네트워크, 볼륨, 환경 변수, 헬스체크, 프로덕션 배포까지 실무에 바로 적용할 수 있습니다.

실무 경험 공유: 스트리밍 인프라의 모니터링 스택(Prometheus, Grafana, Loki)을 Docker Compose로 구축하면서, 개발 환경 구성 시간을 2시간에서 5분으로 단축한 경험을 공유합니다.

들어가며: “컨테이너 여러 개를 어떻게 관리하죠?”

실무 문제 시나리오

시나리오 1: 개발 환경 구성이 복잡해요
웹 서버, 데이터베이스, Redis, Nginx를 각각 설치하고 설정하는 데 하루가 걸립니다. Docker Compose로 docker compose up 한 번에 모든 환경을 구성할 수 있습니다.

시나리오 2: 팀원마다 환경이 달라요
”내 컴퓨터에서는 되는데요?” 문제가 반복됩니다. Docker Compose로 모든 팀원이 동일한 환경을 공유할 수 있습니다.

시나리오 3: 프로덕션 배포가 불안해요
개발 환경과 프로덕션 환경이 달라 배포 후 에러가 발생합니다. Docker Compose로 환경을 일치시킬 수 있습니다.

flowchart TB
    subgraph Before["문제 상황"]
        A1[수동 설치]
        A2[환경 불일치]
        A3[배포 실패]
    end
    subgraph After["Docker Compose"]
        B1[docker compose up]
        B2[동일 환경]
        B3[안정적 배포]
    end
    Before --> After

1. Docker Compose 기초

Docker Compose란?

여러 컨테이너를 정의하고 실행하는 도구입니다. YAML 파일로 서비스, 네트워크, 볼륨을 선언적으로 정의합니다.

설치

# Docker Desktop 설치 시 자동 포함
docker compose version

# Linux에서 별도 설치
sudo curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

최소 예제

# docker-compose.yml
version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
# 실행
docker compose up

# 백그라운드 실행
docker compose up -d

# 중지 및 삭제
docker compose down

2. 멀티 컨테이너 애플리케이션

실전 예제: 웹 애플리케이션 스택

# docker-compose.yml
version: '3.8'

services:
  # Nginx 웹 서버
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./public:/usr/share/nginx/html:ro
    depends_on:
      - app
    networks:
      - frontend

  # Node.js 애플리케이션
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:password@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - frontend
      - backend

  # PostgreSQL 데이터베이스
  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # Redis 캐시
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    networks:
      - backend

volumes:
  postgres_data:
  redis_data:

networks:
  frontend:
  backend:

서비스 간 의존성

flowchart LR
    subgraph Frontend["Frontend Network"]
        Nginx
        App
    end
    subgraph Backend["Backend Network"]
        App2[App]
        DB[PostgreSQL]
        Redis
    end
    Nginx --> App
    App --> App2
    App2 --> DB
    App2 --> Redis

3. 네트워크 구성

기본 네트워크

services:
  web:
    image: nginx
    networks:
      - frontend

  app:
    image: node:18
    networks:
      - frontend
      - backend

  db:
    image: postgres
    networks:
      - backend

networks:
  frontend:
  backend:

커스텀 네트워크

networks:
  frontend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

  backend:
    driver: bridge
    internal: true  # 외부 접근 차단

외부 네트워크 연결

networks:
  existing_network:
    external: true
    name: my-pre-existing-network

4. 볼륨 관리

Named Volumes

services:
  db:
    image: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
    driver: local

Bind Mounts

services:
  web:
    image: nginx
    volumes:
      # 호스트 경로:컨테이너 경로:옵션
      - ./nginx.conf:/etc/nginx/nginx.conf:ro  # 읽기 전용
      - ./public:/usr/share/nginx/html

tmpfs Mounts (메모리)

services:
  app:
    image: node:18
    tmpfs:
      - /tmp
      - /run:size=100M,mode=1777

볼륨 백업 및 복원

# 백업
docker run --rm -v postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres_backup.tar.gz -C /data .

# 복원
docker run --rm -v postgres_data:/data -v $(pwd):/backup alpine tar xzf /backup/postgres_backup.tar.gz -C /data

5. 환경 변수 및 시크릿

.env 파일

# .env
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret123
POSTGRES_DB=myapp
NODE_ENV=production
API_KEY=your-api-key
# docker-compose.yml
services:
  db:
    image: postgres
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}

환경 파일 분리

services:
  app:
    image: node:18
    env_file:
      - .env.common
      - .env.production

시크릿 관리 (Docker Swarm)

services:
  app:
    image: node:18
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true

6. 헬스체크 및 재시작 정책

헬스체크

services:
  app:
    image: node:18
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

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

재시작 정책

services:
  app:
    image: node:18
    restart: unless-stopped  # 수동 중지 전까지 재시작

  worker:
    image: node:18
    restart: on-failure:3  # 실패 시 최대 3회 재시작

  cache:
    image: redis
    restart: always  # 항상 재시작

7. 빌드 및 이미지 관리

Dockerfile과 통합

services:
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
      args:
        - NODE_VERSION=18
        - BUILD_ENV=production
      target: production  # 멀티스테이지 빌드의 특정 단계
      cache_from:
        - myapp:latest
# app/Dockerfile
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine AS base

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM base AS development
RUN npm install

FROM base AS production
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]

이미지 빌드 및 푸시

# 빌드
docker compose build

# 특정 서비스만 빌드
docker compose build app

# 빌드 후 실행
docker compose up --build

# 이미지 푸시
docker compose push

8. 프로덕션 배포

프로덕션 설정 분리

# docker-compose.yml (기본)
version: '3.8'
services:
  app:
    image: myapp:latest
    environment:
      - NODE_ENV=development
# docker-compose.prod.yml (프로덕션 오버라이드)
version: '3.8'
services:
  app:
    environment:
      - NODE_ENV=production
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
# 프로덕션 실행
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

리소스 제한

services:
  app:
    image: node:18
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          cpus: '1'
          memory: 512M

로깅 설정

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

9. 실전 예제: 풀스택 애플리케이션

프로젝트 구조

myapp/
├── docker-compose.yml
├── docker-compose.prod.yml
├── .env
├── nginx/
│   ├── Dockerfile
│   └── nginx.conf
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
├── backend/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
└── scripts/
    ├── init-db.sql
    └── backup.sh

docker-compose.yml

version: '3.8'

services:
  nginx:
    build: ./nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - frontend
      - backend
    networks:
      - frontend
    restart: unless-stopped

  frontend:
    build:
      context: ./frontend
      target: production
    environment:
      - NEXT_PUBLIC_API_URL=http://backend:3000
    networks:
      - frontend
    restart: unless-stopped

  backend:
    build:
      context: ./backend
      target: production
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:${DB_PASSWORD}@db:5432/myapp
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - frontend
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - backend
    restart: unless-stopped

  backup:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/data:ro
      - ./backups:/backups
    entrypoint: /bin/sh
    command: -c "while true; do pg_dump -U user -h db myapp > /backups/backup_$$(date +%Y%m%d_%H%M%S).sql; sleep 86400; done"
    depends_on:
      - db
    networks:
      - backend
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

networks:
  frontend:
  backend:

Nginx 설정

# nginx/nginx.conf
upstream frontend {
    server frontend:3000;
}

upstream backend {
    server backend:3000;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://frontend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

10. 자주 하는 실수와 해결법

문제 1: 컨테이너 간 통신 안 됨

원인: 같은 네트워크에 있지 않음.

# ❌ 잘못된 코드
services:
  app:
    image: node:18
    # 네트워크 지정 안 함

  db:
    image: postgres
    networks:
      - backend

# ✅ 올바른 코드
services:
  app:
    image: node:18
    networks:
      - backend

  db:
    image: postgres
    networks:
      - backend

문제 2: 볼륨 데이터 손실

원인: docker compose down -v로 볼륨까지 삭제.

# ❌ 볼륨까지 삭제
docker compose down -v

# ✅ 컨테이너만 삭제
docker compose down

문제 3: 환경 변수 인식 안 됨

원인: .env 파일 위치 또는 형식 오류.

# .env 파일은 docker-compose.yml과 같은 디렉터리에
# 공백 없이 작성
DB_PASSWORD=secret123
# ❌ DB_PASSWORD = secret123 (공백 있으면 안 됨)

문제 4: 포트 충돌

원인: 호스트 포트가 이미 사용 중.

# 포트 사용 확인
sudo lsof -i :80

# 다른 포트로 변경
services:
  nginx:
    ports:
      - "8080:80"  # 호스트 8080 포트 사용

11. 모니터링 및 로깅

Docker Compose 상태 확인

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

# 로그 확인
docker compose logs

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

# 실시간 로그
docker compose logs -f

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

리소스 사용량 모니터링

# 실시간 리소스 사용량
docker stats

# Compose 서비스만
docker compose ps -q | xargs docker stats

Prometheus + Grafana 통합

services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    volumes:
      - grafana_data:/var/lib/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

volumes:
  prometheus_data:
  grafana_data:

12. CI/CD 통합

GitHub Actions

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        run: |
          docker compose build
          docker compose push

      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /app
            docker compose pull
            docker compose up -d

정리 및 체크리스트

핵심 요약

  • Docker Compose는 멀티 컨테이너 애플리케이션을 YAML로 정의하고 관리
  • 네트워크로 서비스 간 통신을 격리하고 제어
  • 볼륨으로 데이터를 영구 저장
  • 헬스체크재시작 정책으로 안정성 확보
  • 환경별 설정 분리로 개발/프로덕션 환경 관리

프로덕션 체크리스트

  • 환경 변수를 .env 파일로 분리
  • 시크릿 정보는 .gitignore에 추가
  • 헬스체크 설정
  • 재시작 정책 설정
  • 리소스 제한 설정
  • 로깅 드라이버 설정
  • 백업 전략 수립
  • 모니터링 도구 연동

같이 보면 좋은 글

  • Kubernetes 실전 가이드 | Pod·Service·Deployment·Ingress
  • 웹 보안 완벽 가이드 | OWASP Top 10·XSS·CSRF·JWT
  • Next.js 15 완벽 가이드 | App Router·Server Actions·Turbopack

이 글에서 다루는 키워드

Docker, Docker Compose, 컨테이너, DevOps, 멀티 컨테이너, 네트워크, 볼륨, 배포, CI/CD

자주 묻는 질문 (FAQ)

Q. Docker Compose vs Kubernetes, 언제 뭘 쓰나요?

A. 소규모 프로젝트, 개발 환경은 Docker Compose를 권장합니다. 대규모, 멀티 클러스터, 자동 스케일링이 필요하면 Kubernetes를 사용하세요.

Q. 프로덕션에서 Docker Compose를 써도 되나요?

A. 단일 서버 배포는 가능합니다. 하지만 고가용성, 자동 스케일링이 필요하면 Kubernetes나 Docker Swarm을 고려하세요.

Q. 볼륨 데이터를 백업하려면?

A. docker run --rm -v volume_name:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data . 명령으로 백업할 수 있습니다.

Q. 컨테이너 간 통신이 안 되는데요?

A. 같은 네트워크에 있는지 확인하세요. docker compose logs로 네트워크 오류를 확인할 수 있습니다.

---
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3