Docker container là stateless by design: mọi thứ ghi vào writable layer bên trong container sẽ biến mất khi container bị xóa. Đây không phải bug — đây là thiết kế. Container được tạo ra để có thể destroy và recreate bất kỳ lúc nào mà không để lại gì. Nhưng app thực tế cần dữ liệu tồn tại lâu hơn container: database, file upload, cache, config.
Docker cung cấp ba mechanism để mount storage bên ngoài vào container: named volume (Docker quản lý), bind mount (thư mục host), và tmpfs (RAM). Mỗi loại có mô hình persistence, permission, và use case khác nhau. Chọn sai loại — ví dụ không mount volume cho database — là mất dữ liệu khi container bị recreate. Bài này đi qua cả ba, kèm backup strategy và những lỗi permission thường gặp.
flowchart LR
subgraph Container["Container"]
direction TB
WL["Writable Layer<br/>(mất khi xóa container)"]
VL["Volume/Bind Mount<br/>(persist độc lập)"]
end
Host["Host Filesystem"]
RAM["RAM (tmpfs)"]
WL -.->|"dữ liệu tạm"| Container
VL -->|"persist"| Host
RAM -->|"temporary"| Container
Tại sao cần volume
Container có hai nguồn dữ liệu: image layer (read-only, từ Docker image) và writable layer (read-write, tạo ra khi container chạy). Writable layer dùng copy-on-write (CoW) — mỗi lần bạn ghi file, Docker copy file đó từ image layer lên writable layer, rồi ghi thay đổi tại đó. Cơ chế này giải thích ở Phần 3: Image, layer & pull/inspect.
Vấn đề: writable layer gắn liền với container lifecycle. Bạn xóa container, writable layer biến mất cùng toàn bộ dữ liệu. Không có cách nào lấy lại. Không có “recycle bin” cho container.
Đây là ba lý do bạn không bao giờ nên lưu dữ liệu quan trọng trong writable layer:
- Container vòng đời ngắn. Container được thiết kế để tạo, xóa, replace liên tục.
docker compose up -d --force-recreatelà chuyện hàng ngày. - Performance kém. CoW writable layer dùng storage driver (overlay2, aufs…), I/O chậm hơn đáng kể so với volume mount trực tiếp.
- Không share được dữ liệu. Writable layer của container A không thể thấy được từ container B.
--rm với container chứa dữ liệu quan trọng. Flag --rm xóa container ngay khi stop — writable layer mất không thể khôi phục. Nếu bạn chưa mount volume, dữ liệu đi luôn.Named volume: Docker quản lý, bạn yên tâm
Named volume là storage do chính Docker daemon quản lý. Docker tạo thư mục trong /var/lib/docker/volumes/<tên-volume>/_data, mount vào container, và bạn không cần biết nó nằm ở đâu trên host.
# Tạo volume
docker volume create pgdata
# Kiểm tra
docker volume ls
# DRIVER VOLUME NAME
# local pgdata
# Dùng với container
docker run -d \
--name postgres \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
# Inspect volume
docker volume inspect pgdata
Kết quả inspect cho thấy mountpoint chính xác:
[
{
"Name": "pgdata",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
"Scope": "local"
}
]
Ưu điểm:
- Isolation. Docker quản lý storage, process ngoài container không vô tình sửa được dữ liệu trừ khi có root trên host.
- Backup dễ. Bạn chỉ cần biết tên volume, không cần nhớ path.
- Portable. Volume không phụ thuộc vào cấu trúc thư mục host — chạy được trên mọi máy.
- Driver ecosystem. Driver local là mặc định, nhưng có thể dùng driver NFS, CIFS, cloud (AWS EBS, Azure Disk) để mount remote storage.
Nhược điểm:
- Khó truy cập trực tiếp từ host. Bạn không thể
cdvào volume như thư mục bình thường (trừ khi vào/var/lib/docker/volumes/với quyền root). - Mountpoint thay đổi theo OS. Trên Docker Desktop Mac/Windows, volume nằm trong VM ảo — bạn không thể truy cập từ host filesystem.
Volume trong Docker Compose
Docker Compose giúp khai báo volume rất gọn:
# docker-compose.yml
services:
postgres:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
volumes:
pgdata: # Docker tự tạo named volume
Khi bạn docker compose down, volume không bị xóa (trừ khi thêm flag -v). Khi docker compose up lại, volume cũ được attach — dữ liệu nguyên vẹn. Đây là lý do team mình dùng Compose volume cho mọi service database trong development.
Bind mount: mount trực tiếp thư mục host
Bind mount mount một thư mục (hoặc file) cụ thể từ host filesystem vào container. Docker không quản lý bind mount — nó chỉ là “cầu nối” giữa hai filesystem.
# Mount thư mục host vào container
docker run -d \
--name dev-app \
-v /home/longvo/projects/myapp:/app \
node:20
# Bind mount file cấu hình đơn lẻ
docker run -d \
--name nginx \
-v /home/longvo/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx:alpine
Flag :ro ở cuối mount spec nghĩa là read-only — container chỉ đọc, không ghi được. Đây là best practice: nếu container chỉ cần đọc config, mount read-only.
Ưu điểm:
- Dev experience tốt. Code trên host, container thấy ngay — hot reload không cần rebuild image. Đây là lý do mọi dev environment dùng bind mount.
- Truy cập trực tiếp. Bạn dùng editor, file manager trên host bình thường. Không cần
docker cp. - Không phụ thuộc Docker. Dữ liệu nằm trong thư mục host bạn chọn — kể cả khi gỡ Docker, dữ liệu vẫn ở đó.
Nhược điểm:
- Path phụ thuộc host.
/home/longvo/projectscó thể không tồn tại trên máy khác. Bind mount làm mất tính portable của container. - Permission issue. UID/GID trên host khác với trong container — file tạo bởi container có thể không đọc được từ host và ngược lại.
- Bảo mật. Container có thể sửa file trên host nếu mount read-write. Mount nhầm
/etchost vào container là thảm họa.
/var/run/docker.sock vào container để “quản lý Docker từ container” là anti-pattern bảo mật nghiêm trọng — container đó có toàn quyền root trên Docker host.Bind mount vs named volume: khi nào dùng gì?
| Tiêu chí | Named Volume | Bind Mount |
|---|---|---|
| Ai quản lý | Docker daemon | Bạn |
| Portability | Cao — chỉ cần tên volume | Thấp — phụ thuộc path host |
| Backup | Docker CLI hỗ trợ | Dùng tool host bình thường |
| Dev workflow | Bất tiện (không thấy file) | Lý tưởng (hot reload) |
| Production | Nên dùng | Tránh, trừ khi có lý do chính đáng |
| Driver | Hỗ trợ driver (NFS, cloud…) | Không — chỉ local filesystem |
tmpfs: dữ liệu chỉ tồn tại trong RAM
tmpfs mount một filesystem tạm thời trong RAM của container. Dữ liệu mất ngay khi container stop — kể cả khi bạn không xóa container.
# Mount tmpfs vào /tmp của container
docker run -d \
--name app \
--tmpfs /tmp:rw,size=128M \
node:20
Trong Docker Compose:
services:
app:
image: node:20
tmpfs:
- /tmp:size=128M
- /run:size=16M,noexec
Các option phổ biến: size (giới hạn dung lượng), mode (permission, mặc định 1777), noexec (không cho phép execute binary trong tmpfs).
Dùng tmpfs khi nào:
- Cache tạm thời. Session cache, compiled template, thumbnail — thứ có thể tạo lại.
- Secrets tạm thời. API key chỉ cần trong một session, không muốn ghi xuống disk.
- Temporary file.
/tmphoặc/run— process ghi nhiều file nhỏ không cần persist. - Giảm I/O disk. Application ghi log debug rất nhiều — mount log dir vào tmpfs để tránh mòn SSD.
--memory=512M và bạn mount tmpfs 1GB, kernel cho phép — nhưng host sẽ OOM nếu tổng RAM vật lý cạn. Luôn đặt size trên tmpfs và giữ tổng tmpfs + container memory trong giới hạn host.
flowchart TB
subgraph Lifecycle["Data Lifecycle per Mount Type"]
direction LR
subgraph NV["Named Volume"]
NV1["Container Create"] --> NV2["Data Persisted"]
NV2 --> NV3["Container Stop"]
NV3 --> NV4["Data Still Exists<br/>(/var/lib/docker/volumes/)"]
NV4 --> NV5["Container Remove"]
NV5 --> NV6["Data Still Exists<br/>(until docker volume rm)"]
end
subgraph BM["Bind Mount"]
BM1["Container Create"] --> BM2["Data on Host Path"]
BM2 --> BM3["Container Remove"]
BM3 --> BM4["Data Still on Host"]
end
subgraph TF["tmpfs"]
TF1["Container Create"] --> TF2["Data in RAM"]
TF2 --> TF3["Container Stop"]
TF3 --> TF4["Data GONE"]
end
end
NV6 -.->|"docker volume rm"| GoneNV["Data GONE"]
BM4 -.->|"rm -rf on host"| GoneBM["Data GONE"]
Backup strategy: sao lưu volume đúng cách
Named volume nằm trong /var/lib/docker/volumes/ — không tiện để backup bằng tool thông thường. Docker cung cấp pattern backup volume bằng container tạm:
# Backup volume pgdata
docker run --rm \
-v pgdata:/data:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/pgdata-backup-$(date +%Y%m%d).tar.gz -C /data .
# Restore vào volume mới
docker run --rm \
-v pgdata_restore:/data \
-v $(pwd):/backup \
alpine \
tar xzf /backup/pgdata-backup-20260618.tar.gz -C /data
Pattern này dùng --rm (xóa container sau khi chạy) và Alpine (image 7MB) làm “tool” để truy cập volume. Volume được mount read-only (:ro) khi backup để tránh thay đổi dữ liệu trong lúc sao lưu.
Backup database volume cần cẩn thận hơn:
# PostgreSQL: dùng pg_dump thay vì tar raw file
docker exec postgres pg_dump -U postgres mydb > backup.sql
# Hoặc chạy container backup riêng
docker run --rm \
--network app_network \
-v $(pwd):/backup \
postgres:16 \
pg_dump -h postgres -U postgres mydb > backup.sql
Copy raw data file của database đang chạy tạo ra backup không nhất quán — trừ khi bạn stop database trước. Luôn dùng tool dump của database (pg_dump, mysqldump, mongodump) thay vì tar data directory.
Automate backup với cron container. Chạy container backup theo lịch, dùng docker run --rm để không để lại container chết:
# Thêm vào crontab (chạy mỗi đêm 2AM)
0 2 * * * docker run --rm -v pgdata:/data:ro -v /backup:/backup alpine tar czf /backup/pgdata-$(date +\%Y\%m\%d).tar.gz -C /data .
Cách này gọn, không cần cài thêm agent backup trên host, và tận dụng Docker volume driver để backup cả remote volume.
Permission: bài toán UID/GID
Đây là vấn đề gây đau đầu nhất khi dùng Docker volume — đặc biệt là bind mount. Permission issue xảy ra vì:
- Image định nghĩa USER riêng. PostgreSQL image chạy process với UID 999 (user
postgres). Node.js image chạy với UID 1000 (usernode). - Host có UID/GID riêng. Bạn trên host là UID 1000, nhưng container chạy UID 999.
- Bind mount giữ nguyên permission từ host. File tạo bởi host UID 1000 không thể bị sửa bởi container UID 999, và ngược lại.
# Ví dụ: bind mount tạo file với permission sai
$ docker run --rm -v $(pwd):/app -w /app node:20 touch test.txt
$ ls -la test.txt
-rw-r--r-- 1 root root 0 Jun 18 10:00 test.txt # root?! Mình là longvo mà?
File được tạo bởi root vì process trong Node.js container mặc định chạy với UID 0 (root). Đây là lý do bạn thấy file root:root trong project sau khi chạy Docker — và IDE của bạn không sửa được file đó.
Cách fix
Cách 1: user trong Compose hoặc docker run --user
# docker-compose.yml
services:
app:
image: node:20
user: "1000:1000" # Match UID/GID của bạn trên host
volumes:
- .:/app
working_dir: /app
# Hoặc với docker run
docker run --rm -u $(id -u):$(id -g) -v $(pwd):/app -w /app node:20 touch test.txt
Cách 2: chown trong entrypoint script
# Dockerfile
FROM node:20
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
# entrypoint.sh
#!/bin/sh
# Fix permission cho mounted volume (nếu cần)
chown -R node:node /app/node_modules 2>/dev/null || true
# Chạy command chính với user node
exec su-exec node "$@"
Cách này linh hoạt hơn — container tự sửa permission khi start. Nhưng có overhead khởi động (chown trên thư mục lớn có thể chậm).
Cách 3: Dùng named volume thay vì bind mount
services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data # Volume, không bind mount
volumes:
pgdata:
Named volume được Docker quản lý và tự động set permission đúng cho user trong image. Đây là cách ít đau đầu nhất cho production.
--privileged để fix permission. --privileged tắt tất cả security isolation — container có thể mount host filesystem, load kernel module, thay đổi network config. Permission issue không đáng để hy sinh toàn bộ security model.Volume driver: mount từ remote storage
Mặc định, Docker dùng local driver — volume nằm trên ổ đĩa host chạy Docker. Nhưng bạn có thể dùng driver khác để mount volume từ NFS server, cloud block storage, hoặc distributed filesystem:
# Tạo volume với driver khác
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=192.168.1.50,rw \
--opt device=:/exports/data \
nfs-volume
# Dùng volume driver cloud (ví dụ: AWS EBS)
docker volume create \
--driver rexray/ebs \
--opt size=100 \
ebs-volume
Một số driver phổ biến:
| Driver | Storage | Dùng khi |
|---|---|---|
local (mặc định) | Host filesystem | Single host, dev |
local + NFS options | NFS server | Multi-host, shared storage |
rexray/ebs | AWS EBS | AWS EC2, cần IOPS cao |
cloudstor/azure | Azure Disk | AKS, Azure VM |
nfs | NFS | Kubernetes, Docker Swarm |
glusterfs | GlusterFS | Distributed storage |
Volume driver và container portability: Volume tạo với driver cloud không portable — bạn không thể docker run container đó trên máy local nếu không có cloud driver. Khi thiết kế Compose file cho multi-environment, dùng local driver cho dev và override driver trong production config.
Tổng kết
Ba mechanism mount storage trong Docker, mỗi cái một vai trò:
- Named volume — production choice. Docker quản lý, backup dễ, hỗ trợ driver, isolate tốt. Dùng cho database, application state, mọi thứ cần persist.
- Bind mount — development choice. Mount thư mục host, hot reload tiện, nhưng permission issue và không portable. Dùng cho source code, config file trong dev.
- tmpfs — temporary choice. Dữ liệu chỉ sống trong RAM, mất khi container stop. Dùng cho cache, session, secrets tạm thời.
Một quy tắc đơn giản: container là stateless. State nằm ở volume. Mỗi khi bạn docker run, hãy tự hỏi: “Dữ liệu của container này cần sống lâu hơn container không?” Nếu câu trả lời là có — mount volume.
Câu hỏi hay gặp
Volume bị xóa khi nào?
Named volume không tự xóa khi bạn xóa container. Nó chỉ bị xóa khi bạn chạy docker volume rm <tên> hoặc docker compose down -v (flag -v xóa anonymous volume, nhưng named volume trong Compose cũng bị ảnh hưởng). Volume “mồ côi” (orphan) — không còn container nào dùng — vẫn tồn tại. Dùng docker volume prune để dọn orphan volume.
Làm sao migrate dữ liệu từ bind mount sang named volume?
# Tạo volume mới
docker volume create new-volume
# Copy dữ liệu từ host path vào volume
docker run --rm \
-v /host/path:/source:ro \
-v new-volume:/dest \
alpine \
cp -a /source/. /dest/
Sau đó update Compose file dùng named volume thay bind mount, restart container.
Bind mount hay named volume cho CI/CD?
Named volume. CI/CD runner thường là container (Docker-in-Docker hoặc Docker socket mount). Bind mount CI workspace vào build container gây ra permission issue liên tục vì UID không khớp. Named volume cô lập permission tốt hơn. Hầu hết CI platform (GitHub Actions, GitLab CI) đều recommend volume hoặc Docker layer cache thay vì bind mount cho build artifact.
tmpfs có encrypt được không?
tmpfs nằm trong RAM — dữ liệu được kernel quản lý, không ghi xuống disk (trừ khi swap). Về lý thuyết, dữ liệu trong tmpfs có thể bị đọc từ /proc/<pid>/mem hoặc qua kernel exploit. Nếu bạn cần encrypt dữ liệu tạm thời, dùng ramfs với dm-crypt hoặc mount encrypted volume thay vì tmpfs thuần. Nhưng với use case thông thường (cache, temp file), tmpfs là đủ an toàn — dữ liệu biến mất khi container stop.
Bài tiếp theo (Dữ liệu & Vận hành): Phần 7: Logging & debugging container, log driver, rotation, exec, và debug container không shell.