[GDSC] Docker Compose! 다중 컨테이너 오케스트레이션
이전 글에서 Node.js 백엔드, React 프론트엔드, MongoDB 데이터베이스를 각각 독립된 컨테이너로 띄워서 하나의 애플리케이션처럼 동작하게 만들었다. 백엔드 하나, 프론트엔드 하나, 데이터베이스 하나. 겉으로 보면 단 세 개지만, 이걸 전부 도커 명령어로 관리하려고 하면 생각보다 꽤 골치 아프다. 이번 글에서는 그 골칫덩어리를 해결해줄 수 있는 도구, Docker Compose에 대한 이야기를 해보겠다!
결론부터 말하면, 지금까지 터미널에 입력하던 docker run/docker build들을 하나의 설정 파일로 옮겨놓고, docker-compose up 한 줄로 전체 환경을 올렸다 내렸다 할 수 있게 만드는 도구다.
도커 명령어를 쳐야 됐던 시절...
직접 명령어를 쳐서 프로젝트를 올리던 흐름을 다시 떠올려 보자.
- 먼저 네트워크를 하나 만든다.
- MongoDB 컨테이너를
docker run으로 띄우는데, 환경 변수도 넣고, 볼륨도 붙이고, 방금 만든 네트워크에도 연결해야 한다. 명령어가 꽤 길어진다. - Node API 이미지를
docker build로 한 번 빌드한다. - 그 이미지를 기반으로 백엔드 컨테이너를 띄운다. 포트도 열고, 환경 변수도 넣고, 볼륨도 세 개나 붙이고, 네트워크도 연결한다.
- React SPA도 이미지 빌드 후 컨테이너를 띄우는데, 이번에는 바인드 마운트, 포트, 인터랙티브 모드 옵션까지 챙겨야 한다.
어플리케이션을 끌 때도 할 일이 많다. 컨테이너들을 전부 멈추고 지우고, 네트워크와 명명된 볼륨까지 정리해 줘야 한다.
이게 세 개짜리 서비스일 때 이야기다. 실제 서비스에서 컨테이너가 다섯 개, 여섯 개, 열 개씩 넘어가면...? 명령어를 입력하는 것 자체가 일이 된다. 타이핑도 문제지만, 사람은 실수한다. 플래그 하나 빼먹고, 환경 변수 이름 하나 틀리면 한참을 로그만 들여다보게 된다.
도커로 못 할 일은 아니다. 다 된다. 하지만 '할 수 있다'와 '매번 이렇게 하고 싶다'는 전혀 다른 문제지!
Docker Compose
Docker Compose의 정의는...
여러 개의
docker build와docker run을 하나의 설정 파일로 모아서, 한 번에 오케스트레이션하는 도구
조금 더 풀어 보면...
- 여러 컨테이너(서비스)를 구성하는 모든 옵션을 텍스트 파일 하나에 정의해 둔다.
- 그 파일을 기준으로
docker-compose up을 실행하면, 필요한 이미지를 알아서 빌드하고, 모든 컨테이너를 한 번에 올린다. docker-compose down한 줄로 관련 컨테이너와 네트워크를 한 번에 정리할 수 있다.
그러니까 터미널에서 치던 긴 docker run … 묶음을 '사람이 기억하는 방식'에서 '파일에 적어 두고 재현하는 방식'으로 바꿔 주는 도구라고 보면 된다.
중요한 건, Docker Compose는 별 다른 게 아니라 기존 도커 명령들을 자동으로 대신 실행해 주는 레벨이라는 점이다. 내부적으로는 여전히 docker build, docker run이 돌고 있다.
Docker Compose가 아닌 것들
Compose를 처음 쓸 때 헷갈리는 지점들이 있는데...
첫째, Docker Compose는 Dockerfile을 대체하지 않는다.
커스텀 이미지를 만들 때는 여전히 Dockerfile이 필요하고, Compose는 그 Dockerfile을 읽어서 이미지를 빌드해 줄 뿐이다. Compose 파일의 build 옵션이 바로 그 역할이다.
둘째, 이미지나 컨테이너 자체를 바꾸는 도구가 아니다. 이미지, 컨테이너, 볼륨, 네트워크라는 도커의 기본 개념은 그대로 쓰되, '이걸 어떻게 묶어서 띄울지'를 정의하는 역할을 한다.
셋째, 여러 대의 서버에 걸친 대규모 오케스트레이션 도구가 아니다. Compose는 기본적으로 '한 호스트(한 머신) 안에서 여러 컨테이너를 관리'하는 데 최적화되어 있다. 멀티 호스트, 클러스터링 수준은 Kubernetes 같은 다른 도구의 영역이다.
 
