Skip to Content

10장. 사라지는 쓰기, 남는 쓰기 — 컨테이너 레이어·볼륨·바인드 마운트

컨테이너를 지우면 쓴 게 사라지고, 볼륨에 쓰면 남습니다. 그리고 그렇게 남은 파일은 호스트에서도 uid 0이었어요. 5장에서 “안의 root와 밖의 root가 커널 눈에는 같은 사용자”라고 했던 게 여기서 파일로 드러납니다.

{/* 삽화 자리: 선 안의 책상 위에 서류 두 무더기. 한 무더기는 책상에 그냥 얹혀 있고, 다른 무더기는 바닥에서 올라온 선반 위에 놓여 있는데 그 선반은 선 밖까지 이어져 있다. 선을 지우자 책상 위 무더기는 같이 사라지고 선반 위 무더기만 남는다. 프롬프트: docs/book/illustration-prompts-container.md */}


배경

Postgres 컨테이너에 스키마를 밀어 넣고, 이미지 태그를 하나 올려 다시 띄웠다고 해봅시다. 테이블이 없습니다.

8장에서 UpperDir 디렉터리가 통째로 사라지는 걸 봤죠. 그 안에 DB 데이터가 있었던 겁니다.

컨테이너에는 “사라져도 되는 쓰기”와 “남아야 하는 쓰기”를 가르는 장치가 따로 있습니다. 이 장이 그걸 다룹니다.

5장이 남긴 질문도 여기서 답합니다. 컨테이너 안에서 만든 파일은 누구 소유가 되느냐고요.

스토리

먼저 아무것도 안 붙이고 써봤습니다.

docker run --name ch10a alpine:3.20 sh -c 'echo "중요한 데이터" > /data.txt; cat /data.txt' docker rm -f ch10a docker run --rm alpine:3.20 sh -c 'cat /data.txt 2>/dev/null || echo "(없음)"'
=== 1. 컨테이너 레이어에 쓰면 (아무것도 안 붙이고) === 중요한 데이터 컨테이너를 지우고 새로 띄우면: (없음)

✅ 실측 (Docker Engine 29.7.2 / Docker Desktop 4.87.0 / alpine 3.20 / Apple M1 / macOS 26.6, 2026-09. 전체 소스: docs/book/code/container/ch10/volumes.sh)

8장에서 본 그대로입니다. 쓸 때는 잘 써지고, 컨테이너와 함께 사라집니다.

이제 볼륨을 붙여봤습니다.

docker volume create ch10vol docker run --rm -v ch10vol:/v alpine:3.20 sh -c 'echo "중요한 데이터" > /v/data.txt' docker run --rm -v ch10vol:/v alpine:3.20 cat /v/data.txt
=== 2. 볼륨에 쓰면 === 컨테이너를 지우고 새 컨테이너에서 읽으면: 중요한 데이터 볼륨이 실제로 사는 곳: /var/lib/docker/volumes/ch10vol/_data

✅ 실측 (같은 환경, 같은 소스)

남았습니다. 컨테이너를 만든 쪽도 읽은 쪽도 --rm으로 띄워 이미 사라졌는데도요.

마지막 줄이 이유를 말해줍니다. 볼륨은 컨테이너 레이어 바깥, 호스트의 다른 디렉터리에 삽니다. 컨테이너는 그 디렉터리를 자기 /v에 갖다 붙였을 뿐이에요.

리눅스 서버라면 그 경로를 sudo ls로 그냥 보면 됩니다. 맥에서는 /var/lib/docker가 Docker Desktop의 리눅스 VM 안에 있어서 터미널에서는 안 보여요. VM 안을 보려면 docker run --rm --privileged --pid=host alpine nsenter -t 1 -m -- ls /var/lib/docker/volumes처럼 들어가야 합니다. 아래 호스트 쪽 소유자 확인도 같은 방법으로 했습니다.

6장에서 마운트 namespace를 만들면서 tmpfs를 붙여본 게 이겁니다. 볼륨도 결국 마운트고, 컨테이너의 마운트 namespace에 디렉터리 하나를 더 끼워 넣는 일입니다.

핵심

세 가지 쓰기

컨테이너 안에서 파일에 쓰는 자리는 셋입니다.

