G: 안녕하세요! 복잡한 컨테이너 관리, 더 이상 걱정하지 마세요! 😌 오늘날 마이크로서비스 아키텍처와 컨테이너 기반 개발이 대세가 되면서, 우리는 수많은 도커 컨테이너를 다루게 됩니다. 웹 서버, 데이터베이스, 백엔드 API, 프론트엔드 빌드 등 각기 다른 컨테이너들이 서로 유기적으로 연결되어 동작해야 하죠. 🤔
이 모든 컨테이너를 일일이 docker run
명령어로 실행하고, 서로 연결하는 것은 매우 번거롭고 실수하기 쉽습니다. 이럴 때 빛을 발하는 것이 바로 Docker Compose입니다! ✨
Docker Compose는 여러 개의 도커 컨테이너를 정의하고 실행하는 도구입니다. 단일 docker-compose.yml
파일 하나로 애플리케이션의 모든 서비스를 구성하고, 한 번의 명령어로 전체 스택을 올리고 내릴 수 있게 해줍니다. 마치 오케스트라의 지휘자처럼, 다양한 악기(컨테이너)들을 조화롭게 연주시키는 역할을 하죠! 🎼
이번 글에서는 Docker Compose의 핵심인 docker-compose.yml
파일의 기본 구조를 완벽하게 이해하고, 실용적인 예제와 함께 자세히 알아보겠습니다. 이 글을 다 읽고 나면 여러분은 자신 있게 첫 번째 docker-compose.yml
파일을 작성할 수 있을 거예요! 👍
1. Docker Compose, 왜 필요한가요? 🤔
단일 컨테이너 애플리케이션이라면 docker run
으로 충분합니다. 하지만 대부분의 실 서비스는 여러 컴포넌트로 구성됩니다. 예를 들어, 흔히 볼 수 있는 웹 서비스 스택은 다음과 같습니다:
- 웹 서버: (Nginx, Apache) 웹 요청을 처리하고 정적 파일을 제공.
- 애플리케이션 서버: (Node.js, Python Flask, Java Spring Boot) 비즈니스 로직 처리.
- 데이터베이스: (PostgreSQL, MySQL, MongoDB) 데이터 저장.
- 캐시 서버: (Redis, Memcached) 빠른 데이터 접근.
이러한 서비스들을 각각 수동으로 실행하고, 네트워크를 설정하고, 볼륨을 연결하는 것은 매우 비효율적입니다.
- 수동 설정의 어려움: 각 컨테이너의 포트, 네트워크, 볼륨 등을 일일이 기억하고 명령어를 입력해야 합니다.
- 환경 일관성 문제: 개발, 테스트, 운영 환경 간의 설정 차이로 인한 문제가 발생할 수 있습니다.
- 협업의 비효율성: 팀원들이 동일한 환경을 구축하는 데 시간이 오래 걸립니다.
Docker Compose는 이러한 문제들을 해결해줍니다! 🛠️
- 선언적 정의: 모든 서비스와 그 설정을 YAML 파일 하나에 명시합니다.
- 단일 명령어 실행:
docker compose up
명령 한 번으로 전체 스택을 쉽게 배포하고 관리할 수 있습니다. - 환경 일관성: 팀원 모두가 동일한 개발 환경을 신속하게 구축할 수 있습니다.
- 재사용성: 한 번 정의한 설정은 언제든 재사용할 수 있습니다.
2. docker-compose.yml
파일, 기본 구조 뼈대 잡기! 🦴
docker-compose.yml
파일은 YAML(YAML Ain’t Markup Language) 형식으로 작성됩니다. YAML은 사람이 읽기 쉬운 데이터 직렬화 언어로, 들여쓰기를 통해 구조를 표현하는 것이 특징입니다. 들여쓰기가 매우 중요하니 주의하세요! (보통 2칸 공백을 사용합니다.)
docker-compose.yml
파일의 최상위 레벨에는 주로 다음 키들이 사용됩니다.
version
: Docker Compose 파일 형식을 지정합니다. 최신 기능과 문법을 사용하기 위해 보통3.x
버전을 명시합니다. 현재3.8
이상이 널리 사용됩니다.services
: 애플리케이션을 구성하는 모든 컨테이너 서비스들을 정의하는 핵심 섹션입니다.volumes
: 컨테이너 간에 공유되거나 데이터를 영구적으로 저장할 볼륨을 정의합니다.networks
: 서비스들이 통신할 수 있는 네트워크를 정의합니다.
가장 기본적인 docker-compose.yml
파일의 뼈대는 다음과 같습니다.
# docker-compose.yml
version: '3.8' # Docker Compose 파일 형식 버전 지정
services:
# 여기에 각 서비스(컨테이너) 정의가 들어갑니다.
# 예: 웹 서버, 데이터베이스, 백엔드 앱 등
volumes:
# 여기에 영속적인 데이터를 저장할 볼륨 정의가 들어갑니다.
networks:
# 여기에 서비스 간 통신을 위한 네트워크 정의가 들어갑니다.
자, 이제 이 뼈대 안의 각 섹션들을 자세히 파헤쳐 볼까요? 🕵️♀️
3. 핵심 요소 완벽 분석: services
💡
services
섹션은 Docker Compose 파일에서 가장 중요하고 자주 사용되는 부분입니다. 이곳에서 여러분의 애플리케이션을 구성하는 각각의 컨테이너를 정의합니다. 각 서비스는 고유한 이름(예: web
, db
, api
)을 가지며, 그 아래에 해당 컨테이너에 대한 다양한 설정을 명시합니다.
각 서비스에서 사용할 수 있는 주요 지시어들을 예시와 함께 살펴보겠습니다.
3.1. image
: 기존 도커 이미지 사용하기
가장 간단하게 서비스를 정의하는 방법입니다. Docker Hub 등에서 제공하는 기존 이미지를 사용하여 컨테이너를 생성합니다.
services:
nginx:
image: nginx:latest # nginx 최신 이미지를 사용합니다.
redis:
image: redis:6-alpine # redis 6 버전의 alpine 경량 이미지를 사용합니다.
3.2. build
: Dockerfile로부터 이미지 빌드하기
만약 여러분의 애플리케이션이 직접 만든 코드 기반이라면, Dockerfile
을 통해 이미지를 빌드해야 합니다.
build: .
: 현재docker-compose.yml
파일이 있는 디렉토리에서Dockerfile
을 찾아서 빌드합니다.build: ./my_app
:my_app
이라는 하위 디렉토리에서Dockerfile
을 찾아서 빌드합니다.context
와dockerfile
: 특정 디렉토리를 빌드 컨텍스트로 지정하고, 그 안에 있는 특정Dockerfile
을 지정할 수 있습니다.
services:
webapp:
build: . # 현재 디렉토리의 Dockerfile로 이미지를 빌드합니다.
# 또는
# build:
# context: ./my_node_app # ./my_node_app 디렉토리를 빌드 컨텍스트로 사용
# dockerfile: Dockerfile.prod # my_node_app/Dockerfile.prod 파일을 사용
3.3. ports
: 호스트와 컨테이너 포트 연결하기
컨테이너 내부에서 실행되는 애플리케이션에 외부에서 접근할 수 있도록 호스트 머신의 포트와 컨테이너의 포트를 연결합니다.
"호스트_포트:컨테이너_포트"
형식으로 지정합니다.
services:
nginx:
image: nginx:latest
ports:
- "80:80" # 호스트의 80번 포트를 컨테이너의 80번 포트에 연결
- "443:443" # 호스트의 443번 포트를 컨테이너의 443번 포트에 연결
api:
build: .
ports:
- "3000:3000" # 호스트의 3000번 포트를 컨테이너의 3000번 포트에 연결
3.4. volumes
: 데이터 영속성 및 공유
컨테이너가 삭제되어도 데이터가 사라지지 않도록 하거나, 호스트와 컨테이너 간에 파일을 공유할 때 사용합니다.
- 바인드 마운트 (
host_path:container_path
): 호스트 머신의 특정 경로를 컨테이너 내부 경로에 연결합니다. 주로 개발 시 코드 변경사항을 바로 반영하거나, 설정 파일을 마운트할 때 사용합니다. - 명명된 볼륨 (
volume_name:container_path
): Docker가 관리하는 이름 있는 볼륨을 사용합니다. 주로 데이터베이스처럼 영속적인 데이터가 필요한 경우에 사용하며, 아래volumes
섹션에서 정의해야 합니다.
services:
db:
image: postgres:13
volumes:
- db_data:/var/lib/postgresql/data # 'db_data'라는 명명된 볼륨을 사용
webapp:
build: .
volumes:
- ./app:/usr/src/app # 현재 디렉토리의 app 폴더를 컨테이너 내부의 /usr/src/app에 마운트
- /usr/src/app/node_modules # 이 경로는 호스트에 마운트하지 않음 (npm install 결과 저장)
3.5. environment
: 환경 변수 설정
컨테이너 내부에서 사용할 환경 변수를 설정합니다. 민감한 정보는 env_file
을 사용하는 것이 더 안전합니다.
services:
api:
build: .
environment:
- NODE_ENV=production
- DB_HOST=db # 데이터베이스 서비스 이름 'db'를 호스트로 사용
- DB_USER=myuser
- DB_PASSWORD=mypassword
3.6. env_file
: 환경 변수 파일 로드
.env
파일 등 외부에 정의된 환경 변수들을 로드할 때 사용합니다. 보안과 설정 분리에 유용합니다.
# .env 파일 예시:
# DB_HOST=db
# DB_PORT=5432
# API_KEY=your_secret_key
services:
api:
build: .
env_file:
- ./.env # 현재 디렉토리의 .env 파일에서 환경 변수를 로드
- ./config/secrets.env # 다른 경로의 파일도 로드 가능
3.7. depends_on
: 서비스 의존성 정의 (시작 순서)
특정 서비스가 다른 서비스가 시작된 후에 시작되어야 할 때 사용합니다. 예를 들어, 웹 애플리케이션은 데이터베이스가 먼저 시작되어야 제대로 동작할 수 있습니다.
주의: depends_on
은 시작 순서만 보장하며, 서비스가 ‘준비되었음(ready)’을 보장하지 않습니다. 즉, db
컨테이너가 시작되었지만 데이터베이스 서비스 자체가 아직 포트 리스닝을 시작하지 않았을 수도 있습니다. 완전한 준비 상태를 확인하려면 healthcheck
를 사용하는 것이 좋습니다.
services:
webapp:
build: .
depends_on:
- db # db 서비스가 시작된 후에 webapp 서비스가 시작됩니다.
- redis # redis 서비스가 시작된 후에 webapp 서비스가 시작됩니다.
db:
image: postgres:13
redis:
image: redis:6-alpine
3.8. networks
: 특정 네트워크에 연결
서비스를 특정 네트워크에 연결합니다. 기본적으로 모든 서비스는 하나의 기본 네트워크에 연결되지만, 명시적으로 네트워크를 정의하고 연결하면 서비스 간의 격리나 통신 구조를 명확히 할 수 있습니다.
services:
web:
image: nginx
networks:
- frontend_network # frontend_network에 연결
- backend_network # backend_network에도 연결
api:
build: .
networks:
- backend_network # backend_network에 연결
db:
image: postgres
networks:
- backend_network # backend_network에 연결
networks:
frontend_network: # frontend_network 정의
backend_network: # backend_network 정의
3.9. container_name
: 컨테이너 이름 지정
docker ps
등으로 확인할 때 컨테이너에 알아보기 쉬운 이름을 지정할 수 있습니다. 지정하지 않으면 디렉토리명-서비스명-1
과 같은 형태로 자동 생성됩니다.
services:
nginx:
image: nginx
container_name: my_awesome_nginx
3.10. restart
: 컨테이너 재시작 정책
컨테이너가 종료되었을 때 자동으로 재시작할지 여부를 설정합니다.
no
: 재시작 안 함 (기본값)always
: 컨테이너가 종료될 때마다 항상 재시작합니다.on-failure
: 컨테이너가 비정상적으로 종료되었을 때(exit code가 0이 아닐 때)만 재시작합니다.unless-stopped
: 컨테이너가 수동으로 중지될 때까지 재시작합니다.
services:
api:
build: .
restart: always # 항상 재시작
db:
image: postgres
restart: on-failure # 비정상 종료 시 재시작
3.11. command
/ entrypoint
: 컨테이너 시작 명령 오버라이드
Dockerfile에 정의된 기본 명령을 덮어쓰거나 추가 명령을 실행할 때 사용합니다.
command
: 컨테이너가 시작될 때 실행될 명령을 정의합니다. Dockerfile의CMD
를 오버라이드합니다.entrypoint
: 컨테이너가 시작될 때 가장 먼저 실행될 명령을 정의합니다. Dockerfile의ENTRYPOINT
를 오버라이드합니다.command
는entrypoint
의 인자처럼 동작합니다.
services:
my_app:
image: my_custom_image
command: ["python", "app.py", "--debug"] # 기본 명령 대신 이 명령으로 시작
db_client:
image: postgres
entrypoint: ["psql"] # psql 클라이언트로 바로 접속
command: ["-U", "postgres", "-h", "db"] # psql에 전달할 인자
3.12. healthcheck
: 서비스 건강 상태 확인
서비스가 실제로 준비되었는지(예: 웹 서버가 200 OK 응답을 주는지, 데이터베이스가 연결 가능한지) 주기적으로 확인합니다. depends_on
의 한계를 보완해줍니다.
services:
web:
image: nginx
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"] # HTTP 200 응답 확인
interval: 1m30s # 1분 30초마다 검사
timeout: 10s # 10초 내에 응답 없으면 실패
retries: 3 # 3번 실패하면 unhealthy
start_period: 40s # 처음 40초 동안은 healthcheck 실패해도 unhealthy로 간주 안 함
4. 데이터 영속성을 위한 volumes
💾
volumes
섹션은 컨테이너가 삭제되더라도 데이터를 보존하고 싶을 때 사용합니다. Docker Compose 파일에서 명시적으로 볼륨을 정의하면, Docker가 이 볼륨을 생성하고 관리해줍니다.
volumes:
db_data: # 'db_data'라는 이름의 볼륨 정의
app_logs: # 'app_logs'라는 이름의 볼륨 정의
driver: local # 기본 드라이버는 'local' (명시하지 않아도 됨)
driver_opts:
type: nfs
o: addr=192.168.1.1,rw
device: ":/path/to/nfs/share" # NFS 서버 연결 예시
이처럼 정의된 볼륨은 위에서 본 services
섹션의 volumes
지시어에서 db_data:/var/lib/postgresql/data
와 같이 사용될 수 있습니다.
5. 서비스 간 통신을 위한 networks
🔗
기본적으로 Docker Compose는 모든 서비스가 포함된 하나의 default
네트워크를 생성합니다. 따라서 같은 docker-compose.yml
파일 내의 서비스들은 서로의 서비스 이름을 사용하여 통신할 수 있습니다 (예: webapp
서비스에서 db
서비스로 연결할 때 호스트를 db
로 지정).
하지만 더 복잡한 애플리케이션이나 보안상 분리가 필요할 때, 직접 네트워크를 정의할 수 있습니다.
networks:
frontend: # 'frontend' 네트워크 정의
backend: # 'backend' 네트워크 정의
driver: bridge # 기본 드라이버는 bridge (명시하지 않아도 됨)
ipam: # IP 주소 관리 설정 (선택 사항)
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
이렇게 정의된 네트워크는 위에서 본 services
섹션의 networks
지시어에서 networks: - frontend
와 같이 사용됩니다.
네트워크 사용의 장점:
- 격리: 특정 서비스만 특정 네트워크에 참여시켜 불필요한 통신을 차단할 수 있습니다.
- 명확한 구조: 서비스 간의 통신 관계를 시각적으로 명확히 보여줍니다.
- IP 주소 관리:
ipam
을 통해 특정 IP 대역을 할당할 수 있습니다.
6. docker-compose.yml
실행하기 🚀
이제 docker-compose.yml
파일 작성을 마쳤다면, 이를 실행하는 방법을 알아봐야겠죠?
💡 참고: 최신 Docker 버전에서는
docker-compose
명령어가docker compose
(하이픈 없이)로 통합되었습니다. 하지만 기존docker-compose
도 여전히 잘 동작합니다. 이 글에서는 일반적으로 사용되는docker-compose
를 기준으로 설명합니다.
docker-compose.yml
파일이 있는 디렉토리에서 다음 명령어를 실행합니다.
docker-compose up
:docker-compose.yml
파일에 정의된 모든 서비스를 백그라운드가 아닌 포그라운드(현재 터미널)에서 실행하고 로그를 출력합니다. 개발 중 로그를 실시간으로 확인하고 싶을 때 유용합니다.docker-compose up -d
: 모든 서비스를 백그라운드(detached mode)에서 실행합니다. 터미널을 닫아도 컨테이너는 계속 실행됩니다.docker-compose down
:docker-compose.yml
파일에 정의된 모든 서비스 컨테이너를 중지하고 삭제합니다. 이 때 기본적으로 볼륨은 삭제되지 않습니다. 볼륨까지 삭제하려면docker-compose down -v
를 사용합니다.docker-compose ps
: 현재 실행 중인 Compose 서비스 목록을 보여줍니다.docker-compose logs [서비스명]
: 특정 서비스의 로그를 확인합니다. 서비스명을 지정하지 않으면 모든 서비스의 로그를 보여줍니다.docker-compose build
: 서비스에build
지시어가 있는 경우, 해당 이미지를 미리 빌드합니다.up
명령 시 자동으로 빌드되지만, 미리 빌드하여 에러를 확인하고 싶을 때 사용합니다.
7. 실전 예제: 웹 애플리케이션 스택 구축! 🌐
이제 위에서 배운 내용을 바탕으로 간단한 웹 애플리케이션 스택을 구축해보겠습니다. 이 스택은 다음과 같이 구성됩니다:
- Nginx: 웹 서버 역할을 하며, 프론트엔드 정적 파일 서빙 및 백엔드 API로 요청을 프록시합니다.
- Node.js API: 간단한 RESTful API를 제공합니다. (
/api
경로로 요청이 들어오면 응답) - MongoDB: API 서버가 사용할 데이터베이스입니다.
프로젝트 구조:
my-web-app/
├── docker-compose.yml
├── nginx/
│ └── nginx.conf
└── backend/
├── Dockerfile
└── app.js
backend/app.js
(간단한 Node.js Express 앱):
// backend/app.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/api', (req, res) => {
res.json({ message: 'Hello from Node.js API!', timestamp: new Date() });
});
app.listen(port, () => {
console.log(`Backend API listening at http://localhost:${port}`);
});
backend/Dockerfile
:
# backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
nginx/nginx.conf
(Nginx 설정 파일):
# nginx/nginx.conf
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name localhost;
# 정적 파일 (예: index.html)은 여기서 직접 서빙할 수 있지만,
# 이 예제에서는 단순화를 위해 생략했습니다.
# 실제 프론트엔드 앱이 있다면 여기에 root 및 index를 설정합니다.
location /api {
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;
}
# 다른 경로에 대한 기본 처리 (예: 프론트엔드 앱 라우팅)
location / {
root /usr/share/nginx/html; # Nginx 기본 정적 파일 경로 (테스트용)
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
}
docker-compose.yml
:
# docker-compose.yml
version: '3.8'
services:
nginx:
image: nginx:latest
container_name: my_app_nginx
ports:
- "80:80" # 호스트 80번 포트를 Nginx 80번 포트에 연결
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # Nginx 설정 파일을 읽기 전용으로 마운트
# - ./frontend/build:/usr/share/nginx/html # 실제 프론트엔드 빌드 파일을 마운트할 경우
depends_on:
- backend # backend 서비스가 시작된 후에 Nginx 시작
networks:
- app_network # app_network에 연결
backend:
build: ./backend # backend 폴더의 Dockerfile로 이미지 빌드
container_name: my_app_backend
environment:
# 실제 MongoDB 연결 정보는 여기에 추가
MONGODB_URI: mongodb://mongodb:27017/mydatabase # MongoDB 서비스 이름 'mongodb' 사용
ports:
- "3000:3000" # 개발용으로 호스트 포트도 열어둠 (Nginx 프록시를 통해 접근 가능)
depends_on:
- mongodb # mongodb 서비스가 시작된 후에 backend 시작
networks:
- app_network # app_network에 연결
mongodb:
image: mongo:latest # MongoDB 최신 이미지 사용
container_name: my_app_mongodb
volumes:
- mongo_data:/data/db # 명명된 볼륨을 사용하여 데이터 영속성 확보
networks:
- app_network # app_network에 연결
networks:
app_network: # 모든 서비스가 공유할 네트워크 정의
volumes:
mongo_data: # MongoDB 데이터를 저장할 명명된 볼륨 정의
실행 방법:
- 위의 파일들을 각각의 경로에 맞게 생성합니다.
my-web-app
디렉토리로 이동합니다.- 터미널에서 다음 명령어를 실행합니다:
docker-compose up -d
- 컨테이너들이 성공적으로 실행되었는지 확인합니다:
docker-compose ps
접근 테스트:
- 웹 브라우저에서
http://localhost/api
에 접속해보세요. Node.js API가 응답하는 JSON 메시지를 볼 수 있을 거예요! 🎉
이 예제를 통해 여러분은 Docker Compose가 여러 서비스를 어떻게 한데 묶고, 서로 통신하게 하며, 데이터를 영속적으로 관리하는지 명확히 이해할 수 있을 것입니다.
결론: Docker Compose, 이제 여러분의 무기입니다! ✨
지금까지 Docker Compose의 docker-compose.yml
파일 기본 구조와 핵심 요소들을 자세히 살펴보았습니다.
version
: 파일 형식 버전 명시services
: 개별 컨테이너 서비스 정의 (image, build, ports, volumes, environment, depends_on, networks 등)volumes
: 영속적인 데이터 저장을 위한 볼륨 정의networks
: 서비스 간 통신을 위한 네트워크 정의
Docker Compose는 복잡한 다중 컨테이너 애플리케이션의 개발 및 배포를 놀라울 정도로 단순화시켜 줍니다. 이제 여러분은 단일 YAML 파일로 전체 애플리케이션 스택을 관리할 수 있게 되었어요! 🥳
다음 단계는 무엇일까요?
- 직접 예제를 수정하고 실행해보세요: 포트 번호를 바꿔보거나, 다른 이미지를 추가해보는 등 직접 코드를 만져보는 것이 가장 좋은 학습 방법입니다.
- 고급 기능 탐색:
extends
,profiles
,configs
,secrets
,deploy
등 Docker Compose의 더 많은 고급 기능들을 학습하여 프로덕션 환경에 가까운 설정을 구성해보세요. - Docker Swarm 또는 Kubernetes: Docker Compose는 주로 개발 환경이나 소규모 프로덕션에 적합합니다. 대규모 분산 시스템에서는 Docker Swarm이나 Kubernetes와 같은 오케스트레이션 도구로 확장하게 됩니다.
Docker Compose는 컨테이너 기술의 강력함을 최대한 활용할 수 있도록 돕는 필수 도구입니다. 이 글이 여러분의 컨테이너 여정에 훌륭한 출발점이 되기를 바랍니다! 🎓
궁금한 점이 있다면 언제든지 댓글로 질문해주세요! 해피 도커링! 🐳