Docker Compose로 멀티 컨테이너 환경 구축하기: 단계별 튜토리얼

주제 키워드: Docker Compose, 멀티 컨테이너, YAML 구성, 의존성 관리, CI/CD 통합

개인 프로젝트나 사내 인프라를 구축할 때 스프링 부트 웹 애플리케이션과 PostgreSQL DB를 각각 수동으로 docker run 명령어로 띄우다 보니 컨테이너 간 사설 네트워크가 안 묶여서 DB 연결 에러가 나거나, 서버 재부팅 시 DB 컨테이너가 먼저 안 떠서 웹이 뻗는 문제를 겪었습니다. 단일 YAML 파일 하나로 서비스 의존성(depends_on), 내부 네트워크, 영구 볼륨까지 한 번에 관리해 주는 Docker Compose를 도입하면서 이 삽질을 완전히 끝낼 수 있었습니다. 실무에서 바로 써먹을 수 있는 멀티 컨테이너 구성법과 운영 팁을 공유합니다.

1. Docker Compose의 핵심 개념과 장점

1.1 멀티 컨테이너 아키텍처의 필요성

단일 컨테이너로 모든 기능을 구현하면 관심사 분리가 어렵고, 확장성이 제한됩니다. 예를 들어, 웹 서버와 데이터베이스를 하나의 컨테이너에 묶으면:

  • DB 업데이트 시 전체 애플리케이션 재빌드 필요
  • 각 서비스의 독립적인 확장(Scale-out) 불가
  • 로깅/모니터링의 복잡성 증가

Docker Compose는 이런 문제를 해결하며, 서비스 간 의존성을 명확히 정의하고 환경별 구성을 쉽게 전환할 수 있습니다. 공식 문서(Docker Compose Overview)에서도 강조하는 핵심 이점은 다음과 같습니다:

  • 선언적 구성: YAML 파일로 인프라 상태를 코드로 관리
  • 의존성 자동 처리: depends_on으로 컨테이너 시작 순서 제어
  • 환경 변수 분리: .env 파일로 민감 정보 관리

1.2 단일 YAML 파일의 효율성

예를 들어, 웹 애플리케이션과 PostgreSQL DB를 연결하는 구성은 다음과 같이 단순화됩니다:

# docker-compose.yml
version: '3.8'
services:
  webapp:
    image: my-webapp:latest
    ports:
      - "8000:8000"
    environment:
      - DB_HOST=db
      - DB_USER=user
      - DB_PASSWORD=secret
    depends_on:
      - db
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres_data:/var/lib/postgresql/data
volumes:
  postgres_data:

2. 사전 준비: 개발 환경 설정

2.1 Docker 및 Docker Compose 설치

  • Linux (Ubuntu 기준):
sudo apt-get update
sudo apt-get install docker.io docker-compose
  • macOS: Docker Desktop 설치 후 자동 포함
  • Windows: Docker Desktop 설치 시 WSL2와 함께 Compose 활성화

확인 명령:

docker --version
# Docker version 24.0.2, Build cb71016

docker-compose version
# docker-compose version 1.29.2, build 5becea4c

2.2 네트워크 및 볼륨 개념

  • 네트워크: 컨테이너 간 통신을 위한 가상 네트워크. 기본적으로 Compose는 프로젝트명_default 네트워크를 생성하지만, 커스텀 네트워크도 정의 가능합니다.
  • 볼륨: 호스트와 컨테이너 간 데이터 영구 저장. 위 예제의 postgres_data는 DB 데이터를 호스트에 보관하여 컨테이너 재생성 시에도 데이터 유지

3. 단계별 본문: 기본 구성 실습

3.1 간단한 웹 애플리케이션 예제

Python Flask 애플리케이션과 Redis 캐시를 연동하는 예제를 구현해 보겠습니다.

3.1.1 애플리케이션 코드 작성

# app.py
from flask import Flask
import redis
import os

