[도커 30강] 10강. 볼륨·바인드 마운트 차이와 데이터 영속성

[도커 30강] 10강. 볼륨·바인드 마운트 차이와 데이터 영속성

컨테이너를 처음 쓸 때 가장 자주 하는 오해가 있습니다. “컨테이너 안에 파일이 있으니까 당연히 남아 있겠지?” 그런데 컨테이너는 기본적으로 교체 가능한 실행 단위입니다. 컨테이너를 지우고 새로 만들면 내부 변경사항이 같이 사라질 수 있습니다. 그래서 실무에서는 코드, 로그, 데이터베이스 파일처럼 성격이 다른 데이터를 어디에 두고 어떻게 보존할지부터 설계해야 합니다.

오늘은 도커 스토리지의 핵심인 볼륨(volume)바인드 마운트(bind mount) 의 차이를 정확히 구분하고, 어떤 상황에서 무엇을 선택해야 안전한지, 그리고 데이터 영속성을 어떻게 검증해야 하는지까지 한 번에 정리해보겠습니다.


핵심 개념

  • 컨테이너 writable layer(쓰기 레이어)에 저장한 데이터는 컨테이너 생명주기에 강하게 묶여 있습니다. 컨테이너를 삭제하면 데이터도 함께 사라질 수 있습니다.
  • 볼륨(volume) 은 도커가 관리하는 저장소로, 컨테이너와 분리된 수명주기를 갖습니다. 운영 데이터(특히 DB)에는 기본적으로 볼륨을 우선 고려합니다.
  • 바인드 마운트(bind mount) 는 호스트의 특정 경로를 컨테이너에 직접 연결합니다. 개발 중 코드 동기화에는 매우 편하지만, 호스트 경로 의존성이 강합니다.
  • “영속성”은 단순히 파일이 남는 것만 뜻하지 않습니다. 재시작/재생성/업데이트 이후에도 일관되게 복구 가능한 상태를 유지하는 것까지 포함합니다.
  • 볼륨/마운트는 성능, 권한, 백업 전략, 팀 협업 방식과 연결됩니다. 문법보다 운영 기준이 더 중요합니다.

기본 사용

예제 1) 컨테이너 내부 파일은 왜 사라지는가

# 임시 컨테이너 실행 후 파일 생성
docker run --name tmpbox -d alpine sh -c "sleep 600"
docker exec tmpbox sh -c "echo 'hello' > /tmp/hello.txt && ls -l /tmp/hello.txt"

# 컨테이너 삭제 후 다시 생성
docker rm -f tmpbox
docker run --name tmpbox -d alpine sh -c "sleep 600"
docker exec tmpbox sh -c "ls -l /tmp/hello.txt || echo 'file not found'"

설명:

  • 첫 컨테이너에서 만든 /tmp/hello.txt는 해당 컨테이너의 쓰기 레이어에 있었습니다.
  • 컨테이너를 삭제하고 새로 생성하면 그 레이어도 교체되므로 파일이 없어지는 게 정상입니다.
  • “컨테이너 재생성”이 빈번한 환경(배포, CI, 오토스케일)에서는 영속 저장소가 필수입니다.

예제 2) named volume으로 데이터 영속성 확보

# 볼륨 생성
docker volume create lesson10_data

# 볼륨을 마운트한 컨테이너에서 데이터 기록
docker run --name vbox -d -v lesson10_data:/data alpine sh -c "echo 'persisted-data' > /data/value.txt && sleep 600"
docker exec vbox sh -c "cat /data/value.txt"

# 컨테이너 삭제 후 재생성
docker rm -f vbox
docker run --name vbox -d -v lesson10_data:/data alpine sh -c "sleep 600"
docker exec vbox sh -c "cat /data/value.txt"

설명:

  • 데이터가 컨테이너가 아니라 lesson10_data 볼륨에 저장되므로 컨테이너를 바꿔도 파일이 유지됩니다.
  • 운영에서 데이터 영속성이 필요하면 “컨테이너 교체가 일어나도 데이터가 살아남는가?”를 먼저 확인해야 합니다.
  • docker volume ls, docker volume inspect로 실제 저장 위치와 메타정보를 추적할 수 있습니다.

예제 3) 바인드 마운트로 로컬 코드 즉시 반영

mkdir -p ~/docker100/lesson10/app
cat > ~/docker100/lesson10/app/index.html <<'HTML'
<h1>Lesson 10 - bind mount</h1>
<p>version: v1</p>
HTML

docker run --name web10 -d -p 8088:80 \
  -v ~/docker100/lesson10/app:/usr/share/nginx/html:ro \
  nginx:alpine