어디에 쓰이나컨테이너를 지우면쓰는 비용
컨테이너 레이어UpperDir (overlay 위층)사라진다아래층 파일을 고치면 통째 복사, 새 파일은 그냥 쓰기
볼륨/var/lib/docker/volumes/…남는다그냥 파일 쓰기
바인드 마운트호스트의 지정한 경로남는다그냥 파일 쓰기

세 번째 열이 이 장의 제목이고, 네 번째 열이 DB를 컨테이너 레이어에 두면 안 되는 두 번째 이유입니다.

8장에서 20MB 파일의 1바이트를 고치니 20MB가 통째로 올라왔죠. 그게 컨테이너 레이어의 비용입니다. 볼륨과 바인드 마운트는 overlay 마운트 바깥이라 커널이 copy-up을 할 이유가 없어요. 그냥 호스트 파일시스템에 씁니다.

DB 데이터 파일을 컨테이너 레이어에 두면 안 되는 이유가 둘인 셈입니다. 사라져서 안 되고, 느려서 안 됩니다.

볼륨과 바인드 마운트

둘 다 “바깥에 쓴다”인데 차이가 있습니다.

바인드 마운트는 내가 경로를 지정합니다. 호스트의 그 디렉터리를 컨테이너 안 어딘가에 그대로 붙여요.

mkdir -p /tmp/ch10 && echo "호스트에서 만든 파일" > /tmp/ch10/from-host.txt docker run --rm -v /tmp/ch10:/h alpine:3.20 sh -c ' cat /h/from-host.txt echo "컨테이너가 만든 파일" > /h/from-container.txt' cat /tmp/ch10/from-container.txt
=== 3. 바인드 마운트: 호스트 디렉터리를 그대로 === 컨테이너가 보는 것: 호스트에서 만든 파일 호스트가 보는 것 : 컨테이너가 만든 파일

✅ 실측 (같은 환경, 같은 소스)

양방향입니다. 호스트에서 만든 걸 컨테이너가 보고, 컨테이너가 만든 걸 호스트가 봅니다. 개발할 때 소스 코드를 이렇게 붙여두면 편하죠.

볼륨은 반대로 Docker가 경로를 정합니다. 나는 이름만 주고요. 그래서 호스트 디렉터리 구조에 의존하지 않고, 다른 기계에 옮겨도 같은 명령이 돕니다.

빈 자리를 채우는 방식도 정반대입니다. 이게 실무에서 사고를 만드는 쪽이에요.

=== 6. 빈 볼륨 vs 빈 바인드 마운트 === 빈 볼륨을 /etc 에 : 36 빈 디렉터리를 /etc 에 : 3

✅ 실측 (같은 환경, 같은 소스)

빈 볼륨을 /etc에 씌우면 이미지에 있던 파일 36개가 볼륨으로 복사돼 들어옵니다. 빈 디렉터리를 같은 자리에 바인드 마운트하면 그 36개가 가려지고 3개만 남고요. -v 뒤에 이름을 쓰느냐 경로를 쓰느냐, 그 한 글자 차이로 정반대입니다.

node_modules를 볼륨으로 덮어씌우는 트릭이 이래서 동작하고, “compose 붙였더니 설정 파일이 다 사라졌다”는 사고도 여기서 나옵니다.

정리하면 이렇습니다. 바인드 마운트는 “이 호스트의 이 경로”가 중요할 때, 볼륨은 “데이터가 남기만 하면 될 때” 씁니다. 대신 볼륨은 docker rm으로 안 사라집니다. 치우는 건 내 몫이고, 이름 없이 만든 볼륨은 docker volume ls에 해시만 남아 쌓여요.

그 파일은 누구 것인가

5장의 질문입니다. 거기서 namespace 여덟 종류 중 user만 호스트와 같다고 했죠. 컨테이너 안의 root와 호스트의 root가 커널 눈에는 같은 사용자라고요.

그 말이 파일로 어떻게 드러나는지 봤습니다. 볼륨에 컨테이너 root로 파일을 하나 만들고, 안팎에서 각각 소유자를 읽었어요.

