이전 글에서 이미지를 빌드해 컨테이너 하나를 띄웠다. 그런데 실제로 쓸 만한 시스템이 되려면 두 가지가 더 필요하다. 첫째, 컨테이너가 사라져도 데이터를 잃지 않아야 한다. 둘째, 컨테이너가 바깥세상 및 다른 컨테이너와 통신할 수 있어야 한다. 이번 글은 이 둘, 즉 volume(데이터)과 network/port(통신), 그리고 여러 컨테이너를 한 번에 묶는 docker-compose를 다룬다.
문제: 컨테이너는 사라지면 데이터도 사라진다
컨테이너는 일시적(ephemeral) 이다. 컨테이너 안에서 만든 파일은 그 컨테이너의 수명에만 존재한다. 컨테이너를 지우고 새로 띄우면(docker run을 다시 하면), 안에 쌓였던 데이터는 모두 사라진다.
이것은 버그가 아니라 의도된 설계다. 컨테이너는 “이미지로부터 매번 깨끗하게 새로 시작하는” 단위이기 때문에 재현성을 갖는다. 하지만 데이터베이스의 내용, 학습된 모델 가중치, 업로드된 파일처럼 컨테이너보다 오래 살아남아야 하는 데이터는 컨테이너 밖에 따로 보관해야 한다. 그 장치가 volume이다.
Volume: 컨테이너 바깥에 데이터를 영속화
Volume은 컨테이너의 특정 디렉터리를 호스트(또는 Docker가 관리하는 별도 저장 공간) 에 연결해, 컨테이너가 사라져도 데이터가 남도록 하는 장치다. 컨테이너 안에서 그 디렉터리에 쓴 내용이 실제로는 컨테이너 바깥에 저장되므로, 컨테이너를 지웠다 새로 띄워도 데이터가 그대로 유지된다.
크게 두 가지 방식이 있다.
-
named volume: Docker가 관리하는 저장 공간에 이름을 붙여 사용. 데이터베이스처럼 영속 데이터에 적합.
docker run -v mydata:/var/lib/postgresql/data postgres:16컨테이너 안의
/var/lib/postgresql/data(DB가 데이터를 쓰는 곳)를mydata라는 볼륨에 연결한다. 컨테이너를 지워도mydata는 남는다. -
bind mount: 호스트의 특정 폴더를 컨테이너 안에 직접 연결. 내 PC의 소스 코드나 데이터 폴더를 컨테이너에서 바로 쓰고 싶을 때 유용.
docker run -v /home/me/data:/app/data myapp:1.0내 PC의
/home/me/data를 컨테이너의/app/data로 연결한다. 호스트에서 파일을 바꾸면 컨테이너 안에서도 즉시 반영된다.
핵심 구분: 이미지 = 변하지 않는 코드·환경, volume = 컨테이너보다 오래 사는 데이터. 이 둘을 분리하는 것이 컨테이너 운영의 기본 원칙이다.
Port: 컨테이너를 바깥세상과 잇기
컨테이너는 기본적으로 격리되어 있어, 안에서 웹 서버가 돌고 있어도 바깥에서는 접근할 수 없다. 바깥(호스트)과 컨테이너를 잇는 통로가 port 매핑이다. 앞 글에서 본 -p 옵션이 그것이다.
docker run -p 8080:5000 myapp:1.0-p 호스트포트:컨테이너포트 형식이다. 위는 호스트의 8080 포트로 들어온 요청을 컨테이너의 5000 포트로 전달한다는 뜻이다.
브라우저 ──▶ localhost:8080 ──[port 매핑]──▶ 컨테이너:5000 (Flask 앱)
그래서 앱은 컨테이너 안에서 5000번을 듣고 있지만, 사용자는 호스트의 8080번으로 접속한다. 호스트 포트만 바꾸면 같은 이미지를 포트만 달리해 여러 개 띄울 수도 있다.
Network: 컨테이너끼리 통신하기
실제 애플리케이션은 보통 컨테이너 하나로 끝나지 않는다. 예를 들어 웹 앱 컨테이너와 데이터베이스 컨테이너가 함께 돌고, 웹 앱이 DB에 접속해야 한다. 이때 쓰는 것이 Docker network다.
같은 네트워크에 속한 컨테이너들은 서로를 컨테이너 이름으로 찾아 통신할 수 있다. Docker가 컨테이너 이름을 내부 주소로 자동 변환(DNS)해 주기 때문이다.
docker network create mynet # 네트워크 생성
docker run -d --name db --network mynet postgres:16
docker run -d --name web --network mynet myapp:1.0이렇게 하면 web 컨테이너는 DB 주소를 IP가 아니라 그냥 db라는 이름으로 접속할 수 있다 (예: 접속 호스트를 db:5432로 지정). IP는 컨테이너를 다시 띄울 때마다 바뀌지만 이름은 그대로이므로, 이름 기반 통신이 훨씬 안정적이다.
docker-compose: 여러 컨테이너를 한 파일로
위처럼 컨테이너가 둘 이상이 되면, docker run 명령을 여러 번 일일이 치는 일이 번거롭고 실수도 잦다. 볼륨·네트워크·포트·환경 변수까지 매번 옵션으로 넘겨야 한다. 이 모든 구성을 하나의 설정 파일로 선언하고 명령 한 번으로 띄우는 도구가 docker-compose다.
docker-compose.yml 파일에 어떤 컨테이너들을, 어떤 설정으로, 어떻게 연결해 띄울지를 적는다. 앞의 웹+DB 예시를 Compose로 옮기면 이렇게 된다.
services:
web:
build: . # 현재 폴더의 Dockerfile로 이미지 빌드
ports:
- "8080:5000" # 호스트 8080 → 컨테이너 5000
depends_on:
- db # db가 먼저 뜨도록 의존성 명시
db:
image: postgres:16 # 레지스트리의 기존 이미지 사용
environment:
POSTGRES_PASSWORD: secret
volumes:
- mydata:/var/lib/postgresql/data # 데이터 영속화
volumes:
mydata: # named volume 선언이제 명령 하나면 두 컨테이너가 네트워크로 연결된 채 함께 뜬다. (Compose는 자동으로 전용 네트워크를 만들어 서비스 이름 web·db로 서로를 찾게 해 준다.)
docker compose up # 전체 스택 시작
docker compose down # 전체 스택 종료·정리docker run 명령을 손으로 나열하던 명령형(imperative) 방식이, 원하는 최종 상태를 파일에 적어 두는 선언형(declarative) 방식으로 바뀐 점에 주목하자. “어떻게 띄울지”가 아니라 “어떤 상태이길 원하는지”를 기술하는 이 사고방식은, 다음 시리즈에서 다룰 Kubernetes의 핵심 철학과 정확히 같다. 컴퓨터 한 대에서 Compose가 하는 일을, 여러 서버에 걸쳐 더 큰 규모로 하는 것이 바로 Kubernetes다.
요약
- 컨테이너는 일시적이라, 지우면 내부 데이터가 사라진다. 오래 살아남아야 할 데이터는 volume으로 컨테이너 바깥에 영속화한다 (named volume / bind mount).
- port 매핑(
-p 호스트:컨테이너)으로 컨테이너를 바깥세상과 잇고, network로 컨테이너끼리 이름 기반으로 통신한다. - docker-compose는 여러 컨테이너의 구성(이미지·포트·볼륨·네트워크)을 한 YAML 파일에 선언하고 명령 하나로 띄운다.
- 이 “원하는 상태를 선언한다”는 방식은 다음에 다룰 Kubernetes의 핵심 사고방식과 같다 — Compose가 한 대에서 하는 일을 여러 서버로 확장한 것이 Kubernetes다.