# 브라우저에서 http://localhost:8088 확인 후, 파일 수정
cat > ~/docker100/lesson10/app/index.html <<'HTML'
<h1>Lesson 10 - bind mount</h1>
<p>version: v2 (edited on host)</p>
HTML

# 컨테이너 재빌드 없이 즉시 반영 확인
curl -s http://localhost:8088

설명:

  • 바인드 마운트는 호스트 파일이 곧 컨테이너 파일처럼 보이기 때문에 개발 루프가 매우 빠릅니다.
  • :ro(read-only)를 붙이면 컨테이너가 호스트 파일을 수정하지 못해 안전성이 올라갑니다.
  • 대신 경로가 로컬 환경에 강하게 묶이므로, 팀원마다 경로가 다르면 실행 스크립트 표준화가 필요합니다.

예제 4) 실제 DB 컨테이너에 볼륨 적용

# Postgres 데이터를 볼륨에 저장
docker volume create pg_l10_data

docker run --name pg10 -d \
  -e POSTGRES_PASSWORD=pass1234 \
  -e POSTGRES_DB=sampledb \
  -v pg_l10_data:/var/lib/postgresql/data \
  -p 5438:5432 postgres:16
# 간단한 데이터 생성
sleep 3
docker exec -i pg10 psql -U postgres -d sampledb <<'SQL'
create table if not exists notes(id serial primary key, body text);
insert into notes(body) values ('volume keeps me alive');
select * from notes;
SQL

# 컨테이너 재생성 후 데이터 확인
docker rm -f pg10
docker run --name pg10 -d \
  -e POSTGRES_PASSWORD=pass1234 \
  -e POSTGRES_DB=sampledb \
  -v pg_l10_data:/var/lib/postgresql/data \
  -p 5438:5432 postgres:16
sleep 3
docker exec -i pg10 psql -U postgres -d sampledb -c "select * from notes;"

설명:

  • DB 같은 상태 저장 서비스는 볼륨을 쓰지 않으면 테스트에서는 돌아가도 운영에서 장애로 이어질 수 있습니다.
  • 재생성 이후 데이터가 보이는지 직접 검증해두면 “영속성 확인”이 말이 아니라 체크리스트가 됩니다.
  • 이 단계에서 백업/복구(덤프/리스토어)까지 연결하면 실무 준비가 훨씬 탄탄해집니다.

자주 하는 실수

실수 1) 운영 DB를 바인드 마운트로 대충 연결

  • 원인: 로컬 개발에서 쓰던 패턴을 운영에도 그대로 가져옴.
  • 해결: 운영 DB는 우선 볼륨 기반으로 설계하고, 백업 경로/복구 절차까지 문서화합니다. 바인드 마운트는 운영 목적이 명확할 때만 제한적으로 사용합니다.

실수 2) 컨테이너를 지웠는데 데이터가 왜 날아갔는지 이해 못함

  • 원인: writable layer와 외부 저장소의 수명주기 차이를 구분하지 못함.
  • 해결: “데이터는 반드시 -v 또는 compose volumes:로 외부화”를 기본 규칙으로 고정하고 코드리뷰 항목에 넣습니다.

실수 3) 바인드 마운트 권한 문제를 도커 버그로 오해

  • 원인: 호스트 UID/GID와 컨테이너 실행 사용자가 달라 파일 쓰기 실패.
  • 해결: 컨테이너 사용자, 파일 소유권, 권한 비트(chmod/chown)를 함께 점검합니다. 개발/운영 사용자 모델을 분리해 설계합니다.

실수 4) 볼륨 정리 명령을 무심코 실행

  • 원인: docker volume prune이 미사용 볼륨을 삭제한다는 사실을 과소평가.
  • 해결: 운영 환경에서는 프룬 명령을 자동화에 넣기 전에 승인 절차를 두고, 대상 볼륨 목록을 명시적으로 검토합니다.

실무 패턴

실무에서는 데이터를 크게 세 종류로 나눠 다룹니다. (1) 소스 코드/정적 파일, (2) 런타임 생성 데이터(업로드/캐시/로그), (3) 핵심 상태 데이터(DB/큐/스토리지 메타데이터) 입니다. 이 세 가지를 같은 방식으로 마운트하면 꼭 문제가 생깁니다.

개발 환경에서는 바인드 마운트가 강력합니다. 코드 변경이 즉시 반영되어 생산성이 높기 때문입니다. 하지만 운영으로 갈수록 기준이 달라집니다. 운영은 “편의”보다 “예측 가능성”과 “복구 가능성”이 중요하므로, 상태 데이터는 볼륨 중심으로 통일하는 편이 안전합니다. 이때 볼륨 이름 정책(예: project_env_service_data)을 정해두면 장애 대응 시 식별이 쉬워집니다.