docker volume create ch10own docker run --rm -v ch10own:/v alpine:3.20 sh -c 'echo x > /v/by-root.txt' docker run --rm -v ch10own:/v alpine:3.20 ls -ln /v/by-root.txt # 컨테이너 안 sudo ls -ln "$(docker volume inspect ch10own --format '{{.Mountpoint}}')/by-root.txt" # 호스트 (리눅스 기준)
--- 진짜 리눅스 파일시스템에서 (볼륨) --- 컨테이너 안에서 본 소유자 : 0:0 호스트에서 본 소유자 : 0:0 → 같은 uid 0. 안의 root 와 밖의 root 가 같은 사용자다

✅ 실측 (같은 환경, 같은 소스)

같은 숫자입니다.

파일에는 사용자 이름이 아니라 uid 숫자가 적힙니다. 컨테이너 안의 root는 uid 0이고, 호스트의 root도 uid 0이에요. user namespace를 안 나눴으니 그 0이 번역 없이 그대로 건너갑니다.

볼륨도 리눅스 서버의 바인드 마운트도 커널 눈에는 같은 bind mount입니다. 그래서 위 0:0이 곧 리눅스 서버 바인드 마운트의 결과예요. (📄 리눅스 서버에서 직접 재지는 못했습니다. 여기 숫자는 Docker Desktop VM 안 리눅스에서 잰 것입니다.)

이게 실무에서 아픈 자리입니다. 바인드 마운트로 소스 디렉터리를 붙여두고 컨테이너 안에서 빌드를 돌리면, 나온 결과물이 호스트에서 root 소유가 됩니다. 내 계정으로는 못 지우죠. sudo rm -rf를 치게 되는 그 상황이 여기서 나옵니다.

해결책은 컨테이너를 내 uid로 돌리는 겁니다.

docker run --rm -u "$(id -u):$(id -g)" -v "$PWD":/h alpine:3.20 sh -c 'echo x > /h/as-me.txt'

이러면 프로세스가 uid 0이 아니라 내 번호로 돌고, 만드는 파일도 내 소유가 됩니다.

대신 대가가 붙습니다. 이건 바인드 마운트에서만 통해요.

=== 5. -u 로 돌리면 무엇이 깨지나 === 볼륨에 -u 1000 으로 쓰기: sh: can't create /v/a.txt: Permission denied 볼륨 _data 소유자 : drwxr-xr-x 0:0 -u 1000 컨테이너 안의 신원: whoami: unknown uid 1000 HOME=[/]

✅ 실측 (같은 환경, 같은 소스)

새 볼륨의 디렉터리는 root:root 0755라 uid 1000으로는 못 씁니다. 그리고 컨테이너 안에는 그 uid를 가진 사용자가 아예 없어요. whoami가 깨지고 $HOME이 /가 됩니다. git이 dubious ownership으로 멈추거나 npm이 홈을 못 찾는 게 여기서 나와요.

그래서 이미지에 처음부터 USER를 박아두고 볼륨 소유자를 맞춰주는 쪽이 낫습니다.

근본적인 해결은 user namespace를 켜는 것이고요. 5장에서 말한 userns-remap이나, 기본으로 그걸 쓰는 rootless Docker·Podman 쪽입니다.

맥에서는 다르게 보입니다

5장에서 이 이야기를 예고할 때 “바인드 마운트”라고 했는데, 정확히는 리눅스 서버 기준이었습니다. 맥에서 같은 걸 재현하려면 볼륨이어야 해요. 위 결과가 볼륨에서 나온 이유가 그겁니다.

맥에서 호스트 디렉터리를 바인드 마운트하면 다르게 나옵니다.

--- 맥에서 호스트 디렉터리를 바인드 마운트하면 (예외) --- 맥에서 본 소유자: uid=501 내 uid: 501 → 컨테이너 안에서는 여전히 0:0 이다

✅ 실측 (같은 환경, 같은 소스)

컨테이너가 root로 만들었는데 맥에서는 제 계정 소유로 보입니다. Docker Desktop의 파일 공유 계층이 맥 쪽 쓰기를 제 계정으로 대신 수행하기 때문이에요(📄 문서 기반, 미검증).

중요한 건 안쪽 숫자는 안 바뀐다는 겁니다. 컨테이너 안에서는 맥에서도 리눅스에서도 똑같이 0:0으로 보여요. 그래서 컨테이너 안만 봐서는 이 차이를 절대 못 잡습니다.

