목. 8월 14th, 2025

G: 안녕하세요! 복잡한 컨테이너 관리, 더 이상 걱정하지 마세요! 😌 오늘날 마이크로서비스 아키텍처와 컨테이너 기반 개발이 대세가 되면서, 우리는 수많은 도커 컨테이너를 다루게 됩니다. 웹 서버, 데이터베이스, 백엔드 API, 프론트엔드 빌드 등 각기 다른 컨테이너들이 서로 유기적으로 연결되어 동작해야 하죠. 🤔

이 모든 컨테이너를 일일이 docker run 명령어로 실행하고, 서로 연결하는 것은 매우 번거롭고 실수하기 쉽습니다. 이럴 때 빛을 발하는 것이 바로 Docker Compose입니다! ✨

Docker Compose는 여러 개의 도커 컨테이너를 정의하고 실행하는 도구입니다. 단일 docker-compose.yml 파일 하나로 애플리케이션의 모든 서비스를 구성하고, 한 번의 명령어로 전체 스택을 올리고 내릴 수 있게 해줍니다. 마치 오케스트라의 지휘자처럼, 다양한 악기(컨테이너)들을 조화롭게 연주시키는 역할을 하죠! 🎼

이번 글에서는 Docker Compose의 핵심인 docker-compose.yml 파일의 기본 구조를 완벽하게 이해하고, 실용적인 예제와 함께 자세히 알아보겠습니다. 이 글을 다 읽고 나면 여러분은 자신 있게 첫 번째 docker-compose.yml 파일을 작성할 수 있을 거예요! 👍


1. Docker Compose, 왜 필요한가요? 🤔

단일 컨테이너 애플리케이션이라면 docker run으로 충분합니다. 하지만 대부분의 실 서비스는 여러 컴포넌트로 구성됩니다. 예를 들어, 흔히 볼 수 있는 웹 서비스 스택은 다음과 같습니다:

  • 웹 서버: (Nginx, Apache) 웹 요청을 처리하고 정적 파일을 제공.
  • 애플리케이션 서버: (Node.js, Python Flask, Java Spring Boot) 비즈니스 로직 처리.
  • 데이터베이스: (PostgreSQL, MySQL, MongoDB) 데이터 저장.
  • 캐시 서버: (Redis, Memcached) 빠른 데이터 접근.

이러한 서비스들을 각각 수동으로 실행하고, 네트워크를 설정하고, 볼륨을 연결하는 것은 매우 비효율적입니다.

  • 수동 설정의 어려움: 각 컨테이너의 포트, 네트워크, 볼륨 등을 일일이 기억하고 명령어를 입력해야 합니다.
  • 환경 일관성 문제: 개발, 테스트, 운영 환경 간의 설정 차이로 인한 문제가 발생할 수 있습니다.
  • 협업의 비효율성: 팀원들이 동일한 환경을 구축하는 데 시간이 오래 걸립니다.

Docker Compose는 이러한 문제들을 해결해줍니다! 🛠️

  • 선언적 정의: 모든 서비스와 그 설정을 YAML 파일 하나에 명시합니다.
  • 단일 명령어 실행: docker compose up 명령 한 번으로 전체 스택을 쉽게 배포하고 관리할 수 있습니다.
  • 환경 일관성: 팀원 모두가 동일한 개발 환경을 신속하게 구축할 수 있습니다.
  • 재사용성: 한 번 정의한 설정은 언제든 재사용할 수 있습니다.

2. docker-compose.yml 파일, 기본 구조 뼈대 잡기! 🦴

docker-compose.yml 파일은 YAML(YAML Ain’t Markup Language) 형식으로 작성됩니다. YAML은 사람이 읽기 쉬운 데이터 직렬화 언어로, 들여쓰기를 통해 구조를 표현하는 것이 특징입니다. 들여쓰기가 매우 중요하니 주의하세요! (보통 2칸 공백을 사용합니다.)

