G: 안녕하세요! 🚀 컨테이너 오케스트레이션의 핵심 도구 중 하나인 Docker Compose에 대해 궁금하신가요? 복잡한 여러 개의 도커 컨테이너를 한 번에 관리하고 싶으신가요? 그렇다면 이 글이 여러분의 궁금증을 시원하게 해결해 드릴 겁니다! 💡
이 가이드에서는 Docker Compose의 기본 개념부터 핵심 YAML 문법, 실전 예제, 그리고 배포 전략까지, 모든 것을 상세하게 다룰 예정입니다. 이제 함께 Docker Compose의 세계로 떠나볼까요? 🐳
1. Docker Compose, 너는 누구인가? 🤔
Docker Compose는 여러 개의 Docker 컨테이너로 구성된 애플리케이션을 정의하고 실행하기 위한 도구입니다. 간단한 YAML 파일을 사용하여 애플리케이션의 서비스, 네트워크, 볼륨 등을 한 번에 설정하고 관리할 수 있게 해줍니다.
💡 왜 Docker Compose를 사용해야 할까요?
- 간편한 멀티 컨테이너 관리: 웹 서버, 데이터베이스, 백엔드 서비스 등 여러 컨테이너를 각각
docker run
명령어로 실행할 필요 없이, 단 하나의 명령어로 모든 서비스를 시작하고 중지할 수 있습니다. - 재현 가능한 개발 환경:
docker-compose.yml
파일 하나로 팀원 모두가 동일한 개발 환경을 구축할 수 있어 “내 컴퓨터에서는 되는데…” 문제를 줄여줍니다. - 버전 관리 용이: 설정 파일이 코드처럼 관리되므로, 변경 사항을 쉽게 추적하고 롤백할 수 있습니다.
- 빠른 개발 및 테스트: 로컬 환경에서 복잡한 서비스 스택을 빠르게 띄워 개발 및 테스트 효율을 높일 수 있습니다.
한 마디로, Docker Compose는 여러분의 도커 컨테이너들을 위한 “오케스트라 지휘자”와 같습니다! 🎼
2. Docker Compose의 핵심! docker-compose.yml
파일 파헤치기 📚
Docker Compose의 모든 마법은 바로 docker-compose.yml
(또는 docker-compose.yaml
) 파일에서 시작됩니다. 이 파일은 YAML 형식으로 작성되며, 여러분의 애플리케이션 서비스들을 어떻게 구성할지 정의합니다.
기본적인 구조는 다음과 같습니다:
version: '3.8' # Docker Compose 파일 형식 버전 (필수)
services: # 애플리케이션을 구성하는 각 서비스 정의
서비스_이름_1:
이미지_정보
포트_매핑
볼륨_매핑
환경_변수
...
서비스_이름_2:
...
networks: # 서비스 간 통신을 위한 네트워크 정의 (선택 사항)
네트워크_이름_1:
드라이버_정보
volumes: # 데이터 지속성을 위한 볼륨 정의 (선택 사항)
볼륨_이름_1:
드라이버_정보
자, 이제 각 섹션을 자세히 살펴보겠습니다.
2.1. version
(버전 명시)
- Docker Compose 파일 형식의 버전을 지정합니다. 최신 기능과 문법을 사용하려면 가장 높은 안정 버전을 사용하는 것이 좋습니다. 일반적으로
'3.8'
을 많이 사용합니다. - 예시:
version: '3.8'
2.2. services
(핵심 서비스 정의) ✨
services
섹션은 여러분의 애플리케이션을 구성하는 개별 컨테이너(서비스)들을 정의하는 곳입니다. 각 서비스는 고유한 이름을 가지며, 해당 서비스가 어떻게 실행될지 다양한 속성으로 상세하게 설정할 수 있습니다.
-
image
: 🐳 이 서비스가 사용할 Docker 이미지입니다. Docker Hub에서 가져오거나, 로컬에 빌드된 이미지를 사용할 수 있습니다.- 예시:
image: nginx:latest
,image: mongo:4.4
- 예시:
-
build
: 🛠️ 이 서비스의 이미지를 직접 빌드하고 싶을 때 사용합니다.context
: Dockerfile이 위치한 경로를 지정합니다.dockerfile
: 사용할 Dockerfile의 이름을 지정합니다 (기본값은Dockerfile
).- 예시:
services: webapp: build: context: ./app # 현재 디렉터리 아래 app 폴더에 Dockerfile이 있음 dockerfile: Dockerfile.dev # app/Dockerfile.dev 파일을 사용
-
ports
: 🚪 컨테이너의 포트를 호스트 머신의 포트로 매핑합니다.호스트_포트:컨테이너_포트
형식으로 작성합니다.- 예시:
ports: - "80:80"
(호스트의 80번 포트를 컨테이너의 80번 포트로 연결),- "3000:3000"
- 예시:
-
volumes
: 📂 데이터의 지속성을 위해 호스트 머신의 디렉터리나 Docker 볼륨을 컨테이너 내부의 경로에 연결합니다.- 바인드 마운트 (Bind Mount):
호스트_경로:컨테이너_경로
– 개발 환경에서 소스 코드 변경 시 즉시 반영되어 편리합니다.- 예시:
- "./app:/usr/src/app"
(현재 디렉터리 아래app
폴더를 컨테이너/usr/src/app
에 연결)
- 예시:
- 명명된 볼륨 (Named Volume):
볼륨_이름:컨테이너_경로
– Docker가 관리하는 볼륨으로, 데이터 영속성이 중요할 때 사용합니다. (아래volumes
섹션에서 정의해야 함)- 예시:
- "db_data:/data/db"
- 예시:
- 바인드 마운트 (Bind Mount):
-
environment
: ⚙️ 컨테이너 내부에서 사용할 환경 변수를 설정합니다. 민감한 정보는.env
파일을 활용하는 것이 좋습니다.- 예시:
environment: - NODE_ENV=production - DB_HOST=database - DB_PORT=27017
.env
파일 사용 예시:# .env 파일 내용 DB_USER=admin DB_PASSWORD=mysecretpassword
# docker-compose.yml 파일 내용 services: db: image: postgres environment: - POSTGRES_USER=${DB_USER} # .env 파일의 DB_USER 값을 가져옴 - POSTGRES_PASSWORD=${DB_PASSWORD} # .env 파일의 DB_PASSWORD 값을 가져옴
- 예시:
-
depends_on
: 🔗 서비스 간의 의존성을 정의합니다. 명시된 서비스가 먼저 시작된 후에 해당 서비스가 시작됩니다.- 주의:
depends_on
은 시작 순서만 보장하며, 서비스가 완전히 “준비”되었음을 보장하지는 않습니다. (예: 데이터베이스가 완전히 부팅되고 연결을 받을 준비가 되었는지 보장하지 않음) 이를 위해서는healthcheck
나 애플리케이션 레벨의 재시도 로직이 필요합니다. - 예시:
services: webapp: depends_on: - database # database 서비스가 시작된 후에 webapp 서비스 시작 database: image: mongo
- 주의:
-
networks
: 🌐 해당 서비스가 연결될 네트워크를 지정합니다. 기본적으로 Docker Compose는 모든 서비스가 속하는 하나의 기본 네트워크를 생성합니다. 사용자 정의 네트워크를 사용할 경우 명시적으로 연결해야 합니다.- 예시:
services: frontend: networks: - frontend_network backend: networks: - frontend_network - backend_network networks: # 아래 networks 섹션에서 정의해야 함 frontend_network: backend_network:
- 예시:
-
command
&entrypoint
: ✍️ 컨테이너가 시작될 때 실행될 명령어를 오버라이드하거나 추가합니다.command
: 이미지의 기본CMD
를 오버라이드합니다.entrypoint
: 이미지의 기본ENTRYPOINT
를 오버라이드합니다.- 예시:
services: my_app: image: my_custom_image command: ["npm", "start"] # 컨테이너 시작 시 npm start 실행 # entrypoint: ["/usr/bin/my_custom_script"]
-
restart
: 🔄 컨테이너 종료 시 재시작 정책을 정의합니다.no
: 재시작하지 않음 (기본값)always
: 컨테이너가 종료되거나 Docker 데몬이 재시작될 때 항상 재시작on-failure
: 비정상적인 종료(종료 코드 0이 아님) 시에만 재시작unless-stopped
: 컨테이너가 수동으로 중지되지 않는 한 항상 재시작- 예시:
restart: always
-
healthcheck
: 💖 서비스의 상태를 주기적으로 확인하여 컨테이너가 “건강한” 상태인지 여부를 파악합니다.depends_on
과 함께 사용하여 의존성 컨테이너가 실제로 준비되었는지 확인하는 데 유용합니다.- 예시:
services: db: image: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s # 5초마다 테스트 timeout: 5s # 테스트 타임아웃 5초 retries: 5 # 실패 시 5번 재시도 start_period: 10s # 시작 후 10초간은 헬스체크 대기
- 예시:
2.3. networks
(네트워크 정의) 🌐
서비스 간 격리된 통신 환경을 구축하거나, 특정 서비스 그룹만 통신하도록 할 때 사용합니다. bridge
, overlay
등 다양한 드라이버를 사용할 수 있습니다.
- 예시:
networks: frontend_network: driver: bridge # 기본값이지만 명시 가능 backend_network: driver: bridge
2.4. volumes
(볼륨 정의) 💾
데이터의 영속성을 위해 명명된 볼륨을 정의합니다. 데이터베이스 파일 등 컨테이너가 삭제되어도 보존되어야 하는 데이터를 저장하는 데 사용됩니다.
- 예시:
volumes: db_data: # db_data라는 이름의 볼륨 정의 driver: local # 로컬 드라이버 사용 (기본값) static_files: # 정적 파일을 위한 볼륨
3. Docker Compose 기본 명령어 💻
docker-compose.yml
파일을 작성했다면, 이제 몇 가지 간단한 명령어로 애플리케이션을 관리할 수 있습니다.
-
docker-compose up
: 🚀docker-compose.yml
파일에 정의된 모든 서비스를 빌드하고 시작합니다.docker-compose up
: 포그라운드에서 실행되며 로그를 출력합니다.docker-compose up -d
: 백그라운드 (detached mode)에서 실행합니다. 서비스가 시작된 후 터미널 제어권을 돌려줍니다. (가장 많이 사용)
-
docker-compose down
: 🛑 실행 중인 모든 서비스를 중지하고 컨테이너를 제거합니다.docker-compose down -v
: 컨테이너와 함께 연결된 볼륨까지 제거합니다. (데이터베이스 등 영속적인 데이터가 필요 없는 경우 주의해서 사용)
-
docker-compose ps
: 📄 현재 실행 중인 서비스들의 상태를 보여줍니다. -
docker-compose logs [서비스_이름]
: 📜 특정 서비스 또는 전체 서비스의 로그를 확인합니다.docker-compose logs -f webapp
:webapp
서비스의 로그를 실시간으로 추적합니다.
-
docker-compose exec [서비스_이름] [명령어]
: 🖥️ 실행 중인 특정 서비스 컨테이너 내부에서 명령어를 실행합니다.docker-compose exec webapp bash
:webapp
컨테이너 내부로 들어가 bash 쉘을 실행합니다.
-
docker-compose build [서비스_이름]
: 🔨 특정 서비스 또는 모든 서비스의 이미지를 다시 빌드합니다.build
명령어가 있는 서비스에만 해당됩니다. -
docker-compose stop [서비스_이름]
: ⏸️ 특정 서비스를 중지합니다. (제거는 하지 않음) -
docker-compose start [서비스_이름]
: ▶️ 중지된 서비스를 다시 시작합니다.
4. 실전 예제: Nginx + Node.js + MongoDB 애플리케이션 👨💻
가장 흔하게 볼 수 있는 웹 애플리케이션 스택을 Docker Compose로 구축해보겠습니다.
- Nginx: 리버스 프록시 및 정적 파일 서빙
- Node.js (Express): 백엔드 API 서버
- MongoDB: 데이터베이스
4.1. 프로젝트 구조
my-mern-app/
├── app/
│ ├── Dockerfile # Node.js 앱을 위한 Dockerfile
│ ├── package.json # Node.js 의존성
│ └── server.js # Node.js Express 앱
├── nginx/
│ └── nginx.conf # Nginx 설정 파일
└── docker-compose.yml # Docker Compose 메인 파일
4.2. 파일 내용 작성
my-mern-app/app/Dockerfile
:
# Node.js 18 버전 기반 이미지 사용
FROM node:18-alpine
# 작업 디렉터리 설정
WORKDIR /usr/src/app
# package.json 및 package-lock.json 복사
COPY package*.json ./
# 의존성 설치
RUN npm install
# 앱 소스 코드 복사
COPY . .
# 앱이 사용할 포트 노출
EXPOSE 3000
# 앱 실행 명령어
CMD ["npm", "start"]
my-mern-app/app/package.json
:
{
"name": "node-app",
"version": "1.0.0",
"description": "Simple Node.js app",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.6.3"
}
}
my-mern-app/app/server.js
:
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const port = 3000;
// 환경 변수에서 MongoDB 연결 정보 가져오기
const DB_HOST = process.env.DB_HOST || 'localhost';
const DB_PORT = process.env.DB_PORT || '27017';
const DB_NAME = process.env.DB_NAME || 'testdb';
const MONGODB_URI = `mongodb://${DB_HOST}:${DB_PORT}/${DB_NAME}`;
mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('MongoDB connected! 🚀'))
.catch(err => console.error('MongoDB connection error:', err));
// MongoDB 스키마 및 모델 정의 (간단 예시)
const ItemSchema = new mongoose.Schema({
name: String,
description: String
});
const Item = mongoose.model('Item', ItemSchema);
app.use(express.json());
// API 엔드포인트
app.get('/', (req, res) => {
res.send('Hello from Node.js App! 🎉');
});
app.get('/items', async (req, res) => {
try {
const items = await Item.find();
res.json(items);
} catch (error) {
res.status(500).send(error.message);
}
});
app.post('/items', async (req, res) => {
try {
const newItem = new Item(req.body);
await newItem.save();
res.status(201).json(newItem);
} catch (error) {
res.status(400).send(error.message);
}
});
app.listen(port, () => {
console.log(`Node.js app listening at http://localhost:${port}`);
});
my-mern-app/nginx/nginx.conf
:
events {
worker_connections 1024;
}
http {
upstream backend {
server node_app:3000; # node_app 서비스의 3000번 포트로 연결
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://backend; # 모든 요청을 backend (node_app)로 전달
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 /static/ {
# alias /usr/share/nginx/html/static/;
# }
}
}
my-mern-app/docker-compose.yml
:
version: '3.8'
services:
nginx:
image: nginx:latest
ports:
- "80:80" # 호스트의 80포트를 Nginx의 80포트로 연결
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # Nginx 설정 파일 마운트 (읽기 전용)
depends_on:
- node_app # node_app이 시작된 후 Nginx 시작
networks:
- app_network # Nginx와 node_app이 동일한 네트워크 사용
node_app:
build: ./app # app 폴더의 Dockerfile을 사용하여 이미지 빌드
ports:
- "3000:3000" # 개발 시 테스트를 위해 호스트 포트도 열어둠 (선택 사항)
volumes:
- ./app:/usr/src/app # 소스 코드 변경 시 즉시 반영되도록 바인드 마운트
- /usr/src/app/node_modules # node_modules는 호스트에서 제외 (도커 컨테이너 내부의 것을 사용)
environment:
DB_HOST: mongo_db # MongoDB 서비스 이름을 호스트로 지정
DB_PORT: 27017
DB_NAME: testdb
depends_on:
- mongo_db # mongo_db가 시작된 후 node_app 시작
networks:
- app_network # Nginx와 node_app, mongo_db가 동일한 네트워크 사용
mongo_db:
image: mongo:4.4 # MongoDB 이미지 사용
ports:
- "27017:27017" # 개발 시 테스트를 위해 호스트 포트도 열어둠 (선택 사항)
volumes:
- mongo_data:/data/db # 데이터 영속성을 위해 명명된 볼륨 사용
networks:
- app_network # 모든 서비스가 동일한 네트워크 사용
networks:
app_network:
driver: bridge # 기본 브릿지 네트워크
volumes:
mongo_data: # mongo_db 서비스에서 사용할 볼륨 정의
4.3. 실행하기
-
위 파일들을 해당 경로에 맞게 생성합니다.
-
my-mern-app
디렉터리에서 터미널을 엽니다. -
다음 명령어를 실행합니다:
docker-compose up -d
-
잠시 기다린 후, 모든 컨테이너가 시작되었는지 확인합니다:
docker-compose ps
모든 서비스(
nginx
,node_app
,mongo_db
)의 상태가Up
으로 표시되면 성공입니다. -
웹 브라우저에서
http://localhost/
에 접속하면 Node.js 앱의 응답인 “Hello from Node.js App! 🎉” 메시지를 볼 수 있습니다. -
http://localhost/items
에 접속하면 빈 배열[]
이 보일 겁니다. (아직 데이터가 없으므로) -
Postman 또는
curl
로 데이터를 추가해 보세요:curl -X POST -H "Content-Type: application/json" -d '{"name": "Docker Compose", "description": "The best tool ever!"}' http://localhost/items
-
다시
http://localhost/items
에 접속하면 추가된 데이터를 확인할 수 있습니다!
4.4. 정리하기
작업이 끝났다면, 다음 명령어로 모든 컨테이너와 네트워크를 깔끔하게 제거할 수 있습니다:
docker-compose down -v # -v 옵션은 mongo_data 볼륨도 함께 삭제합니다.
5. Docker Compose 배포 전략 및 고급 팁 🚀
개발 환경에서 Docker Compose는 매우 강력하지만, 프로덕션 환경에서는 몇 가지 고려 사항이 있습니다.
5.1. .env
파일로 환경 변수 관리 🔐
민감한 정보(API 키, 데이터베이스 비밀번호 등)는 docker-compose.yml
파일에 직접 넣는 대신 .env
파일을 사용하여 관리하는 것이 좋습니다. .gitignore
에 .env
파일을 추가하여 Git에 커밋되지 않도록 합니다.
# .env
DB_PASSWORD=mySuperSecretPassword
API_KEY=your_api_key_here
# docker-compose.yml
services:
my_service:
environment:
- SECRET_KEY=${API_KEY} # .env 파일에서 가져옴
5.2. 여러 개의 Compose 파일 사용 📂
개발 환경과 프로덕션 환경의 설정이 다를 경우, 여러 개의 Compose 파일을 조합하여 사용할 수 있습니다.
docker-compose.yml
: 공통 설정docker-compose.dev.yml
: 개발 환경 전용 오버라이드 (볼륨 마운트, 디버그 포트 등)docker-compose.prod.yml
: 프로덕션 환경 전용 오버라이드 (리소스 제한, 리스타트 정책, 빌드 컨텍스트 등)
사용 예시:
- 개발 환경:
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d
- 프로덕션 환경:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
5.3. 스케일링 (Scaling) 📈
Docker Compose는 up
명령어를 통해 서비스의 인스턴스를 쉽게 확장할 수 있습니다 (Docker Swarm 또는 Kubernetes 없이 단순한 복제).
docker-compose up --scale webapp=3 -d
위 명령은 webapp
서비스의 컨테이너를 3개로 복제하여 실행합니다. 이는 로드 밸런싱이 필요한 경우 유용합니다. (단, 실제 로드 밸런싱을 위해서는 Nginx와 같은 리버스 프록시가 필요합니다.)
5.4. 프로덕션 환경 고려 사항 🏭
- 리버스 프록시: Nginx, Traefik 등을 사용하여 외부 요청을 적절한 서비스로 라우팅하고, SSL/TLS를 설정합니다.
- 지속적인 통합/배포 (CI/CD): Jenkins, GitLab CI, GitHub Actions 등과 연동하여 코드 푸시 시 자동으로 빌드, 테스트, 배포되도록 자동화합니다.
- 로깅 및 모니터링: ELK 스택 (Elasticsearch, Logstash, Kibana)이나 Prometheus, Grafana 등을 사용하여 애플리케이션 로그와 성능 지표를 수집하고 시각화합니다.
- 보안: 이미지 최소화, 취약점 스캔, 비밀번호 관리 등 보안 모범 사례를 따릅니다.
- 리소스 제한:
resources
섹션을 사용하여 각 서비스에 할당할 CPU 및 메모리 제한을 설정할 수 있습니다.services: my_service: deploy: resources: limits: cpus: '0.5' # 0.5 코어 memory: 512M # 512MB reservations: cpus: '0.25' memory: 256M
6. 문제 해결 팁 (Troubleshooting) 🐞
Docker Compose 사용 중 흔히 발생하는 문제와 해결 팁입니다.
- 컨테이너가 실행되지 않음 (Exited):
docker-compose logs [서비스_이름]
명령어로 로그를 확인합니다. 오류 메시지가 문제의 단서를 제공할 것입니다.- 컨테이너 내부에서 수동으로 명령어를 실행해 보세요:
docker-compose exec [서비스_이름] bash
(또는sh
) ->ls -l
,cat
,npm start
등.
- 포트 충돌 (Port Conflict):
port already in use
오류가 발생하면, 해당 포트가 이미 호스트 머신에서 사용 중인 것입니다.netstat -tulnp | grep [포트_번호]
(Linux/macOS) 또는netstat -ano | findstr :[포트_번호]
(Windows PowerShell)로 어떤 프로세스가 사용 중인지 확인하고 종료하거나, 다른 포트 번호로 변경하세요.
- 볼륨 마운트 문제 (Volume Permissions):
permission denied
오류는 컨테이너 내부의 프로세스가 마운트된 볼륨에 쓰기/읽기 권한이 없을 때 발생합니다. 호스트의 디렉터리 권한을 변경하거나, Dockerfile에서 컨테이너 내부 사용자의 권한을 조절해야 할 수 있습니다.
- 의존성 서비스가 준비되지 않음 (
depends_on
의 한계):depends_on
은 시작 순서만 보장하며, 서비스의 “준비” 상태를 보장하지 않습니다. 백엔드 앱이 데이터베이스에 연결하려고 할 때 데이터베이스가 아직 완전히 부팅되지 않아 연결 실패할 수 있습니다.- 해결책:
- 애플리케이션 코드에 재시도(Retry) 로직을 구현합니다.
healthcheck
를 사용하여 서비스의 준비 상태를 정확히 파악하고,depends_on
과 함께condition: service_healthy
를 사용합니다.services: webapp: depends_on: database: condition: service_healthy # database 서비스가 healthy 상태일 때 webapp 시작 database: healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s
- YAML 문법 오류:
docker-compose config
명령어를 사용하여 YAML 파일의 문법을 검사할 수 있습니다. 오류가 있다면 어떤 줄에서 발생했는지 알려줍니다.
7. 마무리하며 🎉
축하드립니다! 이제 여러분은 Docker Compose의 기본적인 개념부터 심화 사용법, 그리고 실제 애플리케이션 배포 시 고려해야 할 사항들까지 모두 이해하게 되셨습니다. Docker Compose는 로컬 개발 환경을 넘어 소규모 프로덕션 환경에서도 매우 유용하게 사용될 수 있는 강력한 도구입니다.
이 가이드가 여러분의 Docker 여정에 큰 도움이 되었기를 바랍니다. 궁금한 점이 있다면 언제든지 댓글로 남겨주세요! Happy Dockering! 🐳💖