Hình dung trong tường có một cái hốc, và nhà sản xuất đã bày sẵn ít đồ mặc định trong đó. Giờ bạn muốn "gắn kho riêng" vào đúng cái hốc ấy, và cách bạn gắn quyết định số phận mấy món đồ có sẵn. Nếu bạn úp vào một ngăn kéo rỗng, người ta đổ mấy món mặc định sang ngăn kéo của bạn trước đã — nên nó không trống. Nếu bạn vặn thẳng một tấm phẳng kín che lên hốc, mấy món mặc định vẫn nằm đó nhưng bị khuất, coi như biến mất. Còn nếu ngăn kéo bạn úp vào đã có đồ sẵn, họ để yên, không đổ thêm gì. Ba kiểu lưu trữ của Docker — volume, bind mount, tmpfs — cư xử đúng như ba cách gắn ấy, và bài này đo từng cái.

Docker có ba cách đưa dữ liệu vào container, và chúng khác nhau ở nhiều chỗ hơn là chỗ lưu trữ. Bắt đầu bằng câu hỏi cơ bản: gắn vào rồi thì bên trong container là hệ thống tệp gì?

Gắn kiểu Hệ thống tệp bên trong
Volume có tên ext4
Volume ẩn danh ext4
Bind mount (Docker Desktop macOS) fakeowner
--tmpfs tmpfs

Dòng thứ ba đã nói trước rất nhiều điều. Trên Docker Desktop, thư mục của bạn không nằm trong máy ảo Linux, nên Docker phải dựng một lớp trung gian để đưa nó vào. Trên máy chủ Linux thật thì bind mount là chính hệ thống tệp của máy, không có lớp nào cả.

Và Docker ghi nhận chúng khác nhau:

volume  /var/lib/docker/volumes/v-ten/_data                -> /du-lieu
volume  /var/lib/docker/volumes/5e318e2dcb12.../_data      -> /anon-vol
bind    /host_mnt/private/tmp/.../thumuc                   -> /gan-thu-muc

tmpfs không xuất hiện trong .Mounts — nó nằm ở HostConfig.Tmpfs. Nếu bạn viết script duyệt .Mounts để kiểm kê dữ liệu, tmpfs sẽ vô hình.

Cái gì sống sót khi container bị xoá

Tôi ghi một tệp vào từng nơi rồi docker rm -f:

Nơi ghi Sau khi xoá container
Volume có tên còn
Volume ẩn danh còn (nếu bạn nhớ được tên băm của nó)
Bind mount còn
tmpfs mất
Lớp ghi của container mất

Volume ẩn danh (-v /duong-dan không kèm tên) là cái bẫy dọn dẹp: nó sống sót nhưng mang tên kiểu 5e318e2dcb12d772201ce3392f96b597f24d09c1366481a79963108d85bf4bf0, và không ai biết nó của container nào. Sau vài tháng, docker volume ls của bạn đầy những cái tên như vậy.

docker volume ls -qf dangling=true | wc -l    # dem volume mo coi
docker volume prune                            # xoa chung

Khác biệt lớn nhất: cái gì xảy ra khi thư mục đích đã có sẵn nội dung

Image nginx có sẵn /etc/nginx/conf.d/default.conf. Tôi gắn ba thứ khác nhau vào đúng chỗ đó:

Gắn gì Bên trong thấy gì
Volume rỗng default.conf — Docker chép nội dung của image vào volume
Thư mục rỗng của máy chủ (rỗng) — nội dung image bị che mất
Volume đã có dữ liệu dữ liệu của volume; không chép gì thêm

Đây là hành vi khác nhau rõ rệt và rất hay gây bất ngờ — chính là ba cách gắn vào cái hốc tường ở trên.

Volume rỗng thì được nạp mồi. Lần đầu gắn, Docker chép nội dung sẵn có của thư mục trong image vào volume (đổ đồ mặc định sang ngăn kéo rỗng). Nhờ vậy -v pgdata:/var/lib/postgresql/data chạy được ngay: PostgreSQL thấy đúng cấu trúc thư mục mà image chuẩn bị.

Bind mount thì không bao giờ chép. Nó che thẳng lên (tấm phẳng kín úp lên hốc), giống hệt --tmpfs ở phần 30. Gắn một thư mục rỗng lên thư mục cấu hình là ứng dụng mất sạch cấu hình mặc định — chạy được nhưng không phục vụ gì.

Và việc nạp mồi chỉ xảy ra một lần. Volume đã có dữ liệu thì Docker không đụng vào. Nghĩa là bạn cập nhật image với cấu hình mặc định mới, volume cũ vẫn giữ bản cũ, và bạn không hiểu vì sao thay đổi không có tác dụng.

Ai sở hữu tệp

trong image, /kho thuoc ve   : 1000:1000
gan volume moi vao /kho      : 1000:1000
gan thu muc may chu vao /kho : 0:0

Volume thừa hưởng cả quyền sở hữu của thư mục trong image — nên ứng dụng chạy bằng người dùng 1000 vẫn ghi được ngay. Đây là lý do volume "vừa vặn" hơn bind mount khi bạn chạy không phải root (phần 12).

Bind mount thì mang quyền của thư mục trên máy chủ. Trên macOS, lớp fakeowner làm cho quyền luôn khớp nên bạn không bao giờ gặp vấn đề. Trên Linux thì có — đó là lý do kinh điển của Permission denied khi bind mount thư mục vào một container chạy bằng người dùng khác uid.

-v và --mount không tương đương

-v      voi thu muc chua ton tai  -> Docker TU TAO thu muc do
--mount voi thu muc chua ton tai  -> Error: bind source path does not exist

