Table of contents
- 컨테이너는 죽는다, 데이터는 살아야 한다
- 세 가지 마운트 방식
- Named volume — Docker가 알아서 관리한다
- Bind mount — 호스트 디렉토리를 직접 꽂는다
- —mount — -v의 더 명시적인 동생
- tmpfs — 디스크를 안 탄다
- UID/GID 권한 문제 — 초보자가 가장 많이 당한다
- 볼륨 백업과 복원
- 볼륨 정리
- Compose에서의 볼륨
- 정리: 볼륨 선택의 기준
컨테이너는 죽는다, 데이터는 살아야 한다
4편에서 배운 컨테이너 생명주기를 떠올려보자. 컨테이너는 언제든 재생성된다. 배포할 때도, 서버가 재부팅될 때도, 심지어 자동 스케일 아웃될 때도. 이 과정에서 컨테이너 안 파일시스템은 전부 사라진다.
그럼 DB 데이터는? 업로드된 파일은? 캐시는? 전부 날아간다. 그래서 Docker는 아예 데이터를 “컨테이너 밖”에 두는 걸 기본 패턴으로 제시한다. 이걸 볼륨(Volume)이라 부른다.
flowchart LR
subgraph C1["컨테이너 A (v1)"]
APP1["앱 프로세스"]
FS1["컨테이너 파일시스템<br/>(휘발)"]
end
subgraph C2["컨테이너 B (v2, 교체됨)"]
APP2["앱 프로세스"]
FS2["컨테이너 파일시스템<br/>(휘발)"]
end
VOL[("볼륨<br/>(영속)")]
APP1 -.마운트.-> VOL
APP2 -.마운트.-> VOL
컨테이너가 v1에서 v2로 교체돼도, 볼륨은 같은 자리에 그대로 있다. 새 컨테이너는 같은 볼륨을 다시 마운트해서 이전 데이터를 이어간다. 볼륨은 컨테이너보다 오래 산다.
세 가지 마운트 방식
Docker는 데이터를 컨테이너에 끼워주는 방식을 세 가지 제공한다.
| 방식 | 설명 | 주 용도 |
|---|---|---|
| Named volume | Docker가 관리하는 저장 공간 | DB, 앱 데이터 등 운영 데이터 |
| Bind mount | 호스트의 임의 디렉토리를 직접 마운트 | 개발 중 코드 공유, 로그 수집 |
| tmpfs | 메모리에만 존재하는 임시 저장 공간 | 민감한 임시 데이터 |
이 셋이 쓰이는 자리가 분명히 다르다. 하나씩 파보자.
Named volume — Docker가 알아서 관리한다
가장 권장되는 방식이다. Docker가 자기만의 디렉토리 아래(/var/lib/docker/volumes/...)에 데이터를 저장하고, 이름으로 그걸 참조한다.
# 볼륨 생성
docker volume create pgdata
# 볼륨을 마운트해서 컨테이너 실행
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# 볼륨 확인
docker volume ls
# DRIVER VOLUME NAME
# local pgdata
docker volume inspect pgdata
# [{ "Mountpoint": "/var/lib/docker/volumes/pgdata/_data", ... }]
-v pgdata:/var/lib/postgresql/data는 “pgdata라는 볼륨을 컨테이너 안 /var/lib/postgresql/data에 연결해라”다. 앞부분이 볼륨 이름, 뒷부분이 컨테이너 안 경로다.
컨테이너를 지우고 새로 띄워도 같은 볼륨을 마운트하면 데이터가 그대로다.
docker rm -f postgres
# 새 컨테이너로 재연결
docker run -d --name postgres -e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16
# 이전 DB 내용이 그대로 살아 있다
Named volume을 권장하는 이유
- 호스트 경로에 묶이지 않는다. 호스트 디렉토리 경로가 바뀌어도 영향 없음
- 권한 관리가 단순하다. 이미지의 UID/GID와 맞춰 자동 초기화됨 (처음 마운트 시)
- Docker 수준에서 백업/이관이 쉽다.
docker volume명령으로 일관되게 다룸 - 드라이버 교체 가능. 로컬 볼륨뿐 아니라 NFS, AWS EBS 등 플러그인을 꽂을 수 있음
- OS 파일시스템 의존성이 적다. macOS/Windows의 Docker Desktop에서 bind mount보다 훨씬 빠름
DB, 메시지큐, 캐시처럼 ‘이 컨테이너가 소유하는 데이터’는 Named volume. 이게 기본 원칙이다.
Bind mount — 호스트 디렉토리를 직접 꽂는다
호스트 파일시스템의 특정 경로를 컨테이너 안으로 마운트한다. 컨테이너와 호스트가 같은 파일을 본다.
# 호스트의 현재 디렉토리를 컨테이너 /app에 마운트
docker run -d --name dev \
-v $(pwd):/app \
-p 3000:3000 \
node:22-slim node /app/server.js
$(pwd)를 마운트한 덕분에 호스트에서 코드를 수정하면 컨테이너에서도 즉시 반영된다. 개발 환경에서 엄청 편하다. 정확히 말하면 이게 bind mount의 대표 용도다.
운영에선 bind mount를 조심히 쓴다. 이유가 있다.
- 호스트 경로에 의존하니 서버마다 경로가 다르면 이식성이 떨어짐
root권한 문제로 엉뚱한 파일을 덮어쓰거나 삭제할 수 있음- macOS/Windows의 Docker Desktop에서는 I/O가 느림 (가상화된 파일공유를 거침)
운영에서 bind mount가 정당화되는 건 보통 이런 경우다.
- 로그 수집:
/var/log/containers같은 호스트 경로에 로그를 쌓고, 외부 수집기가 읽음 - 호스트의 특정 자원 접근:
/var/run/docker.sock,/etc/localtime같은 호스트 파일 공유 - 설정 파일 주입: 읽기 전용(
:ro)으로 config 파일 마운트
# 읽기 전용 bind mount 예시
docker run -d --name nginx \
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \
-p 80:80 \
nginx:1.27
:ro는 read-only다. 컨테이너가 실수로 설정 파일을 덮어쓰는 걸 막는다.
—mount — -v의 더 명시적인 동생
같은 일을 하는 문법이 하나 더 있다. --mount는 -v보다 장황하지만 덜 모호하다.
# -v 스타일
docker run -v pgdata:/var/lib/postgresql/data postgres:16
# --mount 스타일
docker run --mount type=volume,source=pgdata,target=/var/lib/postgresql/data postgres:16
# bind mount도 마찬가지
docker run --mount type=bind,source=$(pwd),target=/app,readonly node:22-slim
-v는 문법이 간결한 대신 type 구분이 직관적이지 않다(경로 형태로 volume인지 bind인지 유추). --mount는 type=...을 명시하므로 오타나 실수가 줄어든다. 스크립트/CI에선 --mount를 권장하는 흐름이다.
tmpfs — 디스크를 안 탄다
메모리에만 존재하는 임시 저장 공간이다. 컨테이너가 종료되면 사라진다.
docker run -d --name app \
--tmpfs /tmp:size=64m \
myapp
이 옵션이 쓰이는 자리는 두 가지다.
- 성능: 빠른 임시 저장. /tmp 같은 캐시에 유리
- 보안: 디스크에 안 남아야 할 민감한 데이터(세션, 토큰 등)
애플리케이션 대부분은 tmpfs를 신경 쓰지 않아도 된다. 필요할 때만 꺼내 쓰는 옵션이다.
UID/GID 권한 문제 — 초보자가 가장 많이 당한다
볼륨을 쓰다 보면 정체불명의 Permission denied를 만난다. 보통은 UID/GID 불일치 때문이다.
상황을 재현해보자. 호스트에서 현재 유저(UID 1000)의 디렉토리를 만들고, 컨테이너 안에서는 다른 UID로 그 디렉토리에 쓰기를 시도한다.
mkdir -p ./data
sudo chown 1000:1000 ./data
# PostgreSQL은 UID 70 (또는 999)로 실행됨
docker run --rm \
-v $(pwd)/data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
# initdb: error: could not change permissions of directory ...
컨테이너 안 프로세스는 postgres 유저(예: UID 999)인데, 호스트 디렉토리는 UID 1000 소유다. 쓰기 권한이 없다.
해결 방법은 여러 갈래다.
1. Named volume 쓰기 (가장 단순)
Named volume은 처음 마운트될 때 Docker가 이미지의 기본 유저에 맞춰 권한을 설정해준다. 이게 “Named volume을 권장”하는 중요한 이유 중 하나다.
docker run -d --name db \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
# 문제 없이 뜬다
2. bind mount 쓸 때는 호스트 권한을 이미지의 UID/GID에 맞춘다
# postgres 이미지의 UID 확인
docker run --rm postgres:16 id postgres
# uid=999(postgres) gid=999(postgres)
sudo chown -R 999:999 ./data
3. 컨테이너 실행 시 UID를 호스트와 맞추기
docker run --rm \
--user $(id -u):$(id -g) \
-v $(pwd)/data:/app/data \
myapp
--user는 Dockerfile의 USER 지시어를 덮어쓴다. 단, 이미지 내부에 해당 UID의 유저가 등록돼 있지 않아도 무방하지만, 일부 프로그램은 “등록된 유저”를 요구해서 에러를 낼 수 있다.
4. init 단계에서 권한 맞추기 (이미지 제작자의 선택)
데이터 볼륨을 처음 마운트할 때만 한 번 chown을 걸어 주는 entrypoint 스크립트를 두는 패턴도 흔하다.
#!/bin/sh
# entrypoint.sh
chown -R app:app /data
exec gosu app "$@"
gosu는 su보다 시그널을 더 잘 전달하는 대안이다. 공식 이미지들도 이 방식을 많이 쓴다.
핵심 원칙: 운영용 데이터는 Named volume을 쓴다. 꼭 bind mount를 써야 한다면 호스트 디렉토리의 UID/GID를 이미지에 맞춘다.
볼륨 백업과 복원
Named volume은 Docker가 관리하지만, 결국 호스트 파일시스템 어딘가에 있다. 백업도 어렵지 않다.
볼륨을 tar로 떠내기
docker run --rm \
-v pgdata:/source:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /source .
무슨 일이 일어나는지 뜯어보면.
- 임시
alpine컨테이너를 띄우고 - 백업할 볼륨(
pgdata)을/source에 read-only로 마운트 - 호스트의 현재 디렉토리를
/backup에 마운트 /source내용을 tar로 묶어/backup에 저장
컨테이너는 실행이 끝나면 --rm으로 자동 삭제된다. 호스트에는 pgdata-YYYYMMDD.tar.gz가 남는다.
복원
# 새 볼륨 준비
docker volume create pgdata-restore
docker run --rm \
-v pgdata-restore:/target \
-v $(pwd):/backup \
alpine \
sh -c "cd /target && tar xzf /backup/pgdata-20260420.tar.gz"
# 새 볼륨으로 DB 실행
docker run -d --name pg-restored \
-v pgdata-restore:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
DB 같은 상태 서비스는 tar로 파일 덤프하는 방식보다 각 DB 고유의 백업 도구(pg_dump, mongodump 등)를 쓰는 게 안전하다. 실행 중인 파일을 복사하면 트랜잭션 중간 상태가 남을 수 있기 때문이다. tar 덤프는 “일관된 상태”를 보장하려면 컨테이너를 멈춘 뒤에 뜨는 게 정석이다.
볼륨 정리
쓰지 않는 볼륨은 조용히 디스크를 먹는다. 주기적으로 확인하자.
# 현재 어떤 컨테이너에도 연결 안 된 볼륨만 찾기
docker volume ls --filter dangling=true
# 한 번에 정리
docker volume prune
# 특정 볼륨만 삭제
docker volume rm old-cache
docker volume prune은 안전장치가 없다. 실수로 중요한 볼륨을 날리지 않도록, 정리 전에 docker volume ls --filter dangling=true로 한 번 확인하는 습관을 들이자.
Compose에서의 볼륨
단일 docker run 명령으로 볼륨을 관리하다 보면 점점 손이 많이 간다. docker compose는 이걸 선언형으로 정리한다.
# docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
pgdata:
volumes 섹션에서 named volume을 정의하고, 서비스에서 참조한다. ./init.sql은 bind mount다. Compose가 내부적으로 myproject_pgdata 같은 접두어를 붙인 볼륨을 만든다.
docker compose down은 컨테이너만 내린다. 볼륨은 남는다. 볼륨까지 지우려면 docker compose down -v를 명시해야 한다. 이 동작이 데이터 보호의 안전망 역할을 한다.
정리: 볼륨 선택의 기준
마지막으로 “어떤 마운트를 언제 쓰느냐”를 한 장으로 정리한다.
flowchart TD
START["데이터를 컨테이너에 넣어야 한다"] --> Q1{"운영 데이터?<br/>(DB, 파일 업로드 등)"}
Q1 -->|Yes| NV["Named volume"]
Q1 -->|No| Q2{"호스트 파일을<br/>직접 공유?"}
Q2 -->|Yes| BM["Bind mount<br/>(개발용 코드, 설정, 로그)"]
Q2 -->|No| Q3{"디스크에<br/>남으면 안 됨?"}
Q3 -->|Yes| TM["tmpfs"]
Q3 -->|No| NV2["Named volume<br/>(기본값)"]
의심스러우면 Named volume이다. Bind mount는 의도가 분명할 때만 쓴다. tmpfs는 성능/보안 특수 목적에 한정.
다음 편에서는 컨테이너끼리, 또 컨테이너와 외부가 어떻게 통신하는가로 넘어간다. bridge와 host, overlay, none 드라이버의 차이, DNS로 컨테이너 이름을 찾는 구조, 포트 포워딩의 내부 동작, user-defined network의 장점까지 마지막으로 꿰맞춰본다.
→ 6편: 네트워크

Loading comments...