Data Must Outlive the Container
한국어 원문으로 표시합니다.
한 줄 요약
컨테이너 쓰기 레이어는 컨테이너와 함께 죽습니다. 오래 살아야 하는 데이터는 볼륨이나 바인드 마운트로 컨테이너 밖에 두어야 합니다.
왜 이게 필요했나
컨테이너를 재배포했더니 업로드 파일이 전부 사라졌다는 신고는 지금도 흔합니다. 원인은 버그가 아니라 설계입니다. 쓰기 레이어는 컨테이너 객체에 붙어 있고, 컨테이너를 지우면 그 레이어도 함께 사라집니다.
여기에 성능 문제가 겹칩니다. 아래 레이어의 파일을 고치려면 위 레이어로 통째로 복사해야 하는데(copy-up), 데이터베이스 파일처럼 큰 파일에는 매우 불리합니다. 그래서 대용량 쓰기는 반드시 볼륨으로 빼라는 말이 관용구가 됐습니다.
어떻게 동작하나
세 가지 방식이 있고 성격이 다릅니다.
| 항목 | 바인드 마운트 | 이름 있는 볼륨 | tmpfs |
|---|---|---|---|
| 위치 | 호스트 경로를 직접 지정 | 런타임이 관리 | 메모리 |
| 지속성 | 호스트에 영구 | 볼륨에 영구 | 종료 시 소멸 |
| 이식성 | 낮음(경로 의존) | 높음 | 높음 |
| 백업 | 직접 관리 | 전용 명령 | 불가 |
| 주 용도 | 설정 파일, 개발 중 소스 | DB 데이터, 업로드 | 임시 파일, 시크릿 |
문법도 두 가지입니다. 짧은 -v source:target:opts 와 명시적인
--mount type=...,source=...,target=...,readonly 인데, 후자는 옵션 이름이
드러나서 리뷰에서 오해가 적습니다.
읽기 전용 마운트는 생각보다 중요합니다. 설정 파일을 :ro 로 붙이면
컨테이너가 실수로든 침해로든 설정을 고칠 수 없습니다. 애플리케이션이
쓰기를 시도하면 Read-only file system 오류가 나고, 이 오류는 곧
"이 컨테이너가 쓰면 안 되는 곳에 쓰려 하고 있다"는 신호입니다.
현장에서 만나는 모습
백업 패턴은 관용구처럼 굳어 있습니다. 볼륨과 백업 디렉터리를 동시에 마운트한 임시 컨테이너를 띄워 아카이브를 만듭니다.
docker run --rm -v dk-data:/source:ro -v /root/backup:/backup alpine:3.20 tar czf /backup/dk-data.tgz -C /source .
-C 로 소스 디렉터리 안으로 들어가 상대 경로로 담는 것이 핵심입니다.
이렇게 해야 복원할 때 경로가 어긋나지 않습니다.
rootless 환경에서는 소유권 문제가 자주 생깁니다. 호스트와 컨테이너의 UID 매핑이 달라서인데, 이때는 파일을 chmod 777 로 여는 대신 매핑을 이해하고 맞추는 쪽이 옳습니다. 권한을 열어 두면 그 컨테이너뿐 아니라 같은 호스트의 다른 프로세스에게도 열어 주는 것이기 때문입니다.
볼륨과 bind mount 를 고르는 기준
이름은 비슷하지만 성질이 다릅니다.
| 이름 있는 볼륨 | bind mount | |
|---|---|---|
| 관리 주체 | 도커 | 사람 |
| 위치 | /var/lib/docker/volumes/… |
호스트의 아무 경로 |
| 백업 | docker run --rm -v vol:/d … 로 뽑는다 |
호스트 파일 그대로 |
| 권한 | 이미지의 UID 를 따라 초기화 | 호스트 권한 그대로 |
| 개발 편의 | 소스 수정이 안 보인다 | 즉시 반영 |
권한 줄이 실무의 함정입니다. bind mount 는 호스트 파일의 소유자·모드를 그대로
가져오므로, 컨테이너가 nonroot(UID 65532)로 도는데 호스트 파일이 root:root 0644 면 쓰기가 안 됩니다. 반대로 이름 있는 볼륨은 처음 만들 때 컨테이너
안의 대상 경로 권한을 복사해 초기화하므로 대개 그냥 됩니다.
처음 한 번만 복사된다
이름 있는 볼륨의 초기화는 비어 있을 때 한 번뿐 입니다.
빈 볼륨을 /app/config 에 붙이면
→ 이미지의 /app/config 내용이 볼륨으로 복사된다
이미 내용이 있는 볼륨을 붙이면
→ 이미지의 내용은 가려진다. 복사하지 않는다
그래서 이미지를 새로 만들어 기본 설정 파일을 바꿔도 옛 볼륨을 쓰는 컨테이너는 옛 파일을 계속 봅니다. "이미지를 고쳤는데 반영이 안 된다" 의 흔한 원인입니다. 설정을 볼륨에 두지 말고 ConfigMap·환경변수로 주입하는 편이 낫습니다.
쿠버네티스에서의 대응
| 도커 | 쿠버네티스 | 쓰는 곳 |
|---|---|---|
| 이름 있는 볼륨 | PVC | DB 데이터 |
| bind mount | hostPath | 노드 파일 접근(가급적 피한다) |
| tmpfs | emptyDir: {medium: Memory} |
비밀·임시 |
| — | emptyDir: {} |
컨테이너 사이 공유(파드 수명) |
emptyDir 은 파드가 지워지면 사라집니다. 노드 재부팅에도 사라집니다. 그런데
파드 재시작(컨테이너만 죽는 경우)에는 남아 있어, 테스트할 때 착각하기 쉽습니다.
권한 문제는 fsGroup 으로 풉니다.
spec:
securityContext:
fsGroup: 2000 # 볼륨의 그룹 소유자를 2000 으로 바꿔 준다
runAsUser: 1000
runAsNonRoot: true
fsGroup 은 볼륨 안의 모든 파일 소유권을 재귀적으로 바꾸므로, 파일이 아주 많으면
파드 기동이 느려집니다. 그때는 fsGroupChangePolicy: OnRootMismatch 로 루트
디렉터리만 확인하게 합니다.
다음 실습에서 할 것
볼륨을 만들어 쓰고, 컨테이너를 지워도 데이터가 남는지 확인하고, 읽기 전용 마운트의 오류 메시지를 직접 만들어 본 뒤, 백업과 복원을 왕복합니다.