G: 안녕하세요! 복잡한 개발 환경에 지치신 여러분들을 위해, 오늘은 ✨Docker Compose✨라는 마법 같은 도구를 소개해드리려 합니다. 여러 개의 Docker 컨테이너를 하나하나 수동으로 관리하는 번거로움, 이제 그만! Docker Compose가 여러분의 워크플로우를 혁신적으로 바꿔줄 거예요.
이 글을 통해 여러분은 다음을 얻어가실 수 있습니다:
- 🤔 Docker Compose가 왜 필요한지 이해하기
- 💡 Docker Compose의 핵심 개념들을 마스터하기
- 🛠️ 실제 웹 애플리케이션 스택을 예시로 직접 배포해보기
- 🚀 Docker Compose를 더 스마트하게 활용하는 팁 얻기
자, 그럼 시작해볼까요? 레디~ 액션! 🎬
1. 🤔 Docker Compose, 왜 필요할까요?
여러분은 혹시 이런 경험을 해보셨나요?
docker run -d --name my-mongo -p 27017:27017 mongo:latest
docker run -d --name my-backend --link my-mongo:mongo -p 8080:8080 my-backend-image
docker run -d --name my-frontend -p 3000:3000 --link my-backend:backend my-frontend-image
수많은 docker run
명령어와 -p
, --link
, --network
등의 옵션들… 😵💫 이렇게 여러 개의 컨테이너가 복잡하게 엮여있는 애플리케이션을 개발하고 관리하다 보면, 머리가 지끈거리고 실수가 잦아지기 마련입니다. 특히 팀원들과 협업할 때는 “내 컴퓨터에서는 되는데 너 컴퓨터에서는 왜 안 돼?”라는 헬게이트가 열리기도 하죠. 🤯
Docker Compose는 바로 이런 문제들을 해결하기 위해 등장했습니다. Docker Compose는 여러 개의 컨테이너로 구성된 애플리케이션을 정의하고 실행하기 위한 도구입니다. 복잡한 컨테이너들의 설정(이미지, 포트, 볼륨, 네트워크, 환경 변수 등)을 docker-compose.yml
이라는 하나의 YAML 파일에 명시하여, 단 하나의 명령어(docker compose up
)로 모든 컨테이너를 한 번에 띄울 수 있게 해줍니다.
💡 Docker Compose 사용의 핵심 이점:
- ⚡️ 간결함: 모든 설정을 하나의 파일에 집중시켜 관리합니다.
- 🔁 재현성: 어떤 환경에서든 동일한 개발 및 배포 환경을 쉽게 구축할 수 있습니다.
- 🤝 협업: 팀원들과 같은 설정 파일을 공유하여 개발 환경 동기화를 손쉽게 합니다.
- 🔄 일관된 워크플로우: 개발, 테스트, 배포 환경 간의 차이를 최소화합니다.
- 👍 편리한 관리: 컨테이너 시작, 중지, 재시작, 삭제 등이 훨씬 간편해집니다.
한 마디로, Docker Compose는 여러분의 멀티 컨테이너 환경을 매우 우아하고 효율적으로 정리해주는 ‘정리 마스터’라고 할 수 있습니다! ✨
2. 💡 Docker Compose 핵심 개념 완전 정복!
Docker Compose는 docker-compose.yml
이라는 YAML 파일을 통해 작동합니다. 이 파일 안에 우리가 필요한 모든 설정을 정의하게 됩니다. 어떤 내용들이 들어가는지 주요 개념들을 살펴볼까요?
2.1. docker-compose.yml
: 마법의 설계도 🗺️
모든 Docker Compose 프로젝트의 심장입니다. 이 파일은 YAML 문법으로 작성되며, 서비스(컨테이너), 네트워크, 볼륨 등을 정의합니다.
# docker-compose.yml 파일의 예시 구조
version: '3.8' # Docker Compose 파일 형식의 버전 (최신 버전을 사용하는 것이 좋습니다)
services: # 이 섹션에 애플리케이션을 구성하는 각 컨테이너를 정의합니다.
web: # 서비스 이름 (컨테이너 이름과 다를 수 있지만, 기본적으로 컨테이너 이름이 됩니다)
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- api # 'api' 서비스가 먼저 시작되도록 의존성을 명시합니다.
api:
build: . # 현재 디렉토리의 Dockerfile을 사용하여 이미지를 빌드합니다.
environment:
- DB_HOST=database
- DB_PORT=5432
networks:
- my-app-network # 이 서비스가 연결될 사용자 정의 네트워크
database:
image: postgres:13
volumes:
- db_data:/var/lib/postgresql/data # 데이터 영속성을 위한 볼륨
volumes: # 데이터 볼륨을 정의합니다. (데이터 영속성 보장)
db_data:
networks: # 서비스 간 통신을 위한 네트워크를 정의합니다.
my-app-network:
driver: bridge # 기본적으로 bridge 드라이버를 사용합니다.
2.2. services
: 애플리케이션의 핵심 구성 요소들 ⚙️
services
는 Docker Compose 파일의 가장 중요한 부분입니다. 여기에 애플리케이션을 구성하는 각각의 컨테이너를 정의합니다. 각 서비스는 독립적인 컨테이너로 작동하며, 서로 통신할 수 있습니다.
image
vsbuild
:image
: 이미 Docker Hub 등에 미리 빌드되어 푸시된 이미지를 사용할 때 지정합니다.services: my-app: image: myuser/my-app:1.0 # Docker Hub 등에서 이미지를 가져옵니다.
build
: 로컬Dockerfile
을 사용하여 직접 이미지를 빌드할 때 사용합니다. 일반적으로 개발 환경에서 많이 사용됩니다.services: my-app: build: . # 현재 디렉토리의 Dockerfile을 사용합니다. # build: ./backend # 특정 서브디렉토리의 Dockerfile을 사용할 수도 있습니다. context: ./backend # Dockerfile이 있는 경로 dockerfile: Dockerfile # Dockerfile 이름 (기본은 Dockerfile)
ports
: 호스트 머신의 포트와 컨테이너 내부 포트를 연결합니다.HOST_PORT:CONTAINER_PORT
형식입니다.services: web: ports: - "80:80" # 호스트의 80번 포트를 컨테이너의 80번 포트에 연결 - "443:443" # SSL/TLS를 위한 443번 포트 연결
volumes
: 컨테이너의 데이터를 호스트 머신에 영구적으로 저장하거나, 호스트의 파일을 컨테이너 내부에 마운트합니다.HOST_PATH:CONTAINER_PATH
또는VOLUME_NAME:CONTAINER_PATH
형식입니다.services: db: volumes: - db_data:/var/lib/postgresql/data # 명명된 볼륨 (데이터 영속성에 필수!) - ./app/logs:/var/log/app # 호스트 경로 마운트 (로그 파일 등에 유용)
networks
: 서비스 간 통신을 위한 네트워크를 지정합니다. 기본적으로 모든 서비스는 하나의 기본 네트워크에 연결되지만, 명시적으로 사용자 정의 네트워크를 지정하는 것이 좋습니다.services: frontend: networks: - my-app-net backend: networks: - my-app-net # 같은 네트워크에 속한 서비스끼리 이름으로 통신 가능!
environment
: 컨테이너 내부에서 사용할 환경 변수를 설정합니다.services: backend: environment: - NODE_ENV=development - DB_HOST=database # 같은 네트워크 내의 다른 서비스 이름으로 접근 가능! - DB_PORT=5432 # - "MY_SECRET=${ENV_VAR_FROM_HOST}" # 호스트 환경 변수를 가져올 수도 있습니다.
depends_on
: 서비스 간의 의존성을 정의합니다. 명시된 서비스가 먼저 시작된 후에 해당 서비스가 시작됩니다. ⚠️ 주의:depends_on
은 단순히 서비스 “시작 순서”만 보장하며, 해당 서비스가 “완전히 준비(ready)”되었음을 의미하지는 않습니다. 애플리케이션 로직에서 재시도 로직을 구현하거나,healthcheck
를 함께 사용하는 것이 좋습니다.services: backend: depends_on: - database # database 서비스가 먼저 시작된 후 backend가 시작됩니다.
healthcheck
: 컨테이너가 정상적으로 작동하는지 주기적으로 확인합니다. 이는depends_on
의 한계를 보완하여, 서비스가 “준비되었을” 때까지 기다리도록 할 수 있습니다. (Docker Compose v3.4 이상에서depends_on
과healthcheck
를 결합하여condition: service_healthy
를 사용할 수 있습니다.)services: backend: healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 5 depends_on: database: condition: service_healthy # database가 healthcheck를 통과해야 backend 시작
2.3. volumes
와 networks
: 데이터와 소통의 통로 🌉
docker-compose.yml
파일 최상단에서 전역적으로 volumes
와 networks
를 정의하여 서비스들이 공유하거나 사용할 수 있도록 합니다.
volumes
:- 명명된 볼륨 (Named Volumes): 데이터를 컨테이너의 라이프사이클과 독립적으로 관리합니다. 컨테이너가 삭제되어도 데이터는 보존됩니다. 데이터베이스처럼 영속적인 데이터에 필수적입니다.
volumes: db_data: # 'db_data'라는 이름의 볼륨 생성
- 호스트 마운트 (Bind Mounts): 호스트 머신의 특정 경로를 컨테이너 내부에 마운트합니다. 주로 개발 환경에서 소스 코드 변경 사항을 즉시 반영하거나, 설정 파일을 공유할 때 사용됩니다.
services: web: volumes: - ./my-app:/app # 호스트의 './my-app' 디렉토리를 컨테이너의 '/app'에 마운트
- 명명된 볼륨 (Named Volumes): 데이터를 컨테이너의 라이프사이클과 독립적으로 관리합니다. 컨테이너가 삭제되어도 데이터는 보존됩니다. 데이터베이스처럼 영속적인 데이터에 필수적입니다.
networks
:- 사용자 정의 네트워크: 서비스 간의 격리된 통신 채널을 제공합니다. 같은 네트워크에 속한 서비스들은 서로의 서비스 이름으로 통신할 수 있습니다 (DNS Resolution).
networks: app-tier: # 'app-tier'라는 이름의 네트워크 생성 driver: bridge web-tier: # 'web-tier'라는 이름의 네트워크 생성 driver: bridge
- 사용자 정의 네트워크: 서비스 간의 격리된 통신 채널을 제공합니다. 같은 네트워크에 속한 서비스들은 서로의 서비스 이름으로 통신할 수 있습니다 (DNS Resolution).
3. 🛠️ 실전 예제: 웹 애플리케이션 스택 배포하기
이제 실제로 웹 애플리케이션 스택을 Docker Compose로 배포해보겠습니다. 일반적인 시나리오로, Nginx (리버스 프록시/정적 파일 서빙) + Node.js (백엔드 API) + MongoDB (데이터베이스) 구성을 예로 들어볼게요.
3.1. 프로젝트 구조 만들기 📁
먼저, 프로젝트를 위한 디렉토리 구조를 만듭니다.
.
├── docker-compose.yml
├── backend/
│ ├── Dockerfile
│ └── app.js
├── nginx/
│ └── nginx.conf
└── .env # 환경 변수 파일 (선택 사항)
3.2. backend/Dockerfile
작성 📝
Node.js 백엔드 애플리케이션을 위한 Dockerfile을 작성합니다.
# backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8080
CMD ["node", "app.js"]
3.3. backend/app.js
작성 ✍️
간단한 Node.js Express 앱을 만들어 MongoDB 연결을 시도하고 API를 제공합니다.
// backend/app.js
const express = require('express');
const { MongoClient } = require('mongodb');
const app = express();
const port = 8080;
// 환경 변수에서 DB_HOST와 DB_PORT를 가져옵니다.
const dbHost = process.env.DB_HOST || 'localhost';
const dbPort = process.env.DB_PORT || '27017';
const dbName = 'mydatabase';
const mongoUri = `mongodb://${dbHost}:${dbPort}/${dbName}`;
let db;
async function connectToMongo() {
try {
const client = new MongoClient(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true });
await client.connect();
db = client.db(dbName);
console.log(`🎉 MongoDB connected successfully at ${mongoUri}`);
} catch (err) {
console.error(`💔 MongoDB connection failed: ${err.message}`);
console.error(`Attempting to reconnect in 5 seconds...`);
setTimeout(connectToMongo, 5000); // 5초 후 재시도
}
}
connectToMongo(); // MongoDB 연결 시도
app.get('/', (req, res) => {
res.send('Hello from Node.js Backend! 👋');
});
app.get('/status', (req, res) => {
if (db) {
res.json({ status: 'ok', message: 'MongoDB is connected!' });
} else {
res.status(503).json({ status: 'error', message: 'MongoDB is not connected yet.' });
}
});
app.listen(port, () => {
console.log(`Backend API listening on port ${port}`);
});
그리고 backend
디렉토리에 package.json
파일을 생성하고 express
와 mongodb
패키지를 추가합니다.
// backend/package.json
{
"name": "my-backend",
"version": "1.0.0",
"description": "Node.js backend for multi-container app",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.2",
"mongodb": "^6.3.0"
}
}
3.4. nginx/nginx.conf
작성 🌐
Nginx 설정 파일로, backend
서비스로 요청을 프록시합니다.
# nginx/nginx.conf
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html; # 정적 파일 서빙 (만약 frontend가 있다면 여기에)
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:8080/; # 'api'는 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;
}
}
}
3.5. docker-compose.yml
작성 ✍️ (핵심!)
이제 이 모든 서비스를 연결하는 docker-compose.yml
파일을 작성합니다.
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx:latest
container_name: myapp-nginx
ports:
- "80:80" # 호스트의 80번 포트를 Nginx 컨테이너의 80번 포트에 연결
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # Nginx 설정 파일 마운트 (읽기 전용)
# - ./frontend/build:/usr/share/nginx/html # 만약 React/Vue 등 빌드된 프론트엔드가 있다면 여기 마운트
depends_on:
- api # api 서비스가 시작된 후 web 서비스 시작
networks:
- app-network
api:
build:
context: ./backend # backend 디렉토리의 Dockerfile 사용
dockerfile: Dockerfile
container_name: myapp-api
environment:
# MongoDB 서비스 이름은 'mongo'로 접근 (Docker Compose 내부 DNS)
DB_HOST: mongo
DB_PORT: 27017
ports:
- "8080:8080" # (선택 사항) 개발 중 백엔드 직접 접근을 위해 포트 오픈
depends_on:
mongo:
condition: service_healthy # mongo가 건강 상태를 보고할 때까지 api 시작 대기
networks:
- app-network
mongo:
image: mongo:latest
container_name: myapp-mongo
ports:
- "27017:27017" # (선택 사항) 호스트에서 MongoDB Compass 등으로 직접 접근을 위해 포트 오픈
volumes:
- mongo_data:/data/db # MongoDB 데이터 영속성을 위한 명명된 볼륨
healthcheck: # MongoDB 건강 검사 정의
test: echo 'db.runCommand("ping").ok' | mongosh --quiet # MongoDB 핑 테스트
interval: 10s
timeout: 5s
retries: 5
start_period: 20s # 초기 시작 시 건강 검사 시작 전 대기 시간
networks:
- app-network
networks: # 사용자 정의 네트워크 정의
app-network:
driver: bridge
volumes: # 명명된 볼륨 정의
mongo_data:
3.6. .env 파일 (환경 변수) 🔑 (선택 사항)
민감한 정보나 자주 바뀌는 환경 변수는 .env
파일에 저장하고 docker-compose.yml
에서 참조할 수 있습니다.
# .env 파일 (docker-compose.yml과 같은 디렉토리에 위치)
# DB_USERNAME=your_db_user
# DB_PASSWORD=your_db_password
# API_KEY=some_secret_key
(현재 예시에서는 직접 사용하지는 않았지만, 실제 프로젝트에서 유용하게 쓰일 수 있습니다.)
4. 🚀 Docker Compose 명령어 실행하기!
모든 파일 준비가 끝났으니, 이제 Docker Compose의 마법을 부려볼 시간입니다! docker-compose.yml
파일이 있는 디렉토리에서 다음 명령어를 실행합니다.
4.1. 모든 서비스 빌드 및 시작하기 (백그라운드 실행)
$ docker compose up -d
-d
(detach): 컨테이너를 백그라운드에서 실행하여 터미널을 다시 사용할 수 있게 합니다.docker compose
: Docker 엔진 20.10.0 버전부터docker-compose
대신docker compose
명령어를 사용할 수 있습니다. 기존docker-compose
플러그인은 별도 설치가 필요했지만, 이제 Docker Desktop과 함께 기본으로 제공되는 통합 명령어입니다. 최신 사용법이므로 이를 따르는 것이 좋습니다.
4.2. 실행 중인 서비스 확인하기
$ docker compose ps
이 명령어를 실행하면, web
, api
, mongo
세 개의 컨테이너가 모두 Up
상태로 잘 실행되고 있는지 확인할 수 있습니다.
4.3. 컨테이너 로그 확인하기
$ docker compose logs -f
모든 서비스의 실시간 로그를 볼 수 있습니다. 특정 서비스의 로그만 보려면 서비스 이름을 지정합니다.
$ docker compose logs -f api # api 서비스의 로그만 보기
4.4. 서비스 테스트하기
웹 브라우저에서 http://localhost/api/status
에 접속해보세요.
Nginx가 요청을 받아 api
서비스로 프록시하고, api
서비스는 MongoDB 연결 상태를 확인하여 JSON 응답을 줄 것입니다.
{"status":"ok","message":"MongoDB is connected!"}
만약 MongoDB 연결에 실패했다면, status: 'error'
가 표시될 것입니다.
4.5. 특정 서비스 재시작하기
$ docker compose restart api
4.6. 특정 서비스 중지하기 (컨테이너는 남겨둠)
$ docker compose stop api
4.7. 모든 서비스 중지 및 컨테이너 삭제
$ docker compose down
이 명령은 모든 컨테이너, 네트워크를 중지하고 삭제합니다. volumes
는 기본적으로 삭제되지 않으므로, 데이터가 보존됩니다.
4.8. 모든 서비스 중지, 컨테이너 및 볼륨까지 삭제
$ docker compose down -v
-v
(volumes): 명명된 볼륨까지 함께 삭제합니다. 경고: 이 명령은 데이터베이스 데이터 등을 영구적으로 삭제하므로 주의해서 사용하세요! 🚨
5. 🚀 Docker Compose, 더 스마트하게 활용하기!
Docker Compose는 단순한 시작/중지 외에도 강력한 기능들을 제공합니다.
5.1. .env
파일로 환경 변수 관리 🔐
민감한 정보(API 키, 데이터베이스 비밀번호)나 환경에 따라 달라지는 값들은 .env
파일에 저장하고 docker-compose.yml
에서 참조하는 것이 좋습니다.
# .env 파일 (docker-compose.yml과 같은 디렉토리)
DB_USER=myuser
DB_PASSWORD=mypassword
# docker-compose.yml
services:
backend:
environment:
- DB_USER=${DB_USER} # .env 파일에서 DB_USER 값을 가져옴
- DB_PASSWORD=${DB_PASSWORD}
5.2. 여러 Compose 파일 사용하기 (개발/운영 환경 분리) 🌳
개발 환경과 운영 환경의 설정이 다를 때, 여러 개의 Compose 파일을 오버라이드하여 사용할 수 있습니다.
예를 들어, docker-compose.yml
(기본), docker-compose.dev.yml
(개발), docker-compose.prod.yml
(운영) 파일을 만들 수 있습니다.
docker-compose.yml
(공통 설정)docker-compose.dev.yml
(개발 환경에서만 추가될 설정)# docker-compose.dev.yml services: api: build: context: ./backend dockerfile: Dockerfile.dev # 개발 전용 Dockerfile 사용 volumes: - ./backend:/app # 소스 코드 실시간 반영을 위한 바인드 마운트
- 실행 명령어:
$ docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
-f
옵션을 여러 번 사용하여 파일을 지정할 수 있으며, 뒤에 오는 파일이 앞 파일의 설정을 덮어씁니다.
5.3. 서비스 스케일링 (수평 확장) 📈
특정 서비스의 컨테이너 수를 늘려 부하를 분산할 수 있습니다.
$ docker compose up -d --scale api=3 # api 서비스를 3개의 컨테이너로 실행
5.4. profiles
를 이용한 서비스 그룹화 (Compose v3.4 이상) 🏷️
프로젝트 내에 여러 개의 독립적인 애플리케이션 스택이 있거나, 특정 개발 시나리오에만 필요한 서비스들이 있을 때 profiles
를 사용하여 서비스를 그룹화할 수 있습니다.
# docker-compose.yml 예시
version: '3.8'
services:
web:
image: nginx
ports: ["80:80"]
api:
build: ./backend
admin-tool:
image: adminer/adminer # 관리자 툴
ports: ["8081:8080"]
profiles: ["admin"] # 'admin' 프로필이 활성화될 때만 실행
qa-tester:
image: cypress/included # QA 테스트용 컨테이너
profiles: ["qa"] # 'qa' 프로필이 활성화될 때만 실행
networks:
default:
- 모든 서비스 실행 (profiles 제외):
$ docker compose up -d
(web, api만 실행) - 특정 프로필 활성화하여 실행:
$ docker compose --profile admin up -d
(web, api, admin-tool 실행)$ docker compose --profile qa up -d
(web, api, qa-tester 실행) - 여러 프로필 동시 활성화:
$ docker compose --profile admin --profile qa up -d
(web, api, admin-tool, qa-tester 실행)
맺음말 ✨
지금까지 Docker Compose가 무엇인지, 왜 필요한지, 그리고 어떻게 사용하는지 자세히 알아보았습니다. 복잡한 멀티 컨테이너 환경을 Docker Compose로 한 방에 정리하는 방법을 익히셨기를 바랍니다!
Docker Compose는 특히 로컬 개발 환경 구축에 탁월한 성능을 발휘합니다. 개발 환경을 코드로 관리하고, 팀원들과의 협업을 원활하게 하며, “내 컴퓨터에서는 되는데…”와 같은 문제를 효과적으로 줄여줄 것입니다. 💖
물론, 프로덕션 환경에서 대규모 분산 시스템을 운영할 때는 Kubernetes와 같은 더 강력한 오케스트레이션 도구를 고려해야 합니다. 하지만 개발 및 테스트 환경에서는 Docker Compose만으로도 충분히 강력하고 유연한 환경을 구축할 수 있습니다.
이제 여러분의 개발 워크플로우를 Docker Compose로 업그레이드할 시간입니다! 🚀 궁금한 점이 있다면 언제든지 댓글로 남겨주세요. 다음에는 더 유익한 정보로 찾아뵙겠습니다! 해피 도커링! 🐳👋