docker-compose.yml 파일의 최상위 레벨에는 주로 다음 키들이 사용됩니다.

  • version: Docker Compose 파일 형식을 지정합니다. 최신 기능과 문법을 사용하기 위해 보통 3.x 버전을 명시합니다. 현재 3.8 이상이 널리 사용됩니다.
  • services: 애플리케이션을 구성하는 모든 컨테이너 서비스들을 정의하는 핵심 섹션입니다.
  • volumes: 컨테이너 간에 공유되거나 데이터를 영구적으로 저장할 볼륨을 정의합니다.
  • networks: 서비스들이 통신할 수 있는 네트워크를 정의합니다.

가장 기본적인 docker-compose.yml 파일의 뼈대는 다음과 같습니다.

# docker-compose.yml

version: '3.8' # Docker Compose 파일 형식 버전 지정

services:
  # 여기에 각 서비스(컨테이너) 정의가 들어갑니다.
  # 예: 웹 서버, 데이터베이스, 백엔드 앱 등

volumes:
  # 여기에 영속적인 데이터를 저장할 볼륨 정의가 들어갑니다.

networks:
  # 여기에 서비스 간 통신을 위한 네트워크 정의가 들어갑니다.

자, 이제 이 뼈대 안의 각 섹션들을 자세히 파헤쳐 볼까요? 🕵️‍♀️


3. 핵심 요소 완벽 분석: services 💡

services 섹션은 Docker Compose 파일에서 가장 중요하고 자주 사용되는 부분입니다. 이곳에서 여러분의 애플리케이션을 구성하는 각각의 컨테이너를 정의합니다. 각 서비스는 고유한 이름(예: web, db, api)을 가지며, 그 아래에 해당 컨테이너에 대한 다양한 설정을 명시합니다.

각 서비스에서 사용할 수 있는 주요 지시어들을 예시와 함께 살펴보겠습니다.

3.1. image: 기존 도커 이미지 사용하기

가장 간단하게 서비스를 정의하는 방법입니다. Docker Hub 등에서 제공하는 기존 이미지를 사용하여 컨테이너를 생성합니다.

services:
  nginx:
    image: nginx:latest # nginx 최신 이미지를 사용합니다.
  redis:
    image: redis:6-alpine # redis 6 버전의 alpine 경량 이미지를 사용합니다.

3.2. build: Dockerfile로부터 이미지 빌드하기

만약 여러분의 애플리케이션이 직접 만든 코드 기반이라면, Dockerfile을 통해 이미지를 빌드해야 합니다.

  • build: .: 현재 docker-compose.yml 파일이 있는 디렉토리에서 Dockerfile을 찾아서 빌드합니다.
  • build: ./my_app: my_app이라는 하위 디렉토리에서 Dockerfile을 찾아서 빌드합니다.
  • contextdockerfile: 특정 디렉토리를 빌드 컨텍스트로 지정하고, 그 안에 있는 특정 Dockerfile을 지정할 수 있습니다.
services:
  webapp:
    build: . # 현재 디렉토리의 Dockerfile로 이미지를 빌드합니다.
    # 또는
    # build:
    #   context: ./my_node_app # ./my_node_app 디렉토리를 빌드 컨텍스트로 사용
    #   dockerfile: Dockerfile.prod # my_node_app/Dockerfile.prod 파일을 사용

3.3. ports: 호스트와 컨테이너 포트 연결하기

컨테이너 내부에서 실행되는 애플리케이션에 외부에서 접근할 수 있도록 호스트 머신의 포트와 컨테이너의 포트를 연결합니다.

"호스트_포트:컨테이너_포트" 형식으로 지정합니다.

services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80"     # 호스트의 80번 포트를 컨테이너의 80번 포트에 연결
      - "443:443"   # 호스트의 443번 포트를 컨테이너의 443번 포트에 연결
  api:
    build: .
    ports:
      - "3000:3000" # 호스트의 3000번 포트를 컨테이너의 3000번 포트에 연결

3.4. volumes: 데이터 영속성 및 공유

