Docker Compose란 무엇인가? 다중 컨테이너 관리하기
Docker를 사용하다 보면 컨테이너 하나만으로는 부족한 상황이 금방 옵니다. 웹 애플리케이션을 돌리려면 앱 서버 컨테이너가 필요하고, 데이터를 저장하려면 데이터베이스 컨테이너가 필요하고, 캐시를 쓰려면 Redis 컨테이너도 필요합니다. 이걸 하나씩 docker run 명령어로 실행하면 매번 옵션을 기억해야 하고, 컨테이너 간의 네트워크도 수동으로 연결해야 합니다. 컨테이너가 세 개만 돼도 관리가 번거로워집니다. Docker Compose는 이런 다중 컨테이너 환경을 하나의 파일로 정의하고 한 번에 실행할 수 있게 해주는 도구입니다. 이번 글에서는 Docker Compose가 무엇인지, 어떻게 사용하는지, 실제 프로젝트에서 어떤 식으로 구성하는지를 정리해보겠습니다.
Docker Compose가 필요한 이유
Docker로 컨테이너를 하나씩 실행하는 명령어를 생각해봅시다. Node.js 앱 컨테이너를 실행하려면 이런 명령어를 칩니다.
docker run -d --name app -p 3000:3000 --env-file .env my-app
MySQL 컨테이너도 필요합니다.
docker run -d --name db -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret -v db-data:/var/lib/mysql mysql:8
Redis 캐시도 추가합니다.
docker run -d --name cache -p 6379:6379 redis:7
그리고 이 세 컨테이너가 서로 통신할 수 있게 네트워크를 만들어야 합니다.
docker network create my-network
docker network connect my-network app
docker network connect my-network db
docker network connect my-network cache
컨테이너가 세 개밖에 안 되는데도 명령어가 이렇게 많습니다. 서버를 다시 세팅하거나 다른 개발자가 같은 환경을 구성하려면 이 명령어들을 전부 기억하고 순서대로 실행해야 합니다. 실수 한 번이면 컨테이너가 서로 연결이 안 되거나 데이터가 날아갈 수 있습니다.
Docker Compose를 사용하면 이 모든 설정을 docker-compose.yml 파일 하나에 적어두고, 명령어 한 줄로 전부 실행할 수 있습니다.
docker-compose.yml 기본 구조
Docker Compose의 설정 파일은 YAML 형식입니다. 위에서 명령어로 했던 것과 동일한 구성을 파일로 작성하면 이렇습니다.
yaml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- db
- cache
db:
image: mysql:8
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: myapp
volumes:
- db-data:/var/lib/mysql
cache:
image: redis:7
ports:
- "6379:6379"
volumes:
db-data:
```
파일 구조를 하나씩 살펴보겠습니다.
version은 Docker Compose 파일의 형식 버전입니다. 3.8이 현재 많이 사용되는 버전입니다. 최신 Docker Compose에서는 이 항목을 생략해도 되지만 명시적으로 적어두는 게 호환성 면에서 안전합니다.
services 아래에 실행할 컨테이너들을 정의합니다. app, db, cache라는 이름으로 세 개의 서비스를 만들었습니다. 이 이름은 자유롭게 정할 수 있고, 같은 Compose 안에서 컨테이너끼리 통신할 때 이 이름을 호스트명으로 사용합니다.
app 서비스의 build: .은 현재 디렉토리의 Dockerfile을 사용해서 이미지를 빌드하라는 뜻입니다. 직접 만든 앱은 build를 사용하고, MySQL이나 Redis처럼 공식 이미지를 사용하는 서비스는 image를 사용합니다.
ports는 호스트와 컨테이너의 포트를 연결합니다. "3000:3000"은 호스트의 3000번 포트를 컨테이너의 3000번 포트에 매핑합니다.
depends_on은 서비스 간의 실행 순서를 지정합니다. app이 db와 cache에 의존한다고 선언하면 db와 cache가 먼저 시작된 후에 app이 시작됩니다. 다만 depends_on은 컨테이너가 시작되는 순서만 보장하지, 서비스가 실제로 준비될 때까지 기다려주지는 않습니다. MySQL 컨테이너가 시작됐더라도 데이터베이스 초기화가 끝나기 전에 앱이 접속을 시도할 수 있습니다. 이 문제는 뒤에서 다시 다루겠습니다.
volumes는 데이터를 영속적으로 저장하기 위한 설정입니다. 컨테이너는 삭제하면 안에 있던 데이터도 사라지는데, 볼륨을 연결해두면 컨테이너를 삭제하고 다시 만들어도 데이터가 유지됩니다. db-data라는 이름의 볼륨을 만들어서 MySQL 데이터 디렉토리에 연결한 겁니다.
기본 명령어
docker-compose.yml 파일이 있는 디렉토리에서 다음 명령어들을 사용합니다.
모든 서비스를 시작하려면 이렇게 합니다.
```
docker compose up -d
```
-d 옵션은 백그라운드에서 실행한다는 뜻입니다. 이 명령어 하나로 이미지 빌드, 네트워크 생성, 볼륨 생성, 컨테이너 실행이 전부 처리됩니다.
실행 중인 서비스 상태를 확인하려면 이렇게 합니다.
```
docker compose ps
```
각 컨테이너의 이름, 상태, 포트 매핑 정보가 나옵니다.
모든 서비스를 중지하려면 이렇게 합니다.
```
docker compose down
```
컨테이너와 네트워크가 삭제됩니다. 볼륨은 기본적으로 삭제되지 않습니다. 볼륨까지 삭제하고 싶으면 -v 옵션을 추가합니다. 데이터베이스 데이터를 완전히 초기화하고 싶을 때만 사용해야 합니다.
특정 서비스의 로그를 확인하려면 이렇게 합니다.
```
docker compose logs app
docker compose logs -f db
```
-f 옵션을 붙이면 실시간으로 로그가 출력됩니다. 서버 모니터링 글에서 다뤘던 것처럼 로그를 확인하는 건 문제 진단의 기본입니다.
이미지를 다시 빌드하면서 시작하려면 이렇게 합니다.
```
docker compose up -d --build
```
코드를 수정한 후에 변경 사항을 반영하려면 --build 옵션을 추가해야 합니다. 이 옵션이 없으면 이전에 빌드한 이미지를 그대로 사용합니다.
이전 버전에서는 docker-compose라고 하이픈을 붙여서 사용했는데, 최신 Docker에서는 docker compose로 공백을 사용합니다. 둘 다 동작하지만 새 프로젝트에서는 docker compose를 사용하는 게 좋습니다.
컨테이너 간 네트워크 통신
Docker Compose로 실행한 서비스들은 같은 네트워크에 자동으로 연결됩니다. 별도로 네트워크를 만들거나 연결할 필요가 없습니다.
중요한 건 컨테이너끼리 통신할 때 서비스 이름을 호스트명으로 사용한다는 점입니다. 앱에서 데이터베이스에 접속할 때 localhost:3306이 아니라 db:3306으로 접속합니다. 마찬가지로 Redis에 접속할 때는 cache:6379를 사용합니다.
앱의 환경 변수 파일에서 이런 식으로 설정합니다.
```
DB_HOST=db
DB_PORT=3306
REDIS_HOST=cache
REDIS_PORT=6379
docker-compose.yml에서 정의한 서비스 이름 그대로 호스트명으로 쓰면 됩니다. Docker의 내부 DNS가 서비스 이름을 해당 컨테이너의 IP로 자동 변환해줍니다.
환경 변수 관리
환경 변수를 설정하는 방법이 여러 가지 있습니다.
docker-compose.yml 안에서 직접 지정하는 방법입니다.
yaml
services:
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: myapp
간단하지만 비밀번호 같은 민감한 정보가 파일에 그대로 노출됩니다. 개인 프로젝트에서는 괜찮지만 팀 프로젝트에서는 좋지 않습니다.
.env 파일을 사용하는 방법이 더 안전합니다.
yaml
services:
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
```
프로젝트 루트에 .env 파일을 만들어서 값을 넣어둡니다.
```
DB_PASSWORD=secret
DB_NAME=myapp
docker-compose.yml에서 ${변수명} 형식으로 참조하면 .env 파일의 값이 들어갑니다. .env 파일은 .gitignore에 추가해서 저장소에 올라가지 않게 해야 합니다. 이전 글에서 다뤘던 환경 변수 관리의 원칙과 같습니다.
서비스별로 다른 env 파일을 사용할 수도 있습니다.
yaml
services:
app:
build: .
env_file:
- ./app.env
app.env 파일에 앱에서 사용하는 환경 변수를 넣어두면 해당 컨테이너에만 적용됩니다.
depends_on과 헬스체크
아까 depends_on이 컨테이너 시작 순서만 보장하고 서비스 준비 상태는 보장하지 않는다고 했습니다. MySQL 컨테이너가 시작됐어도 초기화가 끝나기 전에 앱이 접속을 시도하면 에러가 납니다.
이 문제를 해결하려면 헬스체크를 설정합니다.
yaml
services:
app:
build: .
depends_on:
db:
condition: service_healthy
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
db 서비스에 healthcheck를 추가했습니다. 10초 간격으로 mysqladmin ping 명령어를 실행해서 MySQL이 응답하는지 확인합니다. 5번 재시도해서 응답이 오면 healthy 상태가 됩니다.
app 서비스의 depends_on에 condition: service_healthy를 추가하면 db가 healthy 상태가 된 후에만 app이 시작됩니다. 이렇게 하면 데이터베이스가 실제로 준비된 후에 앱이 뜨기 때문에 연결 에러를 방지할 수 있습니다.
볼륨 활용하기
볼륨은 데이터를 영속적으로 보관하는 것 외에도 개발 환경에서 유용하게 쓸 수 있습니다.
개발할 때 코드를 수정하면 컨테이너를 다시 빌드해야 변경 사항이 반영됩니다. 매번 빌드하는 건 시간이 걸리기 때문에 로컬 소스 코드 디렉토리를 컨테이너에 직접 마운트하면 코드 수정이 즉시 반영됩니다.
yaml
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
첫 번째 볼륨 .:/app은 현재 디렉토리를 컨테이너의 /app 디렉토리에 마운트합니다. 로컬에서 파일을 수정하면 컨테이너 안에서도 바로 반영됩니다.
두 번째 볼륨 /app/node_modules는 컨테이너의 node_modules를 로컬에서 덮어쓰지 않게 보호하는 설정입니다. 로컬의 node_modules와 컨테이너의 node_modules가 충돌하는 걸 방지합니다.
이런 볼륨 마운트는 개발 환경에서만 사용하고, 운영 환경에서는 이미지 안에 코드를 포함시켜서 배포하는 게 일반적입니다.
개발용과 운영용 설정 분리
개발 환경과 운영 환경의 설정이 다를 수 있습니다. 개발 환경에서는 소스 코드를 마운트하고 디버그 모드를 켜지만, 운영 환경에서는 이미지에 코드를 넣고 최적화된 설정으로 돌려야 합니다.
Docker Compose는 여러 파일을 조합해서 사용할 수 있습니다. 기본 설정을 docker-compose.yml에 넣고, 개발용 추가 설정을 docker-compose.dev.yml에 넣는 식입니다.
yaml
# docker-compose.dev.yml
services:
app:
volumes:
- .:/app
environment:
NODE_ENV: development
```
개발 환경에서 실행할 때는 두 파일을 조합합니다.
```
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
-f 옵션으로 파일을 여러 개 지정하면 뒤의 파일이 앞의 파일을 덮어씌웁니다. 기본 설정에 개발용 설정이 추가되는 구조입니다.
자주 하는 실수와 주의사항
포트 충돌이 가장 흔한 문제입니다. 로컬에서 MySQL이 이미 3306 포트를 사용하고 있는데 Docker Compose에서도 3306으로 매핑하면 충돌이 납니다. 이럴 때는 호스트 포트를 바꾸면 됩니다. “3307:3306″으로 설정하면 로컬에서는 3307로 접속하고 컨테이너 내부에서는 3306을 사용합니다.
볼륨 데이터가 남아있어서 혼란이 생기는 경우도 있습니다. 데이터베이스 설정을 변경했는데 이전 데이터가 남아있으면 변경 사항이 적용되지 않을 수 있습니다. 깨끗하게 시작하고 싶으면 docker compose down -v로 볼륨까지 삭제한 뒤 다시 시작합니다.
이미지를 다시 빌드하지 않아서 코드 변경이 안 반영되는 경우도 자주 있습니다. docker compose up -d만 실행하면 기존 이미지를 재사용하기 때문에 코드 변경이 반영되지 않습니다. 코드를 수정했으면 –build 옵션을 붙이는 걸 잊지 말아야 합니다.
마무리
Docker Compose는 여러 컨테이너로 구성된 환경을 하나의 파일로 관리할 수 있게 해주는 도구입니다. docker-compose.yml에 서비스, 네트워크, 볼륨을 정의하고 docker compose up 한 줄로 전체 환경을 띄울 수 있습니다. 개발 환경을 구성할 때 특히 유용한데, 새로운 팀원이 들어와도 파일 하나만 공유하면 동일한 환경을 바로 만들 수 있습니다. 처음에는 앱 서버와 데이터베이스 두 개로 시작해보고, 익숙해지면 Redis나 Nginx 같은 서비스를 하나씩 추가해나가면 됩니다. 다음 글에서는 서버 운영에서 중요한 환경 변수의 개념과 관리 방법을 더 자세히 다뤄보겠습니다.
Leave a Reply