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:

  1. 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-recreate là chuyện hàng ngày.
  2. 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.
  3. Không share được dữ liệu. Writable layer của container A không thể thấy được từ container B.
Không dùng --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ể cd và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.
Named volume là lựa chọn mặc định cho production. Khi bạn không có lý do đặc biệt để dùng bind mount, hãy dùng named volume. Nó là happy path của Docker — mọi tính năng backup, driver, Compose đều ưu tiên volume.

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/projects có 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 /etc host vào container là thảm họa.
Đừng bind mount thư mục hệ thống. Chỉ bind mount thư mục project hoặc data directory chuyên dụng. Mount /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 VolumeBind Mount
Ai quản lýDocker daemonBạn
PortabilityCao — chỉ cần tên volumeThấp — phụ thuộc path host
BackupDocker CLI hỗ trợDùng tool host bình thường
Dev workflowBất tiện (không thấy file)Lý tưởng (hot reload)
ProductionNên dùngTránh, trừ khi có lý do chính đáng
DriverHỗ 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. /tmp hoặ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.
tmpfs không tính vào container memory limit. tmpfs dùng kernel’s tmpfs, không tính vào cgroup memory limit của container — nhưng nó vẫn dùng RAM vật lý của host. Nếu container có --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ì:

  1. 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 (user node).
  2. Host có UID/GID riêng. Bạn trên host là UID 1000, nhưng container chạy UID 999.
  3. 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.

Không chạy container với --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:

DriverStorageDùng khi
local (mặc định)Host filesystemSingle host, dev
local + NFS optionsNFS serverMulti-host, shared storage
rexray/ebsAWS EBSAWS EC2, cần IOPS cao
cloudstor/azureAzure DiskAKS, Azure VM
nfsNFSKubernetes, Docker Swarm
glusterfsGlusterFSDistributed 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ò:

  1. 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.
  2. 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.
  3. 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.