컨테이너가 삭제되어도 데이터가 사라지지 않도록 하거나, 호스트와 컨테이너 간에 파일을 공유할 때 사용합니다.

  • 바인드 마운트 (host_path:container_path): 호스트 머신의 특정 경로를 컨테이너 내부 경로에 연결합니다. 주로 개발 시 코드 변경사항을 바로 반영하거나, 설정 파일을 마운트할 때 사용합니다.
  • 명명된 볼륨 (volume_name:container_path): Docker가 관리하는 이름 있는 볼륨을 사용합니다. 주로 데이터베이스처럼 영속적인 데이터가 필요한 경우에 사용하며, 아래 volumes 섹션에서 정의해야 합니다.
services:
  db:
    image: postgres:13
    volumes:
      - db_data:/var/lib/postgresql/data # 'db_data'라는 명명된 볼륨을 사용
  webapp:
    build: .
    volumes:
      - ./app:/usr/src/app # 현재 디렉토리의 app 폴더를 컨테이너 내부의 /usr/src/app에 마운트
      - /usr/src/app/node_modules # 이 경로는 호스트에 마운트하지 않음 (npm install 결과 저장)

3.5. environment: 환경 변수 설정

컨테이너 내부에서 사용할 환경 변수를 설정합니다. 민감한 정보는 env_file을 사용하는 것이 더 안전합니다.

services:
  api:
    build: .
    environment:
      - NODE_ENV=production
      - DB_HOST=db       # 데이터베이스 서비스 이름 'db'를 호스트로 사용
      - DB_USER=myuser
      - DB_PASSWORD=mypassword

3.6. env_file: 환경 변수 파일 로드

.env 파일 등 외부에 정의된 환경 변수들을 로드할 때 사용합니다. 보안과 설정 분리에 유용합니다.

# .env 파일 예시:
# DB_HOST=db
# DB_PORT=5432
# API_KEY=your_secret_key

services:
  api:
    build: .
    env_file:
      - ./.env # 현재 디렉토리의 .env 파일에서 환경 변수를 로드
      - ./config/secrets.env # 다른 경로의 파일도 로드 가능

3.7. depends_on: 서비스 의존성 정의 (시작 순서)

특정 서비스가 다른 서비스가 시작된 후에 시작되어야 할 때 사용합니다. 예를 들어, 웹 애플리케이션은 데이터베이스가 먼저 시작되어야 제대로 동작할 수 있습니다.

주의: depends_on시작 순서만 보장하며, 서비스가 ‘준비되었음(ready)’을 보장하지 않습니다. 즉, db 컨테이너가 시작되었지만 데이터베이스 서비스 자체가 아직 포트 리스닝을 시작하지 않았을 수도 있습니다. 완전한 준비 상태를 확인하려면 healthcheck를 사용하는 것이 좋습니다.

services:
  webapp:
    build: .
    depends_on:
      - db    # db 서비스가 시작된 후에 webapp 서비스가 시작됩니다.
      - redis # redis 서비스가 시작된 후에 webapp 서비스가 시작됩니다.
  db:
    image: postgres:13
  redis:
    image: redis:6-alpine

3.8. networks: 특정 네트워크에 연결

서비스를 특정 네트워크에 연결합니다. 기본적으로 모든 서비스는 하나의 기본 네트워크에 연결되지만, 명시적으로 네트워크를 정의하고 연결하면 서비스 간의 격리나 통신 구조를 명확히 할 수 있습니다.

services:
  web:
    image: nginx
    networks:
      - frontend_network # frontend_network에 연결
      - backend_network  # backend_network에도 연결
  api:
    build: .
    networks:
      - backend_network  # backend_network에 연결
  db:
    image: postgres
    networks:
      - backend_network  # backend_network에 연결

networks:
  frontend_network: # frontend_network 정의
  backend_network:  # backend_network 정의

3.9. container_name: 컨테이너 이름 지정

docker ps 등으로 확인할 때 컨테이너에 알아보기 쉬운 이름을 지정할 수 있습니다. 지정하지 않으면 디렉토리명-서비스명-1과 같은 형태로 자동 생성됩니다.

services:
  nginx:
    image: nginx
    container_name: my_awesome_nginx

3.10. restart: 컨테이너 재시작 정책

