G: 안녕하세요, 개발자 여러분! 👋 복잡한 마이크로서비스 아키텍처나 여러 컨테이너로 구성된 애플리케이션을 개발하고 계신가요? 매번 docker run
명령어를 여러 개 입력하거나 수동으로 네트워크를 설정하는 데 지치셨다면, 바로 Docker Compose가 여러분의 구세주가 될 수 있습니다!
Docker Compose는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다. YAML 파일을 사용하여 애플리케이션의 모든 서비스를 구성하면, 단일 명령어로 전체 애플리케이션을 빌드하고 실행하며 관리할 수 있습니다. 마치 오케스트라의 지휘자처럼, Docker Compose는 여러분의 컨테이너들을 일사천리로 움직이게 합니다.
이 글에서는 개발자가 Docker Compose를 더욱 효율적으로 활용할 수 있도록, YAML 파일 작성 팁과 함께 실용적인 베스트 프랙티스들을 자세히 알아보겠습니다.
1. Docker Compose YAML 기본 구조 이해하기 🏗️
Docker Compose 파일은 .yaml
또는 .yml
확장자를 가지며, 다음과 같은 주요 섹션으로 구성됩니다.
# docker-compose.yml
version: '3.8' # Docker Compose 파일 형식 버전
services: # 애플리케이션을 구성하는 서비스들 정의
webapp:
# ... 서비스 설정 ...
database:
# ... 서비스 설정 ...
networks: # 컨테이너 간 통신을 위한 네트워크 정의 (선택 사항)
app_network:
driver: bridge
volumes: # 데이터 영속성을 위한 볼륨 정의 (선택 사항)
db_data:
version
: Docker Compose 파일 형식을 지정합니다. 최신 기능과 문법을 사용하려면 최신 버전을 사용하는 것이 좋습니다 (예:'3.8'
).services
: 이 섹션은 애플리케이션을 구성하는 핵심 부분입니다. 각 서비스는 독립적인 컨테이너로 실행되며, 웹 서버, 데이터베이스, 백엔드 API 등 개별 구성 요소를 나타냅니다.networks
: 컨테이너들이 서로 통신할 수 있도록 네트워크를 정의합니다. 명시적으로 정의하지 않으면 기본 네트워크가 생성됩니다.volumes
: 컨테이너의 데이터를 영구적으로 저장하기 위한 볼륨을 정의합니다. 컨테이너가 삭제되어도 데이터가 유지될 수 있도록 합니다.
💡 팁: YAML 파일은 들여쓰기가 매우 중요합니다! 항상 스페이스 2칸 또는 4칸을 사용하여 정확한 구조를 유지하세요. 탭은 사용하지 않는 것이 좋습니다.
2. 핵심 서비스 지시어 활용 팁 🚀
services
아래의 각 서비스는 다양한 지시어를 통해 컨테이너의 동작을 세밀하게 제어할 수 있습니다.
2.1. image
vs. build
🛠️
image
: 이미 빌드되어 Docker Hub나 다른 레지스트리에 저장된 이미지를 사용할 때 지정합니다. 간편하게 컨테이너를 실행할 수 있습니다.services: database: image: postgres:14-alpine # PostgreSQL 14 버전 이미지 사용
build
: 여러분의 소스 코드로부터 Docker 이미지를 직접 빌드할 때 사용합니다.Dockerfile
이 있는 경로를 지정합니다.services: webapp: build: ./web_app # 현재 디렉토리의 'web_app' 폴더에 있는 Dockerfile로 빌드 context: . # Dockerfile이 있는 디렉토리 (기본값) dockerfile: Dockerfile.dev # 사용할 Dockerfile 이름 (기본값 Dockerfile)
💡 팁: 개발 환경에서는
build
를 사용하여 코드 변경 시 바로 반영되도록 하고, 배포 환경에서는 미리 빌드된image
를 사용하는 것이 일반적입니다.
2.2. ports
🌐
호스트 머신과 컨테이너 간의 포트 매핑을 설정합니다. HOST_PORT:CONTAINER_PORT
형식으로 지정합니다.
services:
webapp:
image: my_webapp_image
ports:
- "80:80" # 호스트의 80번 포트를 컨테이너의 80번 포트에 연결
- "443:443" # HTTPS 포트
- "8080" # 호스트의 임의의 사용 가능한 포트를 컨테이너의 8080번에 연결
⚠️ 주의: 8080
처럼 컨테이너 포트만 지정하면, Docker가 호스트에 임의의 사용 가능한 포트를 할당합니다. docker-compose ps
명령으로 확인해야 합니다.
2.3. volumes
💾
데이터 영속성을 확보하거나 호스트와 컨테이너 간에 파일을 공유할 때 사용합니다.
- 바인드 마운트 (Bind Mounts): 호스트 머신의 특정 경로를 컨테이너 내의 경로에 연결합니다. 개발 환경에서 소스 코드 동기화에 유용합니다.
services: webapp: image: my_webapp_image volumes: - ./src:/app/src # 호스트의 './src' 폴더를 컨테이너의 '/app/src'에 마운트 - ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro # 읽기 전용으로 마운트
💡 팁: 개발 시 소스 코드를 바인드 마운트하면, 호스트에서 코드를 수정해도 컨테이너 내에서 바로 반영되어 개발 속도를 높일 수 있습니다.
- 명명된 볼륨 (Named Volumes): Docker가 관리하는 볼륨으로, 주로 데이터베이스 파일처럼 영구적인 데이터를 저장할 때 사용합니다.
services: database: image: postgres:14-alpine volumes: - db_data:/var/lib/postgresql/data # 'db_data'라는 명명된 볼륨 사용 volumes: # 볼륨 정의 섹션 db_data: driver: local # 로컬 드라이버 사용
💡 팁: 명명된 볼륨은 컨테이너가 삭제되어도 데이터가 보존되기 때문에, 데이터베이스나 캐시처럼 중요한 데이터를 저장하는 데 필수적입니다.
2.4. environment
& env_file
🔒
환경 변수를 설정하여 컨테이너 동작을 유연하게 제어합니다.
environment
: YAML 파일 내부에 직접 환경 변수를 정의합니다.services: webapp: environment: - NODE_ENV=development - DATABASE_URL=postgres://user:pass@db_host:5432/mydb
env_file
:.env
파일 등 외부 파일에서 환경 변수를 로드합니다. 민감한 정보(비밀번호, API 키)를 관리하는 데 필수적입니다.services: webapp: env_file: - .env.development # 개발 환경 변수 파일 - .env.secrets # 민감한 비밀 정보 파일
⚠️ 주의: 민감한 정보는
environment
에 직접 노출하지 말고,.env
파일을 사용하고.gitignore
에 추가하여 버전 관리에서 제외해야 합니다.
2.5. depends_on
🔗
서비스 간의 의존성을 명시하여 특정 서비스가 시작된 후에 다른 서비스가 시작되도록 합니다.
services:
webapp:
build: .
depends_on:
- database # webapp은 database가 먼저 시작된 후에 시작
- cache # webapp은 cache가 먼저 시작된 후에 시작
database:
image: postgres:14-alpine
cache:
image: redis:latest
⚠️ 주의: depends_on
은 단순히 서비스 시작 순서만 보장하며, 서비스가 준비 완료되었음을 보장하지는 않습니다. 예를 들어, 데이터베이스 컨테이너가 시작되었어도 내부적으로 DB 서버가 완전히 구동되기까지 시간이 걸릴 수 있습니다. 이 문제는 healthcheck
로 해결합니다.
2.6. networks
🌉
특정 네트워크에 서비스를 연결합니다. 컨테이너 간의 통신 격리 및 관리에 유용합니다.
services:
webapp:
build: .
networks:
- frontend_network # 외부에서 접근 가능한 네트워크
- backend_network # 내부 서비스 간 통신용 네트워크
database:
image: postgres:14-alpine
networks:
- backend_network # 데이터베이스는 백엔드 네트워크에만 연결
networks:
frontend_network:
driver: bridge
backend_network:
driver: bridge
💡 팁: 각 서비스를 필요한 네트워크에만 연결하여 보안을 강화하고 불필요한 통신을 방지할 수 있습니다. 같은 네트워크에 연결된 서비스는 서비스 이름을 사용하여 서로 통신할 수 있습니다 (예: database
서비스 내에서 webapp
서비스에 연결할 때 http://webapp:80
).
2.7. restart
🔄
컨테이너 종료 시 재시작 정책을 설정합니다.
no
: 재시작 안 함 (기본값)always
: 항상 재시작on-failure
: 비정상 종료 시에만 재시작unless-stopped
: 수동으로 중지되기 전까지 항상 재시작
services:
webapp:
build: .
restart: unless-stopped # 컨테이너가 수동으로 중지되지 않는 한 항상 재시작
database:
image: postgres:14-alpine
restart: always # 항상 재시작
💡 팁: 개발 환경에서는 no
또는 on-failure
를, 중요한 백엔드 서비스나 데이터베이스는 always
또는 unless-stopped
를 고려해볼 수 있습니다.
3. 개발자를 위한 베스트 프랙티스 (심화) ✨
이제 더 견고하고 효율적인 Docker Compose 환경을 구축하기 위한 심화 팁들을 알아봅시다.
3.1. healthcheck
활용하여 서비스 준비 상태 확인 🩺
depends_on
은 시작 순서만 보장하므로, 실제 서비스가 요청을 처리할 준비가 되었는지 확인하기 위해 healthcheck
를 사용합니다.
services:
webapp:
build: .
ports:
- "80:80"
depends_on:
database:
condition: service_healthy # database 서비스가 건강해질 때까지 기다림
database:
image: postgres:14-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"] # DB 연결 테스트
interval: 5s # 5초마다 체크
timeout: 5s # 체크당 최대 5초 대기
retries: 5 # 5회 실패 시 unhealthy
start_period: 10s # 시작 후 10초 동안은 healthcheck 실패 무시
🚀 중요: 이 healthcheck
덕분에 webapp
은 database
컨테이너가 단순히 시작하는 것을 넘어, PostgreSQL 서버가 완전히 구동되어 쿼리를 받을 준비가 될 때까지 기다리게 됩니다. 이는 “컨테이너가 시작되었지만 서비스는 아직 준비되지 않은” 상황에서 발생하는 오류를 방지하는 가장 좋은 방법입니다.
3.2. 다양한 환경 관리: profiles
와 extends
🌍
개발, 테스트, 운영 등 여러 환경에 따라 다른 설정을 사용해야 할 때 유용합니다.
profiles
: 특정 프로파일이 활성화될 때만 해당 서비스가 실행되도록 합니다.# docker-compose.yml services: webapp: build: . ports: - "80:80" profiles: ["dev", "prod"] # dev 또는 prod 프로파일에서 실행 tests: build: . command: npm test profiles: ["test"] # test 프로파일에서만 실행 admin_panel: build: . ports: - "8080:80" profiles: ["dev"] # 개발 환경에서만 실행
실행:
docker-compose --profile dev up -d
또는docker-compose --profile test up --build
-
extends
: 공통 설정을 가진 베이스 파일을 만들고, 다른 파일에서 이를 확장하여 재사용성을 높입니다.# base.yaml (공통 설정) services: base_service: image: my_app_base environment: APP_ENV: default networks: - app_network networks: app_network: driver: bridge # docker-compose.yml (개발 환경) services: webapp: extends: file: base.yaml # base.yaml의 base_service를 확장 service: base_service environment: APP_ENV: development # APP_ENV를 development로 오버라이드 volumes: - ./src:/app/src # docker-compose.prod.yml (운영 환경) services: webapp: extends: file: base.yaml service: base_service environment: APP_ENV: production # 운영 환경에 맞는 추가 설정 (예: 리소스 제한, 로그 드라이버)
실행:
docker-compose -f docker-compose.yml up
(개발) 또는docker-compose -f docker-compose.prod.yml up
(운영)
3.3. 리소스 제한 설정 (deploy
-> resources
) 📈
컨테이너가 사용할 수 있는 CPU, 메모리 등의 리소스를 제한하여 안정성을 높입니다. 특히 개발 환경에서 특정 서비스가 너무 많은 리소스를 차지하는 것을 방지할 수 있습니다.
services:
webapp:
build: .
deploy:
resources:
limits: # 최대 사용량
cpus: '0.5' # 0.5 코어
memory: 512M # 512MB
reservations: # 보장되는 최소 사용량
cpus: '0.25'
memory: 256M
💡 팁: 이는 Docker Swarm 모드에서 주로 사용되지만, Compose CLI로도 단일 호스트에서 적용 가능합니다. 개발 환경에서는 리소스 부족으로 인한 호스트 불안정을 막을 수 있고, 테스트 환경에서는 실제 운영 환경과 유사한 부하 테스트를 가능하게 합니다.
3.4. latest
태그 지양하기 🚫
image: myapp:latest
와 같이 latest
태그를 사용하는 것은 피해야 합니다. latest
태그는 언제든 업데이트될 수 있어, 예상치 못한 변경이나 버그를 유발할 수 있습니다.
항상 특정 버전을 명시하는 것이 좋습니다: image: postgres:14.5-alpine
.
3.5. .dockerignore
파일 활용 🧹
build
지시어를 사용할 때 .dockerignore
파일을 Dockerfile
과 같은 디렉토리에 두면, 이미지 빌드 시 불필요한 파일(예: .git
, node_modules
, logs
)이 컨텍스트에 포함되는 것을 방지하여 빌드 속도를 높이고 이미지 크기를 줄일 수 있습니다.
# .dockerignore 예시
.git
.vscode
node_modules
npm-debug.log
dist/
tmp/
*.log
3.6. 컨테이너 간 통신 시 서비스 이름 사용 🗣️
Docker Compose는 기본적으로 서비스 이름을 DNS 호스트 이름으로 등록해줍니다. 따라서 같은 네트워크에 있는 컨테이너들은 localhost
대신 서비스 이름을 사용하여 서로 통신할 수 있습니다.
# docker-compose.yml
services:
frontend:
build: ./frontend
environment:
API_URL: http://backend:3000 # 'backend' 서비스 이름으로 접근
backend:
build: ./backend
ports:
- "3000:3000"
💡 팁: 이는 개발 환경에서 매우 편리하며, 나중에 배포 환경으로 전환할 때도 서비스 디스커버리 문제를 줄여줍니다.
4. 흔히 하는 실수와 해결책 ⚠️
Docker Compose를 사용하면서 개발자들이 자주 겪는 문제점과 그 해결책입니다.
4.1. 하드코딩된 비밀번호/민감 정보 ❌
- 문제: 데이터베이스 비밀번호, API 키 등을
environment
섹션에 직접 작성하는 경우. - 해결책:
.env
파일을 사용하고,.gitignore
에 추가하여 버전 관리 시스템에 올라가지 않도록 합니다. 실제 배포 시에는 Docker Secrets나 Kubernetes Secrets 같은 보안 솔루션을 사용합니다.
4.2. depends_on
오해 ⏳
- 문제:
depends_on
이 컨테이너의 서비스 준비 완료를 보장한다고 착각하는 경우. - 해결책: 위에서 설명한
healthcheck
를 함께 사용하여 서비스의 실제 준비 상태를 확인하고,depends_on
에condition: service_healthy
를 추가합니다.
4.3. 포트 충돌 🚪💥
- 문제: 여러 서비스가 동일한 호스트 포트를 사용하려고 하거나, 호스트의 이미 사용 중인 포트를 사용하려 할 때.
- 해결책:
ports
매핑에서 각 서비스에 고유한 호스트 포트를 할당하거나, 이미 사용 중인 포트가 없는지 확인합니다.docker-compose ps
명령으로 현재 실행 중인 포트 매핑을 확인할 수 있습니다.
4.4. 볼륨 마운트 경로 오류 🗺️
- 문제: 호스트 또는 컨테이너의 볼륨 경로를 잘못 지정하여 파일이 동기화되지 않거나 데이터가 저장되지 않는 경우.
- 해결책: 절대 경로를 사용하거나,
./
를 사용하여 현재docker-compose.yml
파일 기준의 상대 경로를 명확히 지정합니다. 컨테이너 내부의 경로도 정확한지 확인합니다 (예: Nginx는/etc/nginx/conf.d
, PostgreSQL은/var/lib/postgresql/data
).
4.5. 로그 확인 소홀 🧐
- 문제: 컨테이너가 제대로 실행되지 않는데 로그를 확인하지 않아 문제 원인을 찾기 어려운 경우.
- 해결책:
docker-compose logs [서비스_이름]
명령을 사용하여 특정 서비스의 로그를 확인하고,-f
옵션으로 실시간 로그를 추적합니다. 문제 발생 시 가장 먼저 확인해야 할 부분입니다.
5. 마무리하며: 개발 워크플로우의 혁신 🌟
Docker Compose는 개발자가 다중 컨테이너 애플리케이션을 쉽고 효율적으로 관리할 수 있도록 돕는 강력한 도구입니다. 위에 제시된 팁과 베스트 프랙티스들을 적용하면, 다음과 같은 이점을 누릴 수 있습니다.
- 일관된 개발 환경: 팀원 간의 환경 차이로 인한 “내 컴퓨터에서는 되는데…” 문제를 해결합니다.
- 빠른 온보딩: 새로운 팀원이 프로젝트에 합류했을 때, 단일 명령어로 개발 환경을 즉시 구축할 수 있습니다.
- 효율적인 테스트: 여러 서비스를 쉽게 올리고 내리면서 통합 테스트를 수행할 수 있습니다.
- 시간 절약: 반복적인 수동 설정을 자동화하여 개발 시간을 절약합니다.
이제 여러분은 Docker Compose YAML 파일을 더욱 견고하고 유연하게 작성할 수 있는 지식을 갖추었습니다. 오늘부터 Docker Compose를 적극적으로 활용하여 여러분의 개발 워크플로우를 한 단계 업그레이드해보세요! 궁금한 점이 있다면 언제든지 검색하고 실험해보는 것을 두려워하지 마세요. Happy Dockering! 🚀