app = Flask(__name__)
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = redis.StrictRedis(host=redis_host, port=6379, db=0)

@app.route('/')
def hello():
    redis_client.incr('hits')
    return f"Total hits: {redis_client.get('hits').decode('utf-8')"\n
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8000)

3.1.2 Docker 이미지 빌드

# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . ./
CMD ["python", "app.py"]

# requirements.txt
flask
redis

3.1.3 Docker Compose 파일 작성

# docker-compose.yml
version: '3.8'
services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      REDIS_HOST: redis
    depends_on:
      - redis
  redis:
    image: redis:7.0
    ports:
      - "6379:6379"

3.1.4 실행 및 테스트

# 빌드 및 실행
$ docker-compose up --build

# 별도 터미널에서 테스트
$ curl http://localhost:8000
# 출력: Total hits: 1

3.2 의존성 관리의 중요성

depends_on은 컨테이너 시작 순서만 제어할 뿐, DB 준비 완료 상태를 보장하지 않습니다. 이를 해결하려면 wait-for-it.sh 같은 툴을 사용하거나, 애플리케이션 수준에서 재시도 로직을 구현해야 합니다.

4. 고급 설정: 프로덕션 환경 최적화

4.1 커스텀 네트워크와 환경 변수 분리

4.1.1 네트워크 정의 예제

# docker-compose.yml
version: '3.8'
networks:
  app-network:
    driver: bridge
services:
  web:
    networks:
      - app-network
  db:
    networks:
      - app-network

4.1.2 .env 파일 활용

# .env
DB_USER=admin
DB_PASSWORD=strong_password
# docker-compose.yml
env_file:
  - .env
services:
  web:
    environment:
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}

4.2 프로필 기반 구성 (dev/prod)

Compose 1.27+부터 profiles를 사용해 환경별 서비스를 활성화할 수 있습니다.

# docker-compose.yml
version: '3.8'
services:
  web:
    image: my-webapp:latest
  db:
    image: postgres:15
  devtools:
    image: node:18
    command: npm run dev
    profiles:
      - dev
  monitoring:
    image: prom/prometheus
    profiles:
      - prod

실행 방법:

# 개발 환경
$ docker-compose --profile dev up

# 프로덕션 환경
$ docker-compose --profile prod up

5. 트러블슈팅: 흔한 문제와 해결법

5.1 컨테이너 간 연결 실패

  • 증상: Connection refused 오류 발생
  • 원인: DB가 준비되기 전에 웹 애플리케이션이 연결을 시도
  • 해결:
    1. depends_on에 condition 추가
    2. Dockerize 같은 툴로 헬스 체크
# 조건 추가 예제
depends_on:
  db:
    condition: service_healthy

5.2 볼륨 마운트 권한 문제

  • 증상: Permission denied 오류
  • 해결:
    1. 호스트 디렉터리 권한 변경
    2. Docker Compose에서 user 지정
# 권한 문제 해결 예제
volumes:
  - type: volume
    source: myvol
    target: /app/data
    volume:
      nocopy: true

5.3 로그 분석

  • 컨테이너 로그 확인:
$ docker-compose logs [서비스명]
# 예: docker-compose logs web
  • 실시간 모니터링:
$ docker-compose logs -f web

6. 마치며: Docker Compose의 확장성

  1. CI/CD 통합: GitHub Actions, GitLab CI에서 docker-compose 명령어로 테스트 환경 구축 가능
  2. 확장성: Kubernetes로의 전환을 위해 Compose 파일을 Helm 차트로 변환 가능
  3. 모니터링: Prometheus + Grafana와 연동해 컨테이너 메트릭 수집

추가 학습 자료:

이 가이드를 통해 멀티 컨테이너 환경의 설계부터 운영까지의 핵심 역량을 습득할 수 있습니다. 실제 프로덕션 환경에서는 보안 강화(예: TLS 암호화)와 리소스 제한(예: --memory, --cpus)을 추가로 적용해야 합니다.