컨테이너가 종료되었을 때 자동으로 재시작할지 여부를 설정합니다.

  • no: 재시작 안 함 (기본값)
  • always: 컨테이너가 종료될 때마다 항상 재시작합니다.
  • on-failure: 컨테이너가 비정상적으로 종료되었을 때(exit code가 0이 아닐 때)만 재시작합니다.
  • unless-stopped: 컨테이너가 수동으로 중지될 때까지 재시작합니다.
services:
  api:
    build: .
    restart: always # 항상 재시작
  db:
    image: postgres
    restart: on-failure # 비정상 종료 시 재시작

3.11. command / entrypoint: 컨테이너 시작 명령 오버라이드

Dockerfile에 정의된 기본 명령을 덮어쓰거나 추가 명령을 실행할 때 사용합니다.

  • command: 컨테이너가 시작될 때 실행될 명령을 정의합니다. Dockerfile의 CMD를 오버라이드합니다.
  • entrypoint: 컨테이너가 시작될 때 가장 먼저 실행될 명령을 정의합니다. Dockerfile의 ENTRYPOINT를 오버라이드합니다. commandentrypoint의 인자처럼 동작합니다.
services:
  my_app:
    image: my_custom_image
    command: ["python", "app.py", "--debug"] # 기본 명령 대신 이 명령으로 시작
  db_client:
    image: postgres
    entrypoint: ["psql"] # psql 클라이언트로 바로 접속
    command: ["-U", "postgres", "-h", "db"] # psql에 전달할 인자

3.12. healthcheck: 서비스 건강 상태 확인

서비스가 실제로 준비되었는지(예: 웹 서버가 200 OK 응답을 주는지, 데이터베이스가 연결 가능한지) 주기적으로 확인합니다. depends_on의 한계를 보완해줍니다.

services:
  web:
    image: nginx
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"] # HTTP 200 응답 확인
      interval: 1m30s # 1분 30초마다 검사
      timeout: 10s    # 10초 내에 응답 없으면 실패
      retries: 3      # 3번 실패하면 unhealthy
      start_period: 40s # 처음 40초 동안은 healthcheck 실패해도 unhealthy로 간주 안 함

4. 데이터 영속성을 위한 volumes 💾

volumes 섹션은 컨테이너가 삭제되더라도 데이터를 보존하고 싶을 때 사용합니다. Docker Compose 파일에서 명시적으로 볼륨을 정의하면, Docker가 이 볼륨을 생성하고 관리해줍니다.

volumes:
  db_data:     # 'db_data'라는 이름의 볼륨 정의
  app_logs:    # 'app_logs'라는 이름의 볼륨 정의
    driver: local # 기본 드라이버는 'local' (명시하지 않아도 됨)
    driver_opts:
      type: nfs
      o: addr=192.168.1.1,rw
      device: ":/path/to/nfs/share" # NFS 서버 연결 예시

이처럼 정의된 볼륨은 위에서 본 services 섹션의 volumes 지시어에서 db_data:/var/lib/postgresql/data 와 같이 사용될 수 있습니다.


5. 서비스 간 통신을 위한 networks 🔗

기본적으로 Docker Compose는 모든 서비스가 포함된 하나의 default 네트워크를 생성합니다. 따라서 같은 docker-compose.yml 파일 내의 서비스들은 서로의 서비스 이름을 사용하여 통신할 수 있습니다 (예: webapp 서비스에서 db 서비스로 연결할 때 호스트를 db로 지정).

하지만 더 복잡한 애플리케이션이나 보안상 분리가 필요할 때, 직접 네트워크를 정의할 수 있습니다.

networks:
  frontend: # 'frontend' 네트워크 정의
  backend:  # 'backend' 네트워크 정의
    driver: bridge # 기본 드라이버는 bridge (명시하지 않아도 됨)
    ipam: # IP 주소 관리 설정 (선택 사항)
      config:
        - subnet: 172.20.0.0/16
          gateway: 172.20.0.1

이렇게 정의된 네트워크는 위에서 본 services 섹션의 networks 지시어에서 networks: - frontend 와 같이 사용됩니다.