docker-compose.yml
Compose는 docker-compose.yml 파일에서 시작된다.
터미널에 명령을 치는 대신, 이 파일에 '우리가 하고 싶었던 설정'들을 적어 두는 거라고 보면 된다.
파일의 가장 바깥에는 크게 두 가지가 꼭 들어간다.
하나는 version이다.
Docker Compose 스펙 버전을 의미한다.
version: "3.8"
이건 '이 파일은 Compose 3.8 버전 스펙에 맞춰 썼으니, 그 기준으로 해석해 달라'는 선언이다. 스펙이 시간이 지나면서 조금씩 바뀌기 때문에, 버전을 명시해 두면 도커가 어떤 기능을 쓸 수 있는지 정확히 이해하게 된다.
두 번째는 services이다.
실제로 실행할 컨테이너들을 이 아래에 전부 정의한다. 예를 들어, Node 백엔드, React 프론트엔드, MongoDB를 각각 이렇게 쓸 수 있다.
services:
mongodb:
...
backend:
...
frontend:
...
여기서 mongodb, backend, frontend는 서비스 이름이자, 코드 안에서 호스트 이름처럼 사용할 수 있는 식별자다. 나중에 Node에서 MongoDB에 접속할 때 mongodb라는 이름으로 접근하는 게 바로 이 서비스 이름 덕분이다.
서비스 하나를 예로 들어보자
MongoDB 컨테이너를 docker run으로 띄웠을 때를 떠올려 보자.
이미지 이름은 mongo였고, 환경 변수 두 개를 넘겼고, 데이터 보존을 위해 명명된 볼륨을 붙였고, 네트워크에 연결해서 실행했다.
Compose로 다시 쓰면 대략 이런 모습이 된다.
services:
mongodb:
image: "mongo"
volumes:
- data:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: max
MONGO_INITDB_ROOT_PASSWORD: secret
여기서 눈여겨볼 부분은 volumes다.
data:/data/db라는 한 줄이 “호스트의 data라는 명명된 볼륨을 컨테이너 안의 /data/db에 붙인다"는 의미다. 기존 docker run -v data:/data/db 플래그와 동일하다.
명명된 볼륨은 파일 맨 아래에 따로 선언해 줘야 한다.
volumes:
data:
값은 비워 둔다. '이 이름의 볼륨을 이 프로젝트에서 사용하겠다'라는 걸 도커에게 알려 주는 셈이다.
네트워크는 따로 지정하지 않아도 된다.
Compose는 docker-compose up을 실행하면 자동으로 하나의 기본 네트워크를 만들고, 같은 파일 안에 정의된 모든 서비스를 그 네트워크에 붙인다. 그래서 직접 네트워크를 만들고 컨테이너마다 --network 옵션을 붙이지 않아도 된다.
백엔드는 커스텀 이미지가 필요하다
백엔드 컨테이너는 공식 이미지를 쓰는 게 아니라, 만든 Dockerfile을 기반으로 이미지를 빌드해야 한다. docker build -t goals-node ./backend를 치던 것을 Compose에서는 이렇게 옮긴다.
services:
backend:
build: ./backend
build에 Dockerfile이 들어 있는 폴더 경로를 적어 주면, Compose가 그 폴더에서 Dockerfile을 찾아 이미지를 빌드해 준다.
만약 Dockerfile 이름이 다르거나, 컨텍스트를 더 넓게 잡아야 한다면 context와 dockerfile을 세부적으로 나눠 쓸 수도 있다. 기본적인 상황에서는 저 짧은 형태로 충분하다.
여기에 포트, 볼륨, 환경 변수, 의존성을 차례대로 추가해 보면 이런 구성이 된다.
services:
backend:
build: ./backend
ports:
- "80:80"
volumes:
- logs:/app/logs
- ./backend:/app
- /app/node_modules
env_file:
- ./env/backend.env
depends_on:
- mongodb
하나씩 보면...
ports "80:80"은 호스트 80포트를 컨테이너의 80포트에 연결한다.logs:/app/logs는 명명된 볼륨,./backend:/app은 바인드 마운트,/app/node_modules는 익명 볼륨이다. 도커 명령어에서 쓰던-v옵션을 그대로 yml 문법으로 풀어 썼다고 보면 된다.env_file은 환경 변수를 따로.env파일에 빼고 싶을 때 쓰는 옵션이다.backend.env에MONGODB_USERNAME=max이런 식으로 적어 놓고, 여기서 그 파일을 지정하면 Compose가 알아서 읽어 컨테이너에 주입한다.depends_on은 “backend 컨테이너는 mongodb 서비스가 먼저 올라와 있어야 한다"는 의존 관계를 표현해 준다. 이건 Compose에만 있는 개념이라,docker run으로는 똑같이 표현하기가 꽤 번거롭다.
docker-compose up과 down
docker-compose up
이 한 줄이 하는 일을 풀어 쓰면 다음과 같다.
- 필요한 이미지를 전부 빌드하거나, 도커 허브에서 내려받는다.
services아래 정의된 컨테이너들을 순서대로 생성하고, 기본 네트워크에 연결한다.- 포트, 볼륨, 환경 변수, 의존 관계 등을 전부 적용한 상태로 실행한다.
docker-compose up -d
반대로 환경을 정리하고 싶다면?
docker-compose down
이 명령은 Compose가 관리하던 컨테이너들과 네트워크를 정리한다.
볼륨까지 지우고 싶을 때만 -v를 붙인다.
docker-compose down -v
볼륨을 지우는 옵션을 일부러 분리해 둔 건, 대부분의 경우 데이터 자체는 유지하고 싶어하기 때문이다.
이미지 리빌드를 강제하고 싶을 때는 --build 옵션을 붙여서 실행하면 된다.
docker-compose up --build
혹은 컨테이너는 띄우지 않고 이미지 빌드만 따로 하고 싶다면...
docker-compose build
정리
- Docker Compose는 여러 컨테이너로 구성된 애플리케이션을 '설정 파일 하나 +
up/down두 명령'으로 관리하게 해 주는 도구다. - Dockerfile, 이미지, 컨테이너라는 기본 개념을 대체하지 않고, 그 위에서 오케스트레이션을 담당한다.
- 긴
docker run명령을 외울 필요 없이,docker-compose.yml안에 모든 실행 옵션을 기록해 두고 버전 관리할 수 있다.