Compose를 쓸 때도 같은 원칙이 적용됩니다. 예를 들어 web 서비스는 바인드 마운트로 코드 동기화를 사용하되, postgres는 named volume을 고정합니다. 그리고 각 볼륨이 어떤 데이터의 단일 책임을 가지는지 명시합니다. “한 볼륨에 여러 의미의 데이터를 섞지 않는다”는 규칙은 백업/복구를 단순화합니다.

백업 관점에서는 “볼륨이 있으니 안전하다”가 아니라 “복원 테스트를 했는가”가 기준입니다. 최소한 주기적으로 덤프를 떠서 다른 컨테이너에서 실제 복원이 되는지 확인해야 합니다. 특히 팀이 커지면 백업 파일 위치, 보관 주기, 삭제 정책, 암호화 여부를 표준 문서로 고정해야 인수인계가 됩니다.

보안 측면에서는 바인드 마운트가 호스트 파일시스템과 가까운 만큼 공격 표면이 넓어질 수 있습니다. 민감 경로(/, /etc, 사용자 홈 전체)를 광범위하게 마운트하는 패턴은 피하고, 필요한 하위 디렉터리만 최소 권한(read-only 가능하면 ro)으로 연결해야 합니다.

마지막으로 운영 점검 루틴을 추천합니다. 서비스 배포 전에 다음 4가지를 자동 검사하면 사고를 크게 줄일 수 있습니다.

  1. 상태 저장 서비스가 볼륨 없이 실행되지 않는가
  2. 마운트 경로가 문서와 실제 compose 설정에서 일치하는가
  3. 백업 스크립트가 최신 스키마 기준으로 동작하는가
  4. 복원 리허설이 최근에 수행되었는가

이 4가지만 지켜도 “재시작했더니 데이터가 사라졌다” 같은 초반 대형 사고 대부분을 예방할 수 있습니다.

오늘의 결론

한 줄 요약: 개발 속도는 바인드 마운트로, 운영 영속성은 볼륨으로 확보하라. 그리고 영속성은 반드시 재생성 테스트로 증명하라.

도커를 안정적으로 쓰는 팀은 명령어를 많이 아는 팀이 아니라, 데이터 수명주기를 먼저 설계하는 팀입니다.

연습문제

  1. Alpine 컨테이너에서 /data에 파일을 저장한 뒤, 볼륨 없이 재생성했을 때와 named volume을 썼을 때 결과를 비교해보세요.
  2. Nginx를 바인드 마운트로 띄우고, 호스트 HTML 파일을 수정해 즉시 반영되는지 확인하세요. 이후 :ro 옵션을 제거/추가하며 차이를 설명해보세요.
  3. Postgres 컨테이너에 볼륨을 연결해 테이블/레코드를 만든 뒤 컨테이너를 삭제·재생성하고 데이터가 유지되는지 검증하세요. 검증 절차를 팀 체크리스트 형태로 5줄 이내로 정리해보세요.

이전 강의 정답

9강 연습문제 해설:

  • Dockerfile에서 COPY requirements.txtRUN pip install -r requirements.txt를 먼저 두고, COPY app/ /app/를 나중에 두면 코드만 바뀌는 상황에서 의존성 레이어 캐시를 재사용할 가능성이 큽니다. 빌드 시간이 크게 단축됩니다.
  • requirements.txt에 패키지 버전을 추가/변경하면 해당 레이어부터 캐시가 무효화되어 pip 설치가 다시 실행됩니다. docker build --progress=plain 로그에서 설치 단계 재실행 여부를 명확히 확인할 수 있습니다.
  • .dockerignore에서 .venv, __pycache__, .git, 로그 파일을 제외하면 컨텍스트 전송량이 줄어들고 불필요한 캐시 무효화가 감소합니다. 빌드 속도와 재현성이 함께 개선됩니다.

실습 환경/재현 정보

  • OS: macOS 15+ 또는 Ubuntu 22.04+
  • Docker 버전: Docker Engine/Desktop 25.x 이상
  • 사용 이미지: alpine, nginx:alpine, postgres:16
  • 실행 순서:
    1. 컨테이너 writable layer 데이터 소실 실험
    2. named volume 영속성 실험
    3. bind mount 실시간 반영 실험
    4. Postgres + volume 재생성 검증
  • 재현 체크:
    • 컨테이너 삭제 후에도 볼륨 데이터가 유지되는가?
    • bind mount가 호스트 변경을 즉시 반영하는가?
    • 상태 저장 서비스(DB)가 볼륨 없이 배포되지 않도록 설정되어 있는가?
    • 권한/보안(읽기 전용 마운트, 최소 경로 마운트) 기준이 문서화되어 있는가?