Docker for Developers · Part 3 — Persisting Data: Volumes, Bind Mounts & Env
Làm chủ dữ liệu container với named volume, bind mount, tmpfs; thực hành PostgreSQL bền vững và quản lý cấu hình an toàn.
Đây là Phần 3 của chặng nền tảng về Docker → Compose → Kubernetes. Ở Phần 1 bạn đã biết mỗi container đang chạy có một layer ghi-được mỏng trên các layer image chỉ-đọc; ở Phần 2 bạn đã tự build image của mình.
Mọi thứ bạn tạo hoặc sửa trong layer ghi-được — file database, upload, chỉnh config — chết khi container bị xóa. Ổn với web server stateless; thảm họa với Postgres. Đây là khoảnh khắc Docker bắt đầu giống vận hành hệ thống thật: bạn phải tách rõ runtime tạm thời khỏi state cần sống lâu.
Phần này giải quyết bài toán dữ liệu: file nằm ở đâu trên disk, cách sống sót qua docker rm, cách gắn source code để dev live, và cách truyền cấu hình mà không nhúng secret vào image.
Ba cách gắn storage — named volume vs bind mount vs tmpfs
Docker có thể gắn storage vào container tại đường dẫn bạn chọn. Có ba cơ chế:
| Cơ chế | Dữ liệu ở đâu | Sống qua docker rm? | Dùng khi nào |
|---|---|---|---|
| Named volume | Thư mục do Docker quản lý trên host (bạn không chọn path) | Có (đến khi docker volume rm) | Dữ liệu DB, upload, persistence production |
| Bind mount | Một path cụ thể trên filesystem host của bạn | Có (đó là folder của bạn) | Dev live: gắn ./src vào container |
| tmpfs mount | Chỉ RAM — không ghi xuống disk | Không (mất khi container dừng) | Secret, cache tạm không muốn lên disk |
Quy tắc ngón tay cái: dữ liệu production → named volume; dev local với hot reload → bind mount; scratch nhạy cảm tạm thời → tmpfs.
Góc nhìn senior: mount là hợp đồng ownership. Nếu Docker quản lý dữ liệu, bạn vận hành bằng docker volume và backup volume. Nếu host quản lý dữ liệu, bạn phải chịu trách nhiệm path, permission, performance và chuyện “máy tôi có folder đó không”. Đừng chọn mount chỉ vì lệnh ngắn hơn; chọn theo ai sở hữu dữ liệu và ai backup nó.
Named volume — Docker sở hữu thư mục
Một named volume là “thùng” Docker tạo và quản lý bên dưới (trên Linux, thường dưới /var/lib/docker/volumes/). Bạn tham chiếu bằng tên, không phải path host bạn gõ.
Quản lý volume
docker volume create pgdata # create explicitly (optional — run can auto-create)
docker volume ls # list all volumes
docker volume inspect pgdata # see mountpoint on the host
docker volume rm pgdata # delete one (only when no container uses it)
docker volume prune # delete unused anonymous volumes
docker volume prune --all # include unused named volumes — inspect/backup first
Gắn khi chạy
Cú pháp ngắn (phổ biến nhất):
docker run -d \
--name db \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpine
Ở đây pgdata là tên volume và /var/lib/postgresql/data là nơi Postgres mong đợi file bên trong container. Image Postgres chính thức được build cho path đó — luôn xem docs image để biết mount point đúng.
Cú pháp dài (cùng việc, rõ ràng hơn):
docker run -d \
--name db \
-e POSTGRES_PASSWORD=secret \
--mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
postgres:16-alpine
Ưu tiên named volume khi bạn cần tính portable và không hard-code cấu trúc thư mục laptop — đặc biệt cho database và thứ phải sống qua redeploy.
Bind mount — folder của bạn, bên trong container
Bind mount ánh xạ một path trên host thẳng vào container. Sửa file trên Mac → container thấy thay đổi ngay. Đó là cách bạn có live reload khi dev.
docker run --rm -it \
-v "$(pwd):/app" \
-w /app \
node:24-alpine \
sh
| Phần | Nghĩa |
|---|---|
$(pwd) | Thư mục hiện tại trên host (Windows có thể cần path tuyệt đối) |
:/app | Gắn tại /app bên trong container |
-w /app | Đặt working directory để lệnh chạy trong cây đã gắn |
Chỉ đọc khi container không được sửa source của bạn:
docker run -v "$(pwd)/config:/app/config:ro" myimage
Trên Docker Desktop cho Mac/Windows, bind mount đi qua lớp chia sẻ file — cây
node_moduleslớn có thể chậm hơn Linux native. Named volume cho dependency là fix phổ biến (xem pitfalls bên dưới).
Giữ database — chứng minh dữ liệu sống qua docker rm
Làm ví dụ Postgres trọn vẹn.
1. Khởi động Postgres với named volume:
docker volume create pgdata
docker run -d \
--name mydb \
-e POSTGRES_USER=dev \
-e POSTGRES_PASSWORD=devpass \
-e POSTGRES_DB=appdb \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpine
2. Tạo bảng và chèn một dòng:
docker exec -it mydb psql -U dev -d appdb -c \
"CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, body text);
INSERT INTO notes (body) VALUES ('survives container death');"
3. Xóa container — dữ liệu ở lại trong volume:
docker stop mydb
docker rm mydb
docker ps -a # mydb is gone
4. Khởi động container mới trên cùng volume:
docker run -d \
--name mydb2 \
-e POSTGRES_USER=dev \
-e POSTGRES_PASSWORD=devpass \
-e POSTGRES_DB=appdb \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpine
docker exec -it mydb2 psql -U dev -d appdb -c "SELECT * FROM notes;"
Bạn vẫn thấy survives container death. Container là dùng-một-lần; volume thì không.
docker rm mydb volume pgdata still on disk
│ │
▼ ▼
(container gone) ┌──────────────┐
│ notes table │
└──────────────┘
docker run ... -v pgdata:... ──► mydb2 sees the same files
Biến môi trường & cấu hình
Container là artifact bất biến; cấu hình nên inject lúc chạy, không nên “nướng” vào image.
Trực tiếp -e
docker run -e NODE_ENV=production -e PORT=3000 myapp
--env-file
Đặt default không phải secret trong file (mỗi dòng KEY=value):
# .env (example — never commit real secrets)
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
docker run --env-file .env myapp
Docker truyền các biến đó vào môi trường tiến trình container. App đọc chúng giống khi chạy trên bare metal.
Secret — đừng nhúng vào image
- Không bao giờ
ENV DATABASE_PASSWORD=...trong Dockerfile cho credential thật — layer tồn tại mãi; ai có image đều có thể inspect. - Thêm
.envvào.gitignoređể secret local không lên repo. - Cách đưa secret production (Docker secrets, Vault, cloud parameter store) sâu hơn — sẽ quay lại ở Phần 7 của series.
Dev local thì -e và --env-file đủ; coi .env như export từ password manager, không phải source code.
Các bẫy thường gặp
-
Gắn đè lên thư mục không rỗng che nội dung image — image đã có file tại
/appmà bạn bind-mount folder host rỗng thì container thấy/apprỗng. Gắn vào subpath hoặc đảm bảo folder host có đủ thứ cần. -
Anonymous volume chồng chất —
-v /var/lib/mysql(không tên) tạo volume ngẫu nhiên Docker không tái dùng trừ khi bạn biết hash. Ưu tiên volume có tên:-v mydata:/var/lib/mysql. -
Lệch quyền bind mount — tiến trình trong container có thể chạy UID khác user host; có thể gặp “Permission denied” khi ghi. Fix gồm chỉnh
USERtrong image, quyền folder host, hoặc flag chỉ dev — tùy image. -
Path host tương đối vs tuyệt đối —
-v ./data:/dataphụ thuộc thư mục hiện tại của shell;$(pwd)/datarõ và an toàn hơn trong script. -
Ghi đè
node_modules— bind-mount root project cũng gắnnode_modulestrên host (hoặc thiếu) đè lên module Linux build trong container. Pattern phổ biến: bind app, nhưng dùng named volume chonode_modules:
docker run -v "$(pwd):/app" -v app_modules:/app/node_modules -w /app node:24-alpine npm install
Mount thứ hai “che” /app/node_modules để install nằm trong volume Docker, không phải cây trên Mac.
Bảng tra nhanh
# volumes
docker volume create mydata
docker volume ls
docker volume inspect mydata
docker volume rm mydata
docker volume prune # unused anonymous volumes
docker volume prune --all # also unused named volumes — destructive
# run with storage
docker run -v mydata:/var/lib/postgresql/data postgres:16-alpine
docker run -v "$(pwd):/app" -w /app node:24-alpine
docker run -v "$(pwd)/config:/app/config:ro" myimage
docker run --mount type=volume,source=mydata,target=/data myimage
# environment
docker run -e KEY=value myimage
docker run --env-file .env myimage
docker exec mycontainer env # inspect what's set inside
Bài tập / Exercises
Làm theo thứ tự; bạn sẽ dùng volume và biến môi trường liên tục ở Compose và Kubernetes sau này.
1. Tạo named volume ex-pgdata, chạy Postgres 16 với user ex, password ex, database exdb, gắn volume, chèn một dòng vào bảng ping, rồi docker rm container và khởi động container mới trên cùng volume — chứng minh dòng vẫn còn.
Lời giải
docker volume create ex-pgdata
docker run -d --name exdb1 \
-e POSTGRES_USER=ex -e POSTGRES_PASSWORD=ex -e POSTGRES_DB=exdb \
-v ex-pgdata:/var/lib/postgresql/data \
postgres:16-alpine
docker exec exdb1 psql -U ex -d exdb -c \
"CREATE TABLE ping (msg text); INSERT INTO ping VALUES ('still here');"
docker stop exdb1 && docker rm exdb1
docker run -d --name exdb2 \
-e POSTGRES_USER=ex -e POSTGRES_PASSWORD=ex -e POSTGRES_DB=exdb \
-v ex-pgdata:/var/lib/postgresql/data \
postgres:16-alpine
docker exec exdb2 psql -U ex -d exdb -c "SELECT * FROM ping;"
# still here2. Trong folder trống trên host, tạo hello.txt với nội dung bất kỳ. Chạy container Alpine gắn folder đó vào /work, sửa file bên trong container, rồi cat hello.txt trên host — hai phía phải khớp.
Lời giải
mkdir -p /tmp/docker-ex3 && cd /tmp/docker-ex3
echo "from host" > hello.txt
docker run --rm -it -v "$(pwd):/work" -w /work alpine sh -c \
"echo 'from container' >> hello.txt && cat /work/hello.txt"
cat hello.txt
# from host
# from container3. Viết file .env với GREETING=hello-from-file và chạy alpine với --env-file .env, in biến bằng sh -c 'echo $GREETING'.
Lời giải
cd /tmp/docker-ex3
printf 'GREETING=hello-from-file\n' > .env
docker run --rm --env-file .env alpine sh -c 'echo $GREETING'
# hello-from-file4. Liệt kê mọi volume, nhận ra ex-pgdata, chạy docker volume inspect ex-pgdata và ghi nhớ path Mountpoint trên host.
Lời giải
docker volume ls
docker volume inspect ex-pgdata
# "Mountpoint": "/var/lib/docker/volumes/ex-pgdata/_data" (Linux)
# on Docker Desktop Mac, path is inside the Linux VM — inspect still shows it5. Dừng và xóa container bài tập, rồi xóa đúng named volume ex-pgdata. Dùng lệnh có target rõ thay vì prune diện rộng.
Lời giải
docker stop exdb2 2>/dev/null; docker rm exdb2 2>/dev/null
docker volume inspect ex-pgdata
docker volume rm ex-pgdata
docker volume ls # ex-pgdata no longer existsNâng cao: Chạy container Node vừa bind $(pwd) vào /app vừa named volume tại /app/node_modules; chạy npm init -y && npm install lodash bên trong, xóa container, khởi động lại với cùng mount, và xác nhận node_modules còn mà không cần install đầy đủ trên host.
Điểm chính
- Layer ghi-được của container là tạm thời — lên kế hoạch persistence rõ ràng.
- Named volume cho dữ liệu phải sống qua thay container (DB, upload).
- Bind mount cho workflow dev — editor trên host, app trong container.
- tmpfs khi không muốn persistence trên disk.
- Inject config bằng
-e/--env-file; giữ secret khỏi image và khỏi git.
Tiếp theo
Phần 4 — Networking: Bridge, Ports & Service Discovery: container tìm nhau thế nào, publish port, và vì sao localhost trong container không như bạn nghĩ.