G: 안녕하세요, 개발자 여러분! 🚀 복잡한 애플리케이션을 개발하고 배포하다 보면 여러 개의 서비스(데이터베이스, 백엔드 서버, 프론트엔드 웹 서버 등)가 서로 유기적으로 연결되어 작동해야 하는 경우가 많습니다. 이 각각의 서비스들을 일일이 docker run
명령어로 실행하고 관리하는 것은 여간 번거로운 일이 아닐 수 없죠. 마치 오케스트라의 악기 하나하나를 개별적으로 지휘하는 것과 같습니다. 🤯
이러한 고통을 한 방에 해결해 줄 마법 같은 도구가 바로 Docker Compose입니다! ✨ Docker Compose는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다. 단일 docker-compose.yml
파일 하나로 애플리케이션의 모든 서비스를 한 번에 구성하고 실행할 수 있게 해줍니다.
이 글에서는 Docker Compose의 핵심 개념부터 실제 사용 예제, 그리고 고급 활용 팁까지, 여러분이 Docker Compose를 마스터하는 데 필요한 모든 것을 알려드리겠습니다. 자, 그럼 함께 시작해 볼까요? 💡
1. Docker Compose, 왜 필요할까요? 🤔
하나의 애플리케이션이 여러 서비스로 구성될 때, 각 서비스를 수동으로 관리하는 것은 다음과 같은 문제점을 야기합니다:
- 복잡한 명령어: 각 컨테이너마다 포트 매핑, 볼륨 마운트, 환경 변수 설정 등 길고 복잡한
docker run
명령어를 일일이 입력해야 합니다. 😵 - 재현성 부족: 팀원마다, 혹은 개발 환경마다 컨테이너 설정이 달라져 “내 컴퓨터에서는 되는데…” 문제가 발생할 수 있습니다. 🤷♀️
- 종속성 관리: 데이터베이스가 먼저 시작해야 백엔드 서버가 제대로 동작하는 것처럼, 서비스 간의 시작 순서나 의존성을 수동으로 관리하기 어렵습니다. 🚦
- 리소스 관리: 네트워크 설정, 볼륨 생성 등을 개별적으로 해줘야 합니다.
Docker Compose는 이 모든 문제를 해결해 줍니다! ✅
- 단일 파일 정의:
docker-compose.yml
파일 하나에 전체 애플리케이션의 서비스, 네트워크, 볼륨 등을 정의합니다. 마치 애플리케이션의 청사진을 그리는 것과 같죠! 🗺️ - 간편한 실행:
docker-compose up
명령 한 번으로 정의된 모든 서비스를 한 번에 빌드하고 실행할 수 있습니다. 지휘자가 박자를 맞추듯 모든 악기가 동시에 연주되는 느낌! 🎶 - 재현성 보장:
docker-compose.yml
파일을 공유하면 모든 팀원과 환경에서 동일한 애플리케이션 환경을 구축할 수 있습니다. 🤝 - 간편한 중지 및 제거:
docker-compose down
명령으로 모든 서비스를 깨끗하게 중지하고 제거할 수 있습니다. 🧹
2. Docker Compose 핵심 개념 이해하기 🧠
Docker Compose를 효과적으로 사용하기 위해 알아야 할 몇 가지 핵심 개념들이 있습니다.
2.1. docker-compose.yml
파일 📝
모든 것의 시작점입니다! YAML(YAML Ain’t Markup Language) 형식으로 작성되며, 애플리케이션의 모든 서비스와 그 설정을 정의합니다. 들여쓰기(Indentation)가 매우 중요하니 주의하세요!
기본 구조:
version: '3.8' # Compose 파일 형식 버전 (최신 버전을 사용하는 것이 좋음)
services: # 애플리케이션을 구성하는 서비스들 정의
서비스1:
# 서비스1의 설정 (이미지, 포트, 볼륨 등)
서비스2:
# 서비스2의 설정
networks: # 서비스 간 통신을 위한 네트워크 정의 (선택 사항)
네트워크이름:
# 네트워크 설정
volumes: # 데이터 영속성을 위한 볼륨 정의 (선택 사항)
볼륨이름:
# 볼륨 설정
2.2. Services (서비스) 📦
docker-compose.yml
파일에서 가장 중요한 부분입니다. 각 서비스는 하나의 컨테이너를 나타내며, 해당 컨테이너의 이미지, 빌드 설정, 포트 매핑, 볼륨 마운트, 환경 변수 등을 정의합니다.
image
: 사용할 Docker 이미지 이름을 지정합니다 (예:nginx:latest
,node:16
).build
: Dockerfile을 사용하여 직접 이미지를 빌드해야 할 경우 사용합니다. 주로context
(Dockerfile 경로)와dockerfile
(Dockerfile 이름)을 지정합니다.ports
: 호스트와 컨테이너 간의 포트 매핑을 정의합니다 (예:"80:80"
은 호스트의 80번 포트를 컨테이너의 80번 포트에 연결).volumes
: 컨테이너와 호스트 간의 파일 공유 또는 데이터 영속성을 위해 사용합니다../app:/app
: 호스트의 현재 디렉터리(./app
)를 컨테이너 내부의/app
에 마운트합니다 (바인드 마운트).db_data:/var/lib/mysql
:db_data
라는 이름의 볼륨을 컨테이너의/var/lib/mysql
에 마운트합니다 (명명된 볼륨).
environment
: 컨테이너 내부에서 사용할 환경 변수를 설정합니다.depends_on
: 서비스 간의 의존성을 정의합니다. 예를 들어, 백엔드 서비스가 데이터베이스 서비스에 의존하는 경우depends_on: - db
와 같이 설정합니다. 주의:depends_on
은 컨테이너의 시작 순서만 보장하며, 서비스가 완전히 준비되었음을 보장하지는 않습니다. (예: DB가 실행은 되었지만 연결 준비는 안 되었을 수도 있음).networks
: 서비스가 연결될 네트워크를 지정합니다. 지정하지 않으면 기본default
네트워크에 연결됩니다.
2.3. Networks (네트워크) 🌐
서비스 간의 통신을 가능하게 합니다. Docker Compose는 별도로 지정하지 않으면 모든 서비스가 연결되는 기본 bridge
네트워크를 생성합니다. 명시적으로 커스텀 네트워크를 정의하여 더 깔끔하고 안전한 통신 환경을 구축할 수 있습니다. 각 서비스는 네트워크 이름으로 서로를 찾을 수 있습니다.
2.4. Volumes (볼륨) 💾
컨테이너의 데이터를 영속적으로 저장하는 데 사용됩니다. 컨테이너가 삭제되어도 데이터는 볼륨에 남아있습니다.
- 바인드 마운트 (Bind Mounts): 호스트 파일 시스템의 특정 경로를 컨테이너에 직접 마운트합니다. 개발 중 소스 코드 변경 사항을 즉시 반영할 때 유용합니다.
- 명명된 볼륨 (Named Volumes): Docker가 관리하는 볼륨으로, 주로 데이터베이스와 같이 중요한 데이터를 영속적으로 저장할 때 사용합니다. 컨테이너 내부 경로만 지정하면 Docker가 알아서 볼륨을 생성하고 관리합니다.
3. 간단한 웹 애플리케이션 구축 예제 🏗️
이제 실제 예제를 통해 Docker Compose의 강력함을 경험해 봅시다. 여기서는 Node.js 기반의 간단한 백엔드 API 서버, Nginx 웹 서버 (리버스 프록시 역할), 그리고 MongoDB 데이터베이스로 구성된 애플리케이션을 Docker Compose로 구축해 보겠습니다.
프로젝트 구조:
my-app/
├── backend/
│ ├── Dockerfile
│ └── app.js
├── nginx/
│ └── nginx.conf
└── docker-compose.yml
1. backend/app.js
(Node.js 백엔드 코드):
// backend/app.js
const express = require('express');
const mongoose = require('mongoose'); // MongoDB 연결을 위해
const app = express();
const port = 3000;
// MongoDB 연결 (여기서 'mongodb'는 docker-compose.yml에 정의된 서비스 이름입니다!)
mongoose.connect('mongodb://mongodb:27017/mydatabase', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB Connected! 🥳'))
.catch(err => console.error('MongoDB connection error:', err));
app.get('/', (req, res) => {
res.send('Hello from Backend! 💖');
});
app.get('/api/status', (req, res) => {
res.json({ status: 'Backend is running!', database: mongoose.connection.readyState === 1 ? 'Connected' : 'Disconnected' });
});
app.listen(port, () => {
console.log(`Backend server listening at http://localhost:${port}`);
});
npm install express mongoose
로 필요한 패키지를 설치해야 합니다.
2. backend/Dockerfile
(백엔드 이미지 빌드용):
# backend/Dockerfile
FROM node:16-alpine # 가벼운 Node.js 이미지 사용
WORKDIR /app # 컨테이너 작업 디렉터리 설정
COPY package*.json ./ # package.json, package-lock.json 복사
RUN npm install # 의존성 설치
COPY . . # 현재 디렉터리의 모든 파일 복사
EXPOSE 3000 # 3000번 포트 노출
CMD ["node", "app.js"] # 애플리케이션 실행 명령
3. nginx/nginx.conf
(Nginx 설정 파일):
# nginx/nginx.conf
events {}
http {
server {
listen 80; # 80번 포트에서 수신
location / {
# 요청을 backend 서비스 (docker-compose.yml에 정의된 이름)의 3000번 포트로 전달
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;
}
location /api {
# /api 경로의 요청도 backend 서비스로 전달
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;
}
}
}
4. docker-compose.yml
(핵심 파일!):
# docker-compose.yml
version: '3.8' # Compose 파일 형식 버전 지정 (최신 버전 사용 권장)
services:
nginx: # Nginx 웹 서버 서비스 정의
image: nginx:latest # 최신 Nginx 이미지 사용
ports:
- "80:80" # 호스트의 80번 포트를 컨테이너의 80번 포트에 연결
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # 호스트의 nginx.conf를 컨테이너에 읽기 전용으로 마운트
depends_on: # backend 서비스가 먼저 시작되어야 함 (시작 순서 보장)
- backend
networks:
- my-app-network # custom 네트워크에 연결
backend: # Node.js 백엔드 서비스 정의
build: ./backend # backend 폴더의 Dockerfile을 사용하여 이미지 빌드
ports:
- "3000:3000" # 호스트의 3000번 포트를 컨테이너의 3000번 포트에 연결
environment: # 컨테이너 내부에서 사용할 환경 변수 설정
NODE_ENV: development
volumes:
- ./backend:/app # 개발 중 코드 변경 시 즉시 반영되도록 소스 코드 마운트
- /app/node_modules # node_modules는 호스트에서 마운트되지 않도록 예외 처리
depends_on: # mongodb 서비스가 먼저 시작되어야 함
- mongodb
networks:
- my-app-network # custom 네트워크에 연결
mongodb: # MongoDB 데이터베이스 서비스 정의
image: mongo:latest # 최신 MongoDB 이미지 사용
ports:
- "27017:27017" # 호스트의 27017번 포트를 컨테이너의 27017번 포트에 연결
volumes:
- mongodb_data:/data/db # mongodb_data라는 명명된 볼륨을 사용하여 데이터 영속성 유지
networks:
- my-app-network # custom 네트워크에 연결
networks:
my-app-network: # custom 네트워크 정의
driver: bridge # 기본 브릿지 드라이버 사용
volumes:
mongodb_data: # mongodb_data라는 명명된 볼륨 정의
5. 실행하기! 🚀
docker-compose.yml
파일이 있는 my-app
디렉터리에서 터미널을 열고 다음 명령어를 입력합니다:
docker-compose up -d
up
:docker-compose.yml
파일에 정의된 모든 서비스를 빌드하고 시작합니다.-d
: Detached 모드 (백그라운드)로 컨테이너를 실행합니다. 터미널을 계속 사용할 수 있습니다.
성공적으로 실행되면 다음과 비슷한 출력이 나타날 것입니다:
[+] Running 3/3
⠿ Network my-app_my-app-network Created 0.0s
⠿ Volume "my-app_mongodb_data" Created 0.0s
⠿ Container my-app-mongodb-1 Started 0.0s
⠿ Container my-app-backend-1 Started 0.0s
⠿ Container my-app-nginx-1 Started 0.0s
확인하기:
-
실행 중인 컨테이너 확인:
docker-compose ps
모든 서비스가
Up
상태여야 합니다.Name Command State Ports -------------------------------------------------------------------------------- my-app-backend-1 docker-entrypoint.sh node ap Up 0.0.0.0:3000->3000/tcp my-app-mongodb-1 docker-entrypoint.sh mongod Up 0.0.0.0:27017->27017/tcp my-app-nginx-1 /docker-entrypoint.sh ngin Up 0.0.0.0:80->80/tcp
-
애플리케이션 접속: 웹 브라우저에서
http://localhost/
에 접속해 보세요. “Hello from Backend! 💖” 메시지가 보이면 성공입니다! (Nginx를 통해 백엔드로 요청이 전달됨)http://localhost/api/status
에 접속하면 백엔드와 MongoDB의 연결 상태를 확인할 수 있습니다.
6. 중지 및 제거하기 🛑
애플리케이션 사용을 마쳤거나 다시 시작하고 싶을 때:
docker-compose down
이 명령은 모든 컨테이너를 중지하고 제거하며, 기본적으로 생성된 네트워크도 제거합니다. mongodb_data
와 같이 명시적으로 정의된 볼륨은 데이터를 보존하기 위해 기본적으로 제거되지 않습니다.
- 볼륨까지 완전히 제거하려면:
docker-compose down -v
-v
옵션은docker-compose.yml
파일에 정의된 모든 볼륨을 함께 제거합니다. 데이터가 영구히 삭제되므로 주의해서 사용하세요! ⚠️
4. Docker Compose 주요 명령어 살펴보기 ⌨️
이제 Docker Compose와 함께 자주 사용하게 될 핵심 명령어들을 알아봅시다.
-
docker-compose up [서비스명...]
:docker-compose.yml
파일에 정의된 서비스를 빌드하고 실행합니다.-d
옵션을 추가하면 백그라운드에서 실행됩니다.- 특정 서비스만 실행하고 싶다면 서비스 이름을 지정할 수 있습니다 (예:
docker-compose up -d backend
). - 새로운 변경 사항을 반영하려면 컨테이너를 재빌드해야 할 수 있습니다.
docker-compose up --build
를 사용하세요.
-
docker-compose down
:- 실행 중인 모든 서비스를 중지하고 관련 컨테이너, 네트워크를 제거합니다.
-v
옵션을 사용하면 명명된 볼륨도 함께 제거합니다. (데이터 주의!)
-
docker-compose build [서비스명...]
:- 서비스의 Dockerfile을 사용하여 이미지를 다시 빌드합니다.
up
명령이 자동으로 빌드를 수행하지만, 명시적으로 빌드만 하고 싶을 때 사용합니다. --no-cache
옵션을 사용하면 캐시를 사용하지 않고 새로 빌드합니다.
- 서비스의 Dockerfile을 사용하여 이미지를 다시 빌드합니다.
-
docker-compose ps
:- 현재 실행 중인 서비스들의 상태를 보여줍니다. 컨테이너 이름, 명령어, 상태, 포트 매핑 등을 확인할 수 있습니다.
-
docker-compose logs [서비스명...]
:- 컨테이너의 로그를 출력합니다.
-f
(follow) 옵션을 사용하면 실시간으로 로그를 계속 볼 수 있습니다. (예:docker-compose logs -f backend
)
- 컨테이너의 로그를 출력합니다.
-
docker-compose exec [서비스명] [명령어]
:- 실행 중인 특정 서비스 컨테이너 내부에서 명령어를 실행합니다.
- 디버깅에 매우 유용합니다. (예:
docker-compose exec backend bash
로 백엔드 컨테이너 내부 셸 접속)
-
docker-compose start [서비스명...]
:- 이전에 중지된 서비스를 다시 시작합니다.
-
docker-compose stop [서비스명...]
:- 실행 중인 서비스를 중지합니다 (컨테이너는 제거하지 않음).
-
docker-compose restart [서비스명...]
:- 실행 중인 서비스를 다시 시작합니다.
5. 실전 활용 팁 및 고급 기능 🛠️
Docker Compose는 이외에도 다양한 고급 기능을 제공하여 복잡한 환경을 효율적으로 관리할 수 있게 돕습니다.
5.1. 다중 Compose 파일 사용하기 (Multiple Compose Files) 📂
개발, 테스트, 배포 환경별로 다른 설정을 사용해야 할 때 유용합니다. 예를 들어, 개발 환경에서는 소스 코드를 마운트하고 디버그 모드를 활성화하지만, 배포 환경에서는 볼륨을 사용하고 최적화된 빌드를 사용하고 싶을 수 있습니다.
# 기본 설정 (common.yml)
# 개발 환경 오버라이드 (dev.yml)
# 배포 환경 오버라이드 (prod.yml)
# 개발 환경 실행:
docker-compose -f common.yml -f dev.yml up -d
# 배포 환경 실행:
docker-compose -f common.yml -f prod.yml up -d
-f
옵션을 여러 번 사용하여 여러 파일을 순서대로 로드하며, 나중에 로드된 파일의 설정이 이전 설정을 덮어씁니다.
5.2. 환경 변수 관리 (.env
파일) 🔑
데이터베이스 비밀번호, API 키 등 민감한 정보나 환경별로 달라지는 값들을 docker-compose.yml
파일에 직접 넣는 대신 .env
파일을 사용하여 관리할 수 있습니다.
my-app/.env
파일:
DB_USER=myuser
DB_PASSWORD=mypassword
APP_PORT=8080
docker-compose.yml
에서 사용:
services:
backend:
environment:
DB_USER: ${DB_USER} # .env 파일에서 DB_USER 값을 가져옴
DB_PASSWORD: ${DB_PASSWORD}
ports:
- "${APP_PORT}:3000" # .env 파일에서 APP_PORT 값을 가져옴
mongodb:
environment:
MONGO_INITDB_ROOT_USERNAME: ${DB_USER}
MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD}
docker-compose up
명령 실행 시, Docker Compose는 자동으로 .env
파일을 찾아서 변수를 로드합니다.
5.3. 서비스 스케일링 (Scaling Services) 📈
백엔드 서버처럼 부하를 분산해야 하는 경우, 여러 개의 인스턴스를 동시에 실행할 수 있습니다.
docker-compose up -d --scale backend=3
이 명령은 backend
서비스를 3개의 컨테이너로 스케일 아웃합니다. (부하 분산을 위해 Nginx와 같은 로드 밸런서 서비스가 필요할 수 있습니다.)
5.4. 헬스 체크 (Health Checks) 🩺
depends_on
은 시작 순서만 보장하므로, 서비스가 실제로 “준비 완료”되었는지 확인하기 위해 헬스 체크를 정의할 수 있습니다.
services:
backend:
build: ./backend
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/status"] # 특정 URL에 curl 테스트
interval: 30s # 30초마다 체크
timeout: 10s # 10초 내에 응답 없으면 실패
retries: 5 # 5번 실패하면 unhealthy
start_period: 20s # 처음 20초는 시작 기간으로 간주 (실패해도 unhealthy로 간주 안함)
이렇게 정의하면, docker-compose ps
에서 서비스 상태 옆에 (healthy)
또는 (unhealthy)
가 표시됩니다.
5.5. 재시작 정책 (Restart Policies) ♻️
컨테이너가 종료되었을 때 어떻게 동작할지 정의합니다.
services:
backend:
# ...
restart: always # 항상 재시작 (Docker 데몬 시작 시에도)
# restart: on-failure # 비정상 종료 시에만 재시작
# restart: unless-stopped # 수동으로 중지하지 않은 한 항상 재시작
# restart: no # 재시작 안 함 (기본값)
6. 트러블슈팅 팁 (Troubleshooting Tips) 🔍
Docker Compose 사용 중 문제가 발생했을 때 당황하지 마세요! 몇 가지 기본적인 트러블슈팅 방법이 있습니다.
- 로그 먼저 확인! (
docker-compose logs
): 가장 먼저 해야 할 일입니다. 어떤 서비스가 문제인지, 어떤 오류 메시지가 뜨는지 확인하세요.docker-compose logs -f
로 실시간 로그를 보면서 문제를 파악하는 것이 좋습니다. - 컨테이너 내부 탐험 (
docker-compose exec
): 문제가 되는 컨테이너 내부로 직접 들어가서 상황을 확인해 볼 수 있습니다. 예를 들어,docker-compose exec backend bash
명령으로 백엔드 컨테이너 셸에 접속하여 파일이 제대로 있는지, 필요한 라이브러리가 설치되었는지 등을 확인할 수 있습니다. - 포트 충돌 확인: 이미 호스트에서 사용 중인 포트를 컨테이너가 사용하려고 할 때 문제가 발생할 수 있습니다.
lsof -i :<포트번호>
(macOS/Linux) 또는netstat -ano | findstr :<포트번호>
(Windows PowerShell) 명령으로 해당 포트가 사용 중인지 확인하고,docker-compose.yml
의ports
설정을 변경해 보세요. - 볼륨 및 권한 문제: 볼륨 마운트 시 호스트 파일의 권한 문제로 컨테이너가 파일을 읽거나 쓰지 못하는 경우가 있습니다. 호스트 파일의 권한을 확인하거나,
chmod
명령으로 적절한 권한을 부여해 보세요. - 캐시 문제 (
docker-compose build --no-cache
): Dockerfile 변경 후에도 이전 이미지가 계속 사용되는 것처럼 보인다면,docker-compose build --no-cache
명령으로 캐시를 무시하고 새로 빌드해 보세요. - Compose 파일 버전 확인: 사용 중인 Docker 엔진과 Compose CLI 버전에 따라 지원되는
docker-compose.yml
의version
이 다를 수 있습니다. 최신 버전을 사용하는 것이 좋지만, 간혹 구 버전에서 문제가 발생할 수도 있습니다.
결론 🎉
축하합니다! 🎉 이제 여러분은 Docker Compose의 기본 개념부터 실전 예제, 그리고 고급 활용 팁까지 모두 이해하게 되셨습니다. Docker Compose는 다중 컨테이너 애플리케이션 개발과 배포의 복잡성을 획기적으로 줄여주는 강력한 도구입니다.
처음에는 YAML 파일 작성과 개념들이 다소 생소하게 느껴질 수 있지만, 몇 번의 실습을 통해 금방 익숙해질 것입니다. 오늘 배운 내용을 바탕으로 여러분의 애플리케이션을 Docker Compose로 구성하고 관리해보세요. 개발 workflow가 훨씬 효율적이고 재현성 있게 변할 것입니다.
이제 복잡한 앱도 Docker Compose와 함께라면 두렵지 않습니다! 🚀 Happy Dockering! 🐳