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:
- Bind Mount (바인드 마운트):
-
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
디렉토리로 이동한 후, 다음 명령어를 실행합니다:
-
서비스 빌드 및 시작 (백그라운드 실행):
docker compose up -d --build
up
:docker-compose.yml
파일에 정의된 모든 서비스를 시작합니다.-d
: 서비스를 백그라운드(detached mode)에서 실행합니다. 터미널을 닫아도 서비스가 계속 실행됩니다.--build
: 이미지가 없는 경우 빌드하고, Dockerfile이 변경된 경우 재빌드합니다.
-
서비스 상태 확인:
docker compose ps
실행 중인 모든 컨테이너의 상태를 확인할 수 있습니다.
Up
상태로 표시되면 성공! ✅ -
로그 확인:
docker compose logs -f
모든 서비스의 실시간 로그를 확인할 수 있습니다.
-f
옵션은 로그를 계속 따라가면서 보여줍니다. 특정 서비스의 로그만 보려면docker compose logs -f backend
와 같이 서비스 이름을 지정합니다. -
애플리케이션 접속: 웹 브라우저에서
http://localhost:80
으로 접속합니다. “Welcome to the Dockerized To-Do App! 🐳” 메시지가 보이면 성공입니다!http://localhost:80/todos
로 접속하면 현재는 빈 배열([]
)이 보일 겁니다. POST 요청으로 데이터를 추가해보세요! -
서비스 중지 및 삭제:
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를 적극적으로 적용해보시고, 더욱 효율적인 개발 및 배포 워크플로우를 경험해보세요! 궁금한 점이 있다면 언제든지 댓글로 남겨주세요! 행복한 도커링 되세요! 🐳✨