-v im lặng tạo giúp bạn một thư mục rỗng. Nghe tiện, nhưng khi bạn gõ sai đường dẫn — thiếu một ký tự, sai chữ hoa thường — nó tạo một thư mục rỗng mới thay vì báo lỗi, và ứng dụng khởi động với dữ liệu trống trơn.

--mount dài dòng hơn nhưng thất bại ngay lập tức. Với dữ liệu quan trọng, đó là điều bạn muốn.

Cả hai đều nhận :ro (hoặc readonly), và cả hai đều chặn ghi thật:

sh: can't create /du-lieu/z: Read-only file system

Tốc độ ghi tuần tự — không khác nhau như lời đồn

256 MB, conv=fsync, trên Docker Desktop macOS:

Đích Tốc độ
tmpfs 3,2 GB/s
Lớp ghi container 1,7 GB/s
Volume 993,7 MB/s
Bind mount 966,6 MB/s

Volume và bind mount gần như bằng nhau ở phép đo này — chênh 2,7%, nằm trong nhiễu. Điều đó đi ngược hẳn với danh tiếng "bind mount trên macOS chậm kinh khủng".

Danh tiếng đó không sai, nó chỉ nói về một loại tải khác: nhiều tệp nhỏ và thao tác siêu dữ liệu, chứ không phải ghi tuần tự. Phần sau đo đúng chỗ đó, và con số ở đấy mới thật sự đáng nói.

tmpfs nhanh nhất vì nó là RAM — và như phần 30 đã đo, nó tính vào --memory và biến mất khi container dừng.

Chọn cái nào

Việc Dùng
Dữ liệu của ứng dụng (CSDL, tệp tải lên) Volume có tên
Mã nguồn khi đang phát triển Bind mount
Tệp cấu hình một chiều, chỉ đọc Bind mount :ro
Tệp tạm, bộ đệm, thư mục cần rỗng lúc khởi động tmpfs
Bí mật lúc chạy tmpfs (đừng bao giờ ghi xuống đĩa)

Và đừng dùng volume ẩn danh cho gì cả — luôn đặt tên.

Tự kiểm trên máy bạn

Xem bạn đang có bao nhiêu dữ liệu mồ côi:

docker volume ls -qf dangling=true | wc -l
docker system df -v | head -20

Và kiểm tra một container quan trọng đang gắn những gì:

docker inspect <ten> --format \
  '{{range .Mounts}}{{.Type}}  {{.Source}} -> {{.Destination}} (rw={{.RW}}){{println}}{{end}}'

Nếu thấy volume với Source là một chuỗi băm dài, đó là volume ẩn danh — dữ liệu thật đang nằm ở một cái tên không ai đọc được, và docker volume prune sẽ xoá nó ngay khi container biến mất.

Mẫu số chung

Bài học đầu tiên, chính là cái hốc tường đã có sẵn đồ: hành vi thú vị nhất của một phép "gắn đè" nằm ở lúc chỗ đích không rỗng — có kiểu nạp mồi một lần rồi thôi, có kiểu che khuất thứ bên dưới, và cả hai đều âm thầm khiến cập nhật về sau của bạn không có tác dụng. Volume rỗng được chép mồi đúng một lần, rồi mãi giữ bản cũ dù image đổi; bind mount thì che thẳng, cấu hình mặc định vẫn còn đó nhưng khuất mất. Cùng hai cái bẫy "khởi tạo một lần" và "lớp trên che lớp dưới" ở khắp nơi: một tệp cấu hình chép vào thư mục người dùng ở lần chạy đầu rồi các bản cập nhật sau không đụng tới nữa; dữ liệu seed hay mốc migration của CSDL chỉ chạy một lần nên sửa seed chẳng thay đổi gì database đã dựng; trong Java, một tệp application.properties bên ngoài che mặc định đóng trong jar, hay một trường ở lớp con ẩn trường trùng tên của lớp cha; một biến môi trường đặt sau đè biến đặt trước mà bạn quên. Nguyên tắc: trước khi gắn/ghi đè lên một chỗ đã có nội dung, hỏi rõ "khi đích không rỗng thì điều gì xảy ra" — vì nạp-mồi-một-lần nghĩa là thay đổi ở nguồn sẽ không lan tới, còn che khuất nghĩa là cái gốc vẫn nằm đó chờ gây bất ngờ; cả hai đều là lỗi "sao sửa mà không ăn" kinh điển.

Điều thứ hai, đọc từ -v tự tạo thư mục còn --mount báo lỗi: cái mặc định tiện tay thường che mất lỗi — nó lặng lẽ làm một việc "nghe hợp lý" khi bạn gõ sai, thay vì dừng lại; với thứ quan trọng, hãy chọn công cụ thất bại to tiếng. Gõ sai đường dẫn, -v dựng một thư mục rỗng và ứng dụng khởi động với dữ liệu trắng; --mount thì ném lỗi ngay. Cùng cái bẫy "im lặng làm bừa" ở khắp nơi: một bộ nạp cấu hình trả về rỗng cho khoá gõ sai thay vì báo lỗi; mkdir -p nuốt mất một đường dẫn sai; một ORM tự tạo bảng còn thiếu khiến schema lệch mà không ai hay; một trình phân tích dễ dãi nhận nhầm tên trường rồi lặng lẽ lưu thành "không rõ nguyên nhân". Nguyên tắc: với dữ liệu và thao tác không thể sai, hãy ưu tiên dạng nghiêm khắc, hỏng-là-dừng hơn dạng đoán ý và tự lo — một cú gõ nhầm phải chặn bạn lại ngay, chứ không được trở thành một tập dữ liệu rỗng chạy êm ru cho tới khi hậu quả lộ ra ở tận đâu đó.

Phần sau đo I/O kỹ: nhiều tệp nhỏ, fsync, thao tác siêu dữ liệu — trên cả macOS lẫn Linux.