네트워크 사용의 장점:

  • 격리: 특정 서비스만 특정 네트워크에 참여시켜 불필요한 통신을 차단할 수 있습니다.
  • 명확한 구조: 서비스 간의 통신 관계를 시각적으로 명확히 보여줍니다.
  • IP 주소 관리: ipam을 통해 특정 IP 대역을 할당할 수 있습니다.

6. docker-compose.yml 실행하기 🚀

이제 docker-compose.yml 파일 작성을 마쳤다면, 이를 실행하는 방법을 알아봐야겠죠?

💡 참고: 최신 Docker 버전에서는 docker-compose 명령어가 docker compose (하이픈 없이)로 통합되었습니다. 하지만 기존 docker-compose도 여전히 잘 동작합니다. 이 글에서는 일반적으로 사용되는 docker-compose를 기준으로 설명합니다.

docker-compose.yml 파일이 있는 디렉토리에서 다음 명령어를 실행합니다.

  • docker-compose up: docker-compose.yml 파일에 정의된 모든 서비스를 백그라운드가 아닌 포그라운드(현재 터미널)에서 실행하고 로그를 출력합니다. 개발 중 로그를 실시간으로 확인하고 싶을 때 유용합니다.
  • docker-compose up -d: 모든 서비스를 백그라운드(detached mode)에서 실행합니다. 터미널을 닫아도 컨테이너는 계속 실행됩니다.
  • docker-compose down: docker-compose.yml 파일에 정의된 모든 서비스 컨테이너를 중지하고 삭제합니다. 이 때 기본적으로 볼륨은 삭제되지 않습니다. 볼륨까지 삭제하려면 docker-compose down -v를 사용합니다.
  • docker-compose ps: 현재 실행 중인 Compose 서비스 목록을 보여줍니다.
  • docker-compose logs [서비스명]: 특정 서비스의 로그를 확인합니다. 서비스명을 지정하지 않으면 모든 서비스의 로그를 보여줍니다.
  • docker-compose build: 서비스에 build 지시어가 있는 경우, 해당 이미지를 미리 빌드합니다. up 명령 시 자동으로 빌드되지만, 미리 빌드하여 에러를 확인하고 싶을 때 사용합니다.

7. 실전 예제: 웹 애플리케이션 스택 구축! 🌐

이제 위에서 배운 내용을 바탕으로 간단한 웹 애플리케이션 스택을 구축해보겠습니다. 이 스택은 다음과 같이 구성됩니다:

  1. Nginx: 웹 서버 역할을 하며, 프론트엔드 정적 파일 서빙 및 백엔드 API로 요청을 프록시합니다.
  2. Node.js API: 간단한 RESTful API를 제공합니다. (/api 경로로 요청이 들어오면 응답)
  3. MongoDB: API 서버가 사용할 데이터베이스입니다.

프로젝트 구조:

my-web-app/
├── docker-compose.yml
├── nginx/
│   └── nginx.conf
└── backend/
    ├── Dockerfile
    └── app.js

backend/app.js (간단한 Node.js Express 앱):

// backend/app.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/api', (req, res) => {
  res.json({ message: 'Hello from Node.js API!', timestamp: new Date() });
});

app.listen(port, () => {
  console.log(`Backend API listening at http://localhost:${port}`);
});

backend/Dockerfile:

# backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

nginx/nginx.conf (Nginx 설정 파일):

# nginx/nginx.conf
events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        server_name localhost;

        # 정적 파일 (예: index.html)은 여기서 직접 서빙할 수 있지만,
        # 이 예제에서는 단순화를 위해 생략했습니다.
        # 실제 프론트엔드 앱이 있다면 여기에 root 및 index를 설정합니다.

        location /api {
            proxy_pass http://backend:3000; # backend 서비스의 3000번 포트로 프록시
            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 / {
            root /usr/share/nginx/html; # Nginx 기본 정적 파일 경로 (테스트용)
            index index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
    }
}

docker-compose.yml:

# docker-compose.yml

version: '3.8'

services:
  nginx:
    image: nginx:latest
    container_name: my_app_nginx
    ports:
      - "80:80" # 호스트 80번 포트를 Nginx 80번 포트에 연결
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # Nginx 설정 파일을 읽기 전용으로 마운트
      # - ./frontend/build:/usr/share/nginx/html # 실제 프론트엔드 빌드 파일을 마운트할 경우
    depends_on:
      - backend # backend 서비스가 시작된 후에 Nginx 시작
    networks:
      - app_network # app_network에 연결

  backend:
    build: ./backend # backend 폴더의 Dockerfile로 이미지 빌드
    container_name: my_app_backend
    environment:
      # 실제 MongoDB 연결 정보는 여기에 추가
      MONGODB_URI: mongodb://mongodb:27017/mydatabase # MongoDB 서비스 이름 'mongodb' 사용
    ports:
      - "3000:3000" # 개발용으로 호스트 포트도 열어둠 (Nginx 프록시를 통해 접근 가능)
    depends_on:
      - mongodb # mongodb 서비스가 시작된 후에 backend 시작
    networks:
      - app_network # app_network에 연결

  mongodb:
    image: mongo:latest # MongoDB 최신 이미지 사용
    container_name: my_app_mongodb
    volumes:
      - mongo_data:/data/db # 명명된 볼륨을 사용하여 데이터 영속성 확보
    networks:
      - app_network # app_network에 연결

networks:
  app_network: # 모든 서비스가 공유할 네트워크 정의

volumes:
  mongo_data: # MongoDB 데이터를 저장할 명명된 볼륨 정의

실행 방법:

  1. 위의 파일들을 각각의 경로에 맞게 생성합니다.
  2. my-web-app 디렉토리로 이동합니다.
  3. 터미널에서 다음 명령어를 실행합니다: docker-compose up -d
  4. 컨테이너들이 성공적으로 실행되었는지 확인합니다: docker-compose ps

접근 테스트:

  • 웹 브라우저에서 http://localhost/api 에 접속해보세요. Node.js API가 응답하는 JSON 메시지를 볼 수 있을 거예요! 🎉

이 예제를 통해 여러분은 Docker Compose가 여러 서비스를 어떻게 한데 묶고, 서로 통신하게 하며, 데이터를 영속적으로 관리하는지 명확히 이해할 수 있을 것입니다.


결론: Docker Compose, 이제 여러분의 무기입니다! ✨

지금까지 Docker Compose의 docker-compose.yml 파일 기본 구조와 핵심 요소들을 자세히 살펴보았습니다.

  • version: 파일 형식 버전 명시
  • services: 개별 컨테이너 서비스 정의 (image, build, ports, volumes, environment, depends_on, networks 등)
  • volumes: 영속적인 데이터 저장을 위한 볼륨 정의
  • networks: 서비스 간 통신을 위한 네트워크 정의

Docker Compose는 복잡한 다중 컨테이너 애플리케이션의 개발 및 배포를 놀라울 정도로 단순화시켜 줍니다. 이제 여러분은 단일 YAML 파일로 전체 애플리케이션 스택을 관리할 수 있게 되었어요! 🥳

다음 단계는 무엇일까요?

  • 직접 예제를 수정하고 실행해보세요: 포트 번호를 바꿔보거나, 다른 이미지를 추가해보는 등 직접 코드를 만져보는 것이 가장 좋은 학습 방법입니다.
  • 고급 기능 탐색: extends, profiles, configs, secrets, deploy 등 Docker Compose의 더 많은 고급 기능들을 학습하여 프로덕션 환경에 가까운 설정을 구성해보세요.
  • Docker Swarm 또는 Kubernetes: Docker Compose는 주로 개발 환경이나 소규모 프로덕션에 적합합니다. 대규모 분산 시스템에서는 Docker Swarm이나 Kubernetes와 같은 오케스트레이션 도구로 확장하게 됩니다.

Docker Compose는 컨테이너 기술의 강력함을 최대한 활용할 수 있도록 돕는 필수 도구입니다. 이 글이 여러분의 컨테이너 여정에 훌륭한 출발점이 되기를 바랍니다! 🎓

궁금한 점이 있다면 언제든지 댓글로 질문해주세요! 해피 도커링! 🐳

답글 남기기

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