G: 안녕하세요! 복잡한 마이크로서비스 아키텍처나 다중 컨테이너 애플리케이션을 개발하고 계신가요? 개별적으로 컨테이너를 하나씩 실행하고 관리하는 것이 번거롭고 실수투성이라면, 오늘 다룰 ‘Docker Compose’가 바로 여러분을 위한 구원투수입니다. 🦸♂️
이 글에서는 Docker Compose의 핵심 원리부터 실제 서비스 배포에 이르기까지, A부터 Z까지 모든 것을 상세하게 다룰 예정입니다. 최신 정보와 다양한 예시, 그리고 유용한 팁까지 아낌없이 제공해 드릴 테니, 끝까지 따라와 주세요!
💡 1. Docker Compose, 왜 필요할까요? (문제 인식 & 솔루션)
우리가 웹 애플리케이션을 개발한다고 가정해봅시다. 보통 백엔드 서버(Node.js, Python, Java 등), 데이터베이스(MongoDB, PostgreSQL, MySQL), 그리고 어쩌면 캐시 서버(Redis)나 프록시 서버(Nginx)까지, 여러 개의 컴포넌트가 유기적으로 연결되어 동작합니다.
각 컴포넌트마다 Docker 컨테이너로 만들었다면, 이제 이들을 어떻게 함께 실행하고 관리할까요?
docker run ...
명령어를 여러 번 반복? 🤔- 컨테이너 간의 네트워크 연결은 어떻게? 🕸️
- 데이터 지속성(Persistent Data)은 어떻게 보장? 💾
- 개발 환경, 테스트 환경, 배포 환경에서 동일하게 동작하도록 하려면? 🤯
이런 고민들을 한 번에 해결해 주는 도구가 바로 Docker Compose입니다. Docker Compose는 여러 개의 Docker 컨테이너를 하나로 묶어 정의하고 실행하며 관리하는 도구입니다. 마치 오케스트라의 지휘자처럼, 다양한 컨테이너들이 조화롭게 동작하도록 도와줍니다. 🎼
핵심 이점:
- 간결성: 복잡한
docker run
명령어를 하나의 YAML 파일(docker-compose.yml
)로 정의합니다. - 재현성: 동일한
docker-compose.yml
파일만 있다면, 어느 환경에서든 동일한 애플리케이션 스택을 재현할 수 있습니다. - 개발 생산성 향상: 한 번의 명령으로 전체 개발 환경을 구축하고 실행할 수 있습니다.
- 환경 일관성: 개발, 테스트, 운영 환경 간의 차이를 줄여줍니다.
⚙️ 2. Docker Compose의 핵심 원리 이해하기
Docker Compose의 마법은 주로 docker-compose.yml
이라는 YAML 파일에서 시작됩니다. 이 파일은 애플리케이션을 구성하는 서비스(컨테이너), 네트워크, 볼륨 등을 정의합니다.
2.1. docker-compose.yml
파일 구조 살펴보기
docker-compose.yml
파일은 다음과 같은 주요 섹션으로 구성됩니다.
version: '3.8' # Docker Compose 파일 형식 버전 (최신 버전 사용 권장)
services: # 애플리케이션을 구성하는 각 서비스(컨테이너) 정의
web: # 서비스 이름 (컨테이너 이름, 네트워크 호스트명으로 사용)
image: nginx:latest # 사용할 Docker 이미지 (Docker Hub에서 가져옴)
ports:
- "80:80" # 호스트 포트:컨테이너 포트 매핑
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf # 호스트 경로:컨테이너 경로 (바인드 마운트)
depends_on: # 이 서비스가 시작되기 전에 먼저 시작되어야 하는 서비스
- api
environment: # 컨테이너 내부에서 사용할 환경 변수
NODE_ENV: production
networks: # 이 서비스가 속할 네트워크
- my_app_network
api:
build: # Dockerfile을 사용하여 이미지 빌드
context: ./backend # Dockerfile이 있는 경로
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: mongodb://mongodb:27017/mydatabase
depends_on:
- mongodb
networks:
- my_app_network
mongodb:
image: mongo:latest
volumes:
- mongo_data:/data/db # 이름 있는 볼륨 (데이터 지속성)
networks:
- my_app_network
networks: # 사용자 정의 네트워크
my_app_network:
driver: bridge # 기본 드라이버 (브릿지)
volumes: # 사용자 정의 볼륨
mongo_data: # 'mongo_data'라는 이름의 볼륨
핵심 개념:
- Service (서비스): Docker Compose 애플리케이션의 핵심 구성 요소입니다. 각 서비스는 하나의 컨테이너를 나타내며,
image
또는build
를 통해 컨테이너 이미지를 지정합니다.ports
,volumes
,environment
등 다양한 설정을 통해 컨테이너의 동작 방식을 정의할 수 있습니다. - Network (네트워크): 서비스 간의 통신을 가능하게 합니다. Docker Compose는 기본적으로 프로젝트 이름을 따서 브릿지 네트워크를 생성하고 모든 서비스를 연결합니다. 하지만 위 예시처럼 사용자 정의 네트워크를 생성하여 서비스 간의 격리나 특정 연결 설정을 할 수 있습니다. 🌐
- Volume (볼륨): 컨테이너 내부의 데이터를 호스트 머신에 저장하여 컨테이너가 삭제되더라도 데이터가 유지되도록 합니다. 바인드 마운트(
./data:/app/data
)와 이름 있는 볼륨(my_volume:/data
) 두 가지 주요 방식이 있습니다. 이름 있는 볼륨은 데이터 지속성에 더 적합합니다. 💾
2.2. Docker Compose의 주요 명령어
docker compose
(또는 구 버전의 docker-compose
) 명령어를 통해 docker-compose.yml
파일을 제어합니다.
docker compose up
:docker-compose.yml
파일에 정의된 모든 서비스를 빌드(필요시)하고 실행합니다.-d
(detached mode): 백그라운드에서 실행합니다. 터미널을 차지하지 않습니다.--build
: 이미지 캐시를 사용하지 않고 항상 이미지를 새로 빌드합니다.
docker compose down
:docker compose up
으로 생성된 모든 컨테이너, 네트워크, 볼륨(옵션)을 중지하고 제거합니다.-v
(volumes): 컨테이너와 함께 볼륨도 제거합니다. (주의! 데이터가 사라집니다.)
docker compose build [서비스명]
: 특정 서비스 또는 모든 서비스의 이미지를 빌드합니다.docker compose ps
: 현재 실행 중인 서비스들의 상태를 보여줍니다.docker compose logs [서비스명]
: 특정 서비스 또는 모든 서비스의 로그를 실시간으로 스트리밍합니다.-f
(follow): 실시간으로 새로운 로그를 계속 보여줍니다.
docker compose exec [서비스명] [명령어]
: 실행 중인 특정 서비스 컨테이너 내부에서 명령어를 실행합니다. (예:docker compose exec web bash
)docker compose start [서비스명]
: 중지된 서비스를 시작합니다.docker compose stop [서비스명]
: 실행 중인 서비스를 중지합니다.docker compose restart [서비스명]
: 실행 중인 서비스를 재시작합니다.
💻 3. Docker Compose 설치하기
대부분의 경우 Docker Desktop을 설치하면 Docker Compose가 함께 설치됩니다. 🥳
- Windows / macOS: Docker Desktop을 설치하면
docker compose
(또는docker-compose
) 명령어를 바로 사용할 수 있습니다. - Linux:
- Docker Desktop for Linux를 설치하거나,
- 독립적으로 설치할 수 있습니다. 최신 버전의 Docker는
docker compose
플러그인을 기본으로 포함하고 있으므로, Docker를 최신 버전으로 업데이트하는 것이 좋습니다. - 구 버전의
docker-compose
는pip
를 통해 설치하기도 했습니다:sudo pip install docker-compose
설치 확인:
docker compose version
# 또는 구 버전의 경우:
# docker-compose --version
🌐 4. 실전 예제: Nginx + Node.js + MongoDB 웹 애플리케이션 배포
이제 실제로 Nginx (리버스 프록시) + Node.js (백엔드 API) + MongoDB (데이터베이스)로 구성된 웹 애플리케이션을 Docker Compose로 배포해 보겠습니다.
4.1. 프로젝트 구조
my-web-app/
├── docker-compose.yml
├── backend/
│ ├── Dockerfile
│ └── app.js
├── nginx/
│ ├── Dockerfile
│ └── nginx.conf
└── .env # 환경 변수 파일 (선택 사항)
4.2. 코드 및 설정 파일 작성
backend/Dockerfile
# Node.js 백엔드 Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
backend/app.js
// 간단한 Node.js 백엔드 (Express)
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const port = 3000;
// 환경 변수에서 MongoDB 연결 URL 가져오기
const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017/myapp';
mongoose.connect(mongoUrl)
.then(() => console.log('MongoDB connected successfully! ✨'))
.catch(err => console.error('MongoDB connection error:', err));
app.get('/', (req, res) => {
res.send('Hello from Node.js Backend! 🌍');
});
app.get('/api/status', (req, res) => {
if (mongoose.connection.readyState === 1) {
res.json({ status: 'ok', database: 'connected' });
} else {
res.json({ status: 'not ok', database: 'disconnected' });
}
});
app.listen(port, () => {
console.log(`Backend server listening on http://localhost:${port}`);
});
(참고: package.json
에 express
와 mongoose
를 추가하고 npm install
을 실행해야 합니다.)
nginx/Dockerfile
# Nginx Dockerfile
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx/nginx.conf
# Nginx 설정 파일 (리버스 프록시)
events {
worker_connections 1024;
}
http {
server {
listen 80;
location / {
proxy_pass http://backend:3000; # 'backend'는 docker-compose.yml에 정의된 서비스 이름
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 /api {
proxy_pass http://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;
}
}
}
.env
(선택 사항 – 환경 변수 관리)
NODE_ENV=development
MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=secret
docker-compose.yml
version: '3.8'
services:
nginx:
build: ./nginx
ports:
- "80:80" # 호스트의 80번 포트를 Nginx 컨테이너의 80번 포트에 연결
depends_on:
- backend # Nginx는 backend 서비스가 시작된 후에 시작
networks:
- myapp_network
backend:
build: ./backend
ports:
- "3000:3000" # 개발/디버깅용으로 포트 노출 (선택 사항)
environment:
# .env 파일에서 환경 변수를 읽어옴
MONGO_URL: mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017/mydatabase?authSource=admin
NODE_ENV: ${NODE_ENV}
depends_on:
mongodb:
condition: service_healthy # MongoDB가 'healthy' 상태가 될 때까지 기다림
networks:
- myapp_network
healthcheck: # 백엔드 서비스 헬스체크 정의
test: ["CMD", "curl", "-f", "http://localhost:3000/api/status"]
interval: 10s
timeout: 5s
retries: 5
mongodb:
image: mongo:latest
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
volumes:
- mongo_data:/data/db # 데이터 지속성을 위한 이름 있는 볼륨
networks:
- myapp_network
healthcheck: # MongoDB 헬스체크 정의
test: echo 'db.runCommand("ping").ok' | mongo localhost:27017/mydatabase --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --quiet
interval: 10s
timeout: 5s
retries: 5
networks:
myapp_network:
driver: bridge
volumes:
mongo_data:
4.3. 애플리케이션 실행하기 🚀
프로젝트 루트(my-web-app/
)에서 다음 명령어를 실행합니다.
docker compose up --build -d
--build
:backend
와nginx
의Dockerfile
이 변경되었을 경우, 이미지를 새로 빌드합니다. (첫 실행 시 필수)-d
: 컨테이너들을 백그라운드에서 실행합니다.
잠시 기다린 후, 모든 서비스가 정상적으로 시작되었는지 확인합니다.
docker compose ps
출력:
NAME COMMAND SERVICE STATUS PORTS
my-web-app-backend-1 "docker-entrypoint.s…" backend running (healthy) 0.0.0.0:3000->3000/tcp
my-web-app-mongodb-1 "docker-entrypoint.s…" mongodb running (healthy) 27017/tcp
my-web-app-nginx-1 "/docker-entrypoint.…" nginx running 0.0.0.0:80->80/tcp
running (healthy)
상태를 확인하면 성공입니다!
이제 웹 브라우저를 열어 http://localhost/
에 접속해 보세요. Nginx를 통해 Node.js 백엔드의 “Hello from Node.js Backend! 🌍” 메시지를 볼 수 있을 겁니다.
http://localhost/api/status
에 접속하면 MongoDB 연결 상태도 확인할 수 있습니다.
4.4. 애플리케이션 중지 및 제거 🛑
docker compose down
이 명령은 모든 컨테이너와 네트워크를 중지하고 제거합니다. 만약 MongoDB의 데이터 볼륨까지 완전히 삭제하려면:
docker compose down -v
경고: -v
옵션은 볼륨의 데이터까지 완전히 삭제하므로 주의해서 사용해야 합니다!
🌟 5. Docker Compose 고급 기능 및 활용 팁
5.1. 환경 변수 관리: .env
파일과 env_file
민감한 정보(API 키, 데이터베이스 비밀번호)나 환경에 따라 달라지는 값(개발/운영 모드)은 docker-compose.yml
파일에 직접 넣는 것보다 환경 변수로 관리하는 것이 좋습니다.
.env
파일:docker-compose.yml
과 같은 디렉토리에.env
파일을 생성하고KEY=VALUE
형식으로 변수를 정의합니다. Compose는 이 파일을 자동으로 로드하고docker-compose.yml
내에서${KEY}
형태로 참조할 수 있게 합니다. (위 예제에서 사용됨)env_file
: 특정 서비스에만 적용되는 환경 변수 파일이 필요할 때 사용합니다.services: my_service: env_file: - ./configs/dev.env - ./configs/common.env
5.2. 서비스 종속성 관리: depends_on
과 healthcheck
depends_on
: 서비스 시작 순서를 제어합니다. (예:backend
는mongodb
가 시작된 후에 시작). 하지만depends_on
은 단순히 컨테이너가 시작되는 것을 보장할 뿐, 서비스 내부 프로세스가 완전히 준비되는 것을 보장하지 않습니다. ⚠️healthcheck
: 서비스가 실제로 동작 가능(ready)한 상태인지 주기적으로 확인합니다. 위 예제에서backend
와mongodb
에healthcheck
를 정의하고backend
의depends_on
에condition: service_healthy
를 추가하여,mongodb
가 완전히 동작 준비가 될 때까지backend
가 기다리도록 했습니다. 이는 프로덕션 환경에서 매우 중요한 기능입니다! 👍
5.3. 다양한 환경 설정: profiles
개발, 테스트, 운영 등 여러 환경에 따라 다른 설정을 사용하고 싶을 때 profiles
를 사용합니다.
version: '3.8'
services:
web:
image: nginx
ports:
- "80:80"
profiles: ["production"] # 이 서비스는 'production' 프로파일에서만 실행
dev_tools:
image: my_dev_tool
ports:
- "8080:8080"
profiles: ["development"] # 이 서비스는 'development' 프로파일에서만 실행
실행 시:
- 개발 환경:
docker compose --profile development up
- 운영 환경:
docker compose --profile production up
- 모두 실행:
docker compose up
(프로파일이 없는 서비스는 항상 실행)
5.4. 재사용 가능한 설정: extends
반복되는 서비스 설정을 줄이고 싶을 때 extends
를 사용합니다.
# common-services.yml
version: '3.8'
services:
base_service:
image: busybox
environment:
APP_ENV: development
volumes:
- app_data:/app
# docker-compose.yml
version: '3.8'
services:
my_app:
extends:
file: common-services.yml
service: base_service
command: ["sh", "-c", "echo 'Hello from my_app'"]
📊 6. Docker Compose와 서비스 배포 (프로덕션 환경)
Docker Compose는 로컬 개발 환경 구축 및 CI/CD 파이프라인에서의 테스트 환경 구성에 가장 강력합니다. 하지만, 대규모 프로덕션 환경에서는 몇 가지 한계가 있습니다.
6.1. Docker Compose의 프로덕션 환경 사용 시 고려사항:
- 확장성(Scaling):
docker compose up --scale web=3
명령으로 서비스 컨테이너를 여러 개 띄울 수는 있지만, 이는 기본적인 수준의 스케일링입니다. 부하 분산(Load Balancing)이나 자동 스케일링 기능이 내장되어 있지 않습니다. - 자동 복구(Self-healing): 컨테이너에 문제가 생기거나 호스트 머신에 장애가 발생했을 때, Docker Compose는 자동으로 컨테이너를 다른 노드로 이동시키거나 재시작하는 기능이 없습니다.
- 고가용성(High Availability): 단일 호스트 머신에 의존하므로, 해당 머신에 문제가 생기면 전체 애플리케이션이 다운될 수 있습니다.
- 모니터링 & 로깅: 별도의 모니터링 및 로깅 솔루션과 통합해야 합니다.
6.2. 대안: Docker Swarm 및 Kubernetes (쿠버네티스)
대규모, 고가용성이 필요한 프로덕션 환경에서는 Docker Compose보다 더 강력한 오케스트레이션 도구들을 사용합니다.
- Docker Swarm: Docker 자체에 내장된 컨테이너 오케스트레이션 도구입니다. Docker Compose와 유사한 문법을 사용하여
docker-compose.yml
파일을 Swarm 모드에서 배포할 수 있습니다 (docker stack deploy
). 비교적 가볍고 배우기 쉽습니다. - Kubernetes (쿠버네티스): 현재 컨테이너 오케스트레이션의 사실상 표준입니다. 훨씬 복잡하지만, 확장성, 자동 복구, 로드 밸런싱, 서비스 디스커버리 등 모든 기능을 제공하여 대규모 분산 시스템 구축에 최적화되어 있습니다.
결론적으로, Docker Compose는 프로덕션 환경을 로컬에서 시뮬레이션하고, 소규모 애플리케이션을 단일 서버에 배포하는 데 매우 유용합니다. 하지만 진정한 분산 시스템과 고가용성이 필요하다면 Swarm이나 Kubernetes로 전환하는 것을 고려해야 합니다.
🧐 7. Docker Compose 사용 시 Best Practices & Troubleshooting
7.1. Best Practices (모범 사례)
version 3
사용:docker-compose.yml
파일의version
을 항상 최신 권장 버전(현재 3.8 이상)으로 유지하세요.- 명확한 서비스 이름: 서비스 이름은 컨테이너 이름이자 네트워크 호스트명으로 사용되므로, 간결하고 의미 있는 이름을 사용하세요.
- 환경 변수 활용: 민감한 정보나 환경별 설정은
.env
파일이나env_file
로 관리하여docker-compose.yml
파일을 깨끗하게 유지하세요. - 이름 있는 볼륨 사용: 컨테이너가 재생성되어도 데이터가 손실되지 않도록 데이터베이스 등 중요한 데이터는 이름 있는 볼륨으로 관리하세요. (예:
mongo_data:/data/db
) - 네트워크 명시적 정의: 명시적으로 네트워크를 정의하고 서비스를 연결하여 서비스 간의 의존성을 명확히 하고, 불필요한 통신을 제한하세요.
healthcheck
적극 활용:depends_on
만으로는 부족합니다.healthcheck
를 통해 서비스가 실제로ready
상태가 되었는지 확인하고, 이를 다른 서비스의 시작 조건으로 활용하세요.- 불필요한 포트 노출 금지: 외부에 노출되어야 하는 포트(
ports
)만 정의하세요. 내부 통신은 네트워크를 통해 서비스 이름으로 이루어집니다. - 주석 활용:
docker-compose.yml
파일은 복잡해질 수 있으므로, 각 설정에 대한 간략한 주석을 추가하면 유지보수에 도움이 됩니다.
7.2. Troubleshooting (문제 해결)
- 컨테이너 시작 오류:
- 로그 확인:
docker compose logs [서비스명]
명령으로 해당 서비스의 로그를 확인하세요. 대부분의 오류는 로그에 기록됩니다. - 포트 충돌: 호스트의 포트가 이미 다른 프로세스에 의해 사용 중일 수 있습니다.
lsof -i :<포트번호>
(macOS/Linux) 또는netstat -ano | findstr :<포트번호>
(Windows CMD)로 확인하세요. - 환경 변수 누락/오류: 필요한 환경 변수가 제대로 전달되었는지 확인하세요.
Dockerfile
또는 빌드 문제:docker compose build --no-cache
로 다시 빌드해 보세요.
- 로그 확인:
- 서비스 간 통신 불가:
- 네트워크 확인: 모든 서비스가 동일한 네트워크에 연결되어 있는지 확인하세요.
- 호스트명 확인: 서비스 간 통신 시에는
localhost
가 아닌docker-compose.yml
에 정의된 서비스 이름을 사용해야 합니다 (예:http://backend:3000
). - 방화벽: 호스트 머신의 방화벽이 컨테이너 간 또는 호스트-컨테이너 간 통신을 차단하고 있을 수 있습니다.
- 데이터 지속성 문제:
- 볼륨 확인:
volumes
설정이 올바른지, 이름 있는 볼륨이 제대로 생성되었는지docker volume ls
로 확인하세요. - 권한 문제: 바인드 마운트 시 호스트 파일 시스템의 권한 문제가 발생할 수 있습니다. 컨테이너 내부의 프로세스가 해당 파일/폴더에 쓸 수 있는 권한이 있는지 확인하세요.
- 볼륨 확인:
- YAML 문법 오류: YAML은 들여쓰기가 매우 중요합니다. 들여쓰기 오류나 잘못된 문법으로 인해 파싱 오류가 발생할 수 있습니다. 온라인 YAML 유효성 검사 도구를 사용해 보세요.
🥳 8. 마치며: Docker Compose, 당신의 개발 라이프를 바꿔줄 마법!
지금까지 Docker Compose의 핵심 원리부터 실제 웹 애플리케이션 배포 예제, 그리고 고급 기능과 문제 해결 팁까지 A부터 Z까지 상세하게 살펴보았습니다. Docker Compose는 개발자가 다중 컨테이너 애플리케이션을 훨씬 쉽고 효율적으로 관리할 수 있도록 돕는 강력한 도구입니다.
이제 더 이상 복잡한 docker run
명령어를 일일이 입력하거나, 개발 환경과 배포 환경의 불일치로 스트레스받을 필요가 없습니다. docker-compose.yml
파일 하나로 모든 것을 정의하고 docker compose up
한 줄로 전체 스택을 실행하는 마법을 경험해 보세요. ✨
오늘 배운 내용을 바탕으로 자신만의 프로젝트에 Docker Compose를 적극적으로 활용해 보시길 권장합니다. 처음에는 조금 낯설 수 있지만, 익숙해지면 여러분의 개발 워크플로우에 혁신을 가져올 것입니다. Happy Dockering! 🐳💖