그래서 맥에서 개발하면 앞의 그 문제를 안 만납니다. 리눅스 서버에 올리는 순간 만나고요. “내 노트북에서는 되는데”의 또 다른 버전입니다. 4장에서 digest 이야기를 하며 말했던 그 축이죠.

번역해줄 선이 없었습니다

볼륨과 바인드 마운트는 선에 구멍을 뚫는 게 아닙니다. 애초에 그 선이 파일 경로만 나눴을 뿐이고, 사용자 신원은 나눈 적이 없거든요. 6장에서 마운트 namespace를 만들어봤지만 거기서 user는 건드리지 않았습니다.

그래서 파일이 선을 넘어가면 그 파일의 주인도 같이 넘어갑니다. 번역해줄 선이 안 그어져 있으니까요.

여는 글에서 한 말이 여기서 파일로 다시 증명됩니다. 어디에 선을 그었고 어디에 안 그었는지 모르면, 넘어가지 말아야 할 것이 넘어갑니다.

정리

  • 컨테이너 레이어에 쓴 것은 컨테이너와 함께 사라집니다. 8장에서 UpperDir이 없어지는 걸 확인했고, 실측에서 파일도 그대로 사라졌습니다.
  • 볼륨과 바인드 마운트는 컨테이너 레이어 바깥에 씁니다. 그래서 남고, 이미지에 있던 큰 파일을 고쳐도 copy-up이 없습니다.
  • 빈 볼륨은 이미지에 있던 내용을 복사해 받고, 빈 바인드 마운트는 그걸 덮어 가립니다. 실측에서 /etc가 36개와 3개로 갈렸습니다.
  • 바인드 마운트는 내가 호스트 경로를 지정하고, 볼륨은 Docker가 정합니다. 전자는 “이 경로”가 중요할 때, 후자는 “남기만 하면 될 때” 씁니다.
  • 파일에는 이름이 아니라 uid 숫자가 적힙니다. user namespace를 안 나눴으니 컨테이너 root(uid 0)가 만든 파일은 호스트에서도 uid 0입니다. 실측에서 안팎 모두 0:0이었습니다.
  • 그래서 바인드 마운트에 빌드 결과물을 떨구면 호스트에서 root 소유가 됩니다. -u $(id -u):$(id -g)로 돌리면 되지만 볼륨에서는 Permission denied가 나고 컨테이너 안에 그 uid가 없어 whoami가 깨집니다. 근본 해결은 user namespace입니다.
  • 맥의 Docker Desktop은 바인드 마운트에서 맥 쪽 쓰기를 내 계정으로 대신 합니다. 컨테이너 안 숫자는 그대로 0:0이라 안쪽만 봐서는 못 잡아요. 그래서 맥에서는 이 문제를 못 만나고 리눅스 서버에서 만납니다.

생각해볼 질문

2부가 여기서 끝납니다. 커널이 긋는 선을 다 봤어요. 프로세스가 무엇인지(5장), 무엇을 가리는지(6장), 얼마나 쓰게 하는지(7장), 어떤 파일시스템을 보는지(8장), 어떻게 이어지는지(9장), 무엇이 남는지(10장)요.

이제 이 위에 진짜 앱을 올릴 차례입니다. 여는 글에서 docker stop에 10초 걸리던, 아니 3초 만에 SIGKILL로 죽던 그 Spring Boot 앱이요.

그런데 올리기 전에 물어볼 게 있습니다.

4장에서 이미지가 레이어로 쌓이고 같은 층은 한 번만 저장된다고 했죠. 8장에서는 위층이 아래층을 덮고, 지워도 아래층엔 남는다고 했고요.

그럼 Dockerfile을 어떤 순서로 쓰느냐가 이미지 크기와 빌드 시간을 바꾼다는 뜻입니다. 코드 한 줄 고쳤을 뿐인데 의존성을 통째로 다시 받는 빌드, 겪어보셨을 겁니다.

왜 그 순서가 크기와 시간을 동시에 바꿀까요. 그리고 어디를 잘라야 이미지가 줄어들까요.

▶ 11장. 레이어 캐시와 빌드 순서 — 한 줄 고쳤는데 왜 다 다시 만드나

Last updated on