화. 8월 12th, 2025

G: 안녕하세요! 🐳 도커 컨테이너를 하나씩 docker run 명령어로 실행하는 데 지치셨나요? 백엔드, 데이터베이스, 프론트엔드, 캐시 서버 등 여러 서비스가 복잡하게 얽혀 있는 프로젝트를 배포할 때마다 수많은 명령어를 입력하는 건 여간 번거로운 일이 아닐 수 없습니다. 하지만 걱정 마세요! ✨ Docker Compose가 여러분의 복잡한 배포 과정을 마법처럼 단순하게 만들어 줄 테니까요!

이번 글에서는 Docker Compose가 무엇인지부터 시작해, 어떻게 docker-compose.yml 파일을 작성하고 실제 서비스에 적용하는지 실전 예제를 통해 자세히 알아보겠습니다. 이 글을 다 읽고 나면, 여러분도 여러 컨테이너를 손쉽게 오케스트레이션하는 마스터가 될 수 있을 거예요! 💡


1. Docker Compose, 너는 누구니? 🤔

Docker Compose는 여러 개의 도커 컨테이너를 정의하고 실행하기 위한 도구입니다. 복잡한 애플리케이션 서비스들을 하나의 파일(docker-compose.yml)에 정의하여 관리하고, 단일 명령어로 손쉽게 시작하거나 중지할 수 있도록 도와줍니다. 마치 오케스트라의 지휘자처럼, 여러 악기(컨테이너)들이 서로 조화롭게 연주(실행)될 수 있도록 관리해주는 역할을 하죠. 🎼

왜 Docker Compose를 사용해야 할까요?

  • 간편한 관리: 여러 컨테이너를 하나의 파일로 정의하여 관리하므로, docker run 명령어를 여러 번 입력할 필요가 없습니다.
  • 재현 가능한 환경: 개발, 테스트, 프로덕션 환경을 동일하게 구성할 수 있어 “내 컴퓨터에서는 되는데…” 같은 문제를 줄여줍니다.
  • 쉬운 협업: 팀원 간 환경 설정 공유가 매우 쉬워집니다. docker-compose.yml 파일만 공유하면 끝!
  • 빠른 배포/삭제: docker compose up 한 줄이면 모든 서비스가 배포되고, docker compose down 한 줄이면 깔끔하게 삭제됩니다.

2. docker-compose.yml 파일 해부하기 🕵️‍♀️

docker-compose.yml 파일은 YAML 형식으로 작성되며, 도커 컨테이너를 정의하는 핵심 파일입니다. 주요 구성 요소를 하나씩 살펴볼까요?

2.1. 기본 구조 뼈대 🏗️

version: '3.8' # Docker Compose 파일 형식 버전 (최신 버전 3.x 사용 권장)

services:    # 정의할 서비스(컨테이너)들을 나열합니다.
  서비스_이름1:
    # 서비스_이름1에 대한 설정
  서비스_이름2:
    # 서비스_이름2에 대한 설정

networks:    # 서비스들이 통신할 네트워크를 정의합니다. (선택 사항)
  네트워크_이름:
    # 네트워크 설정

volumes:     # 데이터를 영구적으로 저장할 볼륨을 정의합니다. (선택 사항)
  볼륨_이름:
    # 볼륨 설정

2.2. services ⚙️: 각 컨테이너 정의하기

services 섹션은 Docker Compose의 핵심입니다. 여기에 정의된 각 항목은 하나의 독립적인 컨테이너로 실행됩니다.

  • image: Docker Hub에 있는 이미지를 사용하여 컨테이너를 생성합니다.
    services:
      web:
        image: nginx:latest # nginx 최신 이미지 사용
  • build: Dockerfile을 사용하여 직접 이미지를 빌드합니다.
    • context: Dockerfile이 위치한 경로를 지정합니다. 보통 ./ (현재 디렉토리)를 사용합니다.
    • dockerfile: 사용할 Dockerfile의 이름을 지정합니다. (기본값: Dockerfile)
      services:
      app:
      build:
        context: . # 현재 디렉토리에서 Dockerfile을 찾음
        dockerfile: Dockerfile.dev # 'Dockerfile.dev' 파일을 사용
  • ports: 호스트(내 컴퓨터)와 컨테이너 간의 포트 매핑을 설정합니다. HOST_PORT:CONTAINER_PORT 형식으로 지정합니다.
    services:
      web:
        image: nginx
        ports:
          - "80:80"       # 호스트의 80번 포트를 컨테이너의 80번 포트에 연결
          - "443:443"     # 호스트의 443번 포트를 컨테이너의 443번 포트에 연결
  • environment: 컨테이너 내부에서 사용할 환경 변수를 설정합니다. 민감한 정보는 .env 파일로 분리하는 것이 좋습니다.
    services:
      app:
        image: myapp
        environment:
          - DATABASE_URL=mongodb://mongo:27017/mydatabase
          - NODE_ENV=development
          # 또는
          # DATABASE_URL: ${MONGO_URI} # .env 파일에서 MONGO_URI 값을 가져옴
  • volumes: 컨테이너와 호스트 간에 데이터를 공유하거나 컨테이너 내부 데이터를 영구적으로 저장할 때 사용합니다.

    • Bind Mount (바인드 마운트): HOST_PATH:CONTAINER_PATH 형식으로 호스트의 특정 경로를 컨테이너에 연결합니다. 개발 시 코드 변경사항을 바로 반영할 때 유용합니다.
    • Named Volume (이름 지정 볼륨): VOLUME_NAME:CONTAINER_PATH 형식으로 Docker가 관리하는 이름 지정 볼륨을 사용합니다. 데이터베이스 데이터와 같이 영구적인 데이터 저장에 적합합니다.
      
      services:
      app:
      image: myapp
      volumes:
        - ./app:/usr/src/app # 호스트의 './app' 폴더를 컨테이너의 '/usr/src/app'에 연결
        - data_volume:/var/lib/mysql # 'data_volume'이라는 이름의 볼륨을 사용

    volumes: # (별도 섹션) data_volume:

  • networks: 해당 서비스가 참여할 네트워크를 지정합니다. 지정하지 않으면 기본(bridge) 네트워크에 연결됩니다.

    services:
      web:
        image: nginx
        networks:
          - my_app_network # 'my_app_network'라는 네트워크에 연결
    
    networks: # (별도 섹션)
      my_app_network:
        driver: bridge # 브릿지 네트워크 드라이버 사용
  • depends_on: 서비스 간의 의존성을 정의합니다. 명시된 서비스가 먼저 시작된 후에 현재 서비스가 시작됩니다. ⚠️ 주의: depends_on은 서비스의 “시작 순서”만 보장하며, 해당 서비스가 “완전히 준비”되었는지(예: 데이터베이스 연결 준비 완료)는 보장하지 않습니다. 이를 위해서는 healthcheck를 함께 사용하는 것이 좋습니다.
    services:
      web:
        image: myapp
        depends_on:
          - db # 'db' 서비스가 먼저 시작된 후에 'web' 서비스가 시작됨
      db:
        image: postgres
  • command / entrypoint: 컨테이너가 시작될 때 실행할 명령을 정의합니다. Dockerfile의 CMD/ENTRYPOINT를 재정의합니다.
  • restart: 컨테이너 종료 시 재시작 정책을 설정합니다. (예: always, on-failure, unless-stopped)
    services:
      app:
        image: myapp
        restart: always # 항상 재시작 시도

3. 실전 예제: Node.js 웹 앱 + MongoDB + Nginx 배포하기 🌐 DB 🛡️

이제 실제 애플리케이션을 Docker Compose로 배포하는 과정을 살펴보겠습니다. 간단한 Node.js 웹 애플리케이션(To-Do 앱)을 만들고, 데이터를 MongoDB에 저장하며, Nginx를 리버스 프록시로 사용하여 서비스를 배포해볼게요!

3.1. 프로젝트 구조 📁

my-todo-app/
├── nginx/
│   └── nginx.conf         # Nginx 설정 파일
├── backend/
│   ├── Dockerfile         # Node.js 앱 Dockerfile
│   ├── package.json       # Node.js 의존성
│   └── app.js             # Node.js 앱 코드
└── docker-compose.yml     # Docker Compose 설정 파일

3.2. backend/app.js (Node.js 예제 코드) 💻

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

const app = express();
const PORT = process.env.PORT || 3000;
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/todoapp'; // 기본값 설정

mongoose.connect(MONGO_URI)
  .then(() => console.log('MongoDB Connected! ✅'))
  .catch(err => console.error('MongoDB connection error:', err));

const TodoSchema = new mongoose.Schema({
  title: String,
  completed: Boolean
});
const Todo = mongoose.model('Todo', TodoSchema);

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Welcome to the Dockerized To-Do App! 🐳');
});

app.get('/todos', async (req, res) => {
  const todos = await Todo.find();
  res.json(todos);
});

app.post('/todos', async (req, res) => {
  const newTodo = new Todo(req.body);
  await newTodo.save();
  res.status(201).json(newTodo);
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT} 🚀`);
});

3.3. backend/package.json 📦

// backend/package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "Node.js Todo Backend",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.6.3"
  }
}

3.4. backend/Dockerfile 📜

# backend/Dockerfile
FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

3.5. nginx/nginx.conf (Nginx 설정 파일) 🛡️

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

http {
    server {
        listen 80; # 호스트의 80번 포트를 통해 들어오는 요청을 받음

        location / {
            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;
        }
    }
}

설명: Nginx는 80번 포트로 들어오는 모든 요청을 backend 서비스의 3000번 포트로 전달합니다. Docker Compose 네트워크 내에서는 서비스 이름(backend)을 사용하여 다른 컨테이너에 접근할 수 있습니다.

3.6. docker-compose.yml 파일 작성 📝

이제 이 모든 서비스를 연결할 docker-compose.yml 파일을 작성합니다.

# docker-compose.yml
version: '3.8'

services:
  # 1. Node.js 백엔드 서비스
  backend:
    build:
      context: ./backend # backend 폴더의 Dockerfile을 사용
    ports:
      - "3000:3000" # 개발 중 직접 접근을 위해 포트 매핑 (선택 사항)
    environment:
      # MongoDB 컨테이너의 서비스 이름 (db)과 기본 포트 (27017)를 사용
      MONGO_URI: mongodb://db:27017/todoapp
      PORT: 3000
    depends_on:
      - db # db 서비스가 먼저 시작되도록 의존성 설정
    networks:
      - app-network # 'app-network' 네트워크에 연결

  # 2. MongoDB 데이터베이스 서비스
  db:
    image: mongo:latest # 최신 MongoDB 이미지 사용
    ports:
      - "27017:27017" # 로컬에서 직접 MongoDB에 연결할 경우를 대비 (선택 사항)
    volumes:
      - mongo-data:/data/db # 'mongo-data'라는 이름의 볼륨을 사용하여 데이터 영속성 유지
    networks:
      - app-network # 'app-network' 네트워크에 연결

  # 3. Nginx 리버스 프록시 서비스
  nginx:
    image: nginx:latest # 최신 Nginx 이미지 사용
    ports:
      - "80:80" # 호스트의 80번 포트를 Nginx의 80번 포트에 연결
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # 호스트의 nginx.conf 파일을 컨테이너에 마운트 (읽기 전용)
    depends_on:
      - backend # backend 서비스가 먼저 시작되도록 의존성 설정
    networks:
      - app-network # 'app-network' 네트워크에 연결

# 네트워크 정의
networks:
  app-network:
    driver: bridge # 기본 브릿지 네트워크 사용

# 볼륨 정의
volumes:
  mongo-data: # MongoDB 데이터를 위한 이름 지정 볼륨

3.7. docker compose 명령어로 실행하기 🚀

my-todo-app 디렉토리로 이동한 후, 다음 명령어를 실행합니다:

  1. 서비스 빌드 및 시작 (백그라운드 실행):

    docker compose up -d --build
    • up: docker-compose.yml 파일에 정의된 모든 서비스를 시작합니다.
    • -d: 서비스를 백그라운드(detached mode)에서 실행합니다. 터미널을 닫아도 서비스가 계속 실행됩니다.
    • --build: 이미지가 없는 경우 빌드하고, Dockerfile이 변경된 경우 재빌드합니다.
  2. 서비스 상태 확인:

    docker compose ps

    실행 중인 모든 컨테이너의 상태를 확인할 수 있습니다. Up 상태로 표시되면 성공! ✅

  3. 로그 확인:

    docker compose logs -f

    모든 서비스의 실시간 로그를 확인할 수 있습니다. -f 옵션은 로그를 계속 따라가면서 보여줍니다. 특정 서비스의 로그만 보려면 docker compose logs -f backend 와 같이 서비스 이름을 지정합니다.

  4. 애플리케이션 접속: 웹 브라우저에서 http://localhost:80으로 접속합니다. “Welcome to the Dockerized To-Do App! 🐳” 메시지가 보이면 성공입니다! http://localhost:80/todos로 접속하면 현재는 빈 배열([])이 보일 겁니다. POST 요청으로 데이터를 추가해보세요!

  5. 서비스 중지 및 삭제:

    docker compose down

    실행 중인 모든 컨테이너를 중지하고 제거합니다. 데이터 볼륨까지 완전히 삭제하려면:

    docker compose down --volumes

    ⚠️ --volumes 옵션은 volumes 섹션에 정의된 이름 지정 볼륨(mongo-data)을 포함하여 모든 데이터를 영구적으로 삭제하므로 주의해서 사용하세요!


4. Docker Compose 활용 팁 및 모범 사례 💡

  • .env 파일 활용: 민감한 정보(비밀번호, API 키 등)나 환경별로 달라지는 값은 docker-compose.yml 파일에 직접 넣는 대신 .env 파일에 정의하고, $VARIABLE_NAME 또는 ${VARIABLE_NAME} 형식으로 참조하세요. docker compose 명령어를 실행하는 디렉토리에 .env 파일을 두면 자동으로 로드됩니다.
    # .env 파일
    MONGO_DB_USER=myuser
    MONGO_DB_PASSWORD=mypassword
    # docker-compose.yml
    services:
      db:
        environment:
          MONGO_INITDB_ROOT_USERNAME: ${MONGO_DB_USER}
          MONGO_INITDB_ROOT_PASSWORD: ${MONGO_DB_PASSWORD}
  • 여러 Compose 파일 사용: 개발 환경과 프로덕션 환경 등 용도에 따라 다른 설정을 사용하고 싶다면 여러 docker-compose.yml 파일을 조합할 수 있습니다.

    • docker-compose.yml (공통 설정)
    • docker-compose.dev.yml (개발 환경 오버라이드)
    • docker-compose.prod.yml (프로덕션 환경 오버라이드)

    실행 시: docker compose -f docker-compose.yml -f docker-compose.dev.yml up

  • healthcheck 활용: depends_on은 서비스 시작 순서만 보장하므로, 서비스가 완전히 준비되었는지 확인하려면 healthcheck를 사용하는 것이 좋습니다.
    services:
      backend:
        # ...
        depends_on:
          db:
            condition: service_healthy # db 서비스가 healthcheck를 통과해야 시작
      db:
        # ...
        healthcheck:
          test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
          interval: 5s
          timeout: 2s
          retries: 10
  • 볼륨은 이름을 지정하여 사용: 익명 볼륨(ex: /data/db)보다는 이름 지정 볼륨(mongo-data:/data/db)을 사용하여 데이터를 명확하게 관리하고, docker compose down --volumes 시 의도치 않은 데이터 삭제를 방지하세요.
  • 커스텀 네트워크 사용: 모든 서비스가 기본 브릿지 네트워크에 있어도 통신은 가능하지만, 명확한 서비스 간의 통신 관계를 위해 커스텀 네트워크를 정의하고 서비스를 연결하는 것이 좋습니다.

5. 마무리하며 🎉

축하합니다! 🎉 이제 여러분은 Docker Compose를 사용하여 여러 컨테이너 애플리케이션을 효율적으로 배포하고 관리하는 방법을 익히셨습니다. 복잡한 환경 설정과 번거로운 배포 과정은 이제 옛말이 될 거예요.

Docker Compose는 개발 환경 구축, CI/CD 파이프라인 구성, 소규모 프로덕션 배포 등 다양한 시나리오에서 강력한 도구로 활용될 수 있습니다. 오늘 배운 내용을 바탕으로 여러분의 프로젝트에 Docker Compose를 적극적으로 적용해보시고, 더욱 효율적인 개발 및 배포 워크플로우를 경험해보세요! 궁금한 점이 있다면 언제든지 댓글로 남겨주세요! 행복한 도커링 되세요! 🐳✨

답글 남기기

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