jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

9 MIN READ Updated JUL 11, 2026

Đâ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 ở đâuSống qua docker rm?Dùng khi nào
Named volumeThư mục do Docker quản lý trên host (bạn không chọn path) (đến khi docker volume rm)Dữ liệu DB, upload, persistence production
Bind mountMột path cụ thể trên filesystem host của bạn (đó là folder của bạn)Dev live: gắn ./src vào container
tmpfs mountChỉ RAM — không ghi xuống diskKhông (mất khi container dừng)Secret, cache tạm không muốn lên disk
Container /data /app /tmp Named volume Docker-managed Bind mount ./src on host tmpfs RAM only persists ✓ persists ✓ · live edit gone on stop ✗
Three ways to attach storage to a container: a named volume (Docker-managed, persists), a bind mount (your host folder, persists & live-editable), and tmpfs (RAM, gone on stop)

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 portablekhô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ầnNghĩa
$(pwd)Thư mục hiện tại trên host (Windows có thể cần path tuyệt đối)
:/appGắ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_modules lớ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 .env và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--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 /app mà bạn bind-mount folder host rỗng thì container thấy /app rỗ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 USER trong image, quyền folder host, hoặc flag chỉ dev — tùy image.

  • Path host tương đối vs tuyệt đối-v ./data:/data phụ thuộc thư mục hiện tại của shell; $(pwd)/data rõ và an toàn hơn trong script.

  • Ghi đè node_modules — bind-mount root project cũng gắn node_modules trê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 cho node_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 here

2. 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 container

3. 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-file

4. 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 it

5. 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 exists

Nâ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ĩ.