Docker Compose: bind mount hay named volume?
Phân biệt bind mount và named volume trong Docker Compose để chọn đúng cho config, database và media, tránh lỗi permission, đồng thời biết cách inspect, backup và migrate.
Bind mount hoặc named volume: câu trả lời ngắn gọn
Volume trong Docker Compose có 2 loại. Lựa chọn phụ thuộc vào bên nào quản lý các file. Dùng bind mount cho các file bạn tự ghi và đọc, chẳng hạn file cấu hình, template và static site. Dùng named volume cho dữ liệu do ứng dụng quản lý, chẳng hạn file database, search index và media đã upload. Bind mount trỏ đến một path trên host mà bạn có thể mở bằng editor. Named volume là vùng lưu trữ do Docker tạo và theo dõi, và bạn truy cập vùng này thông qua Docker.
Cả hai đều nằm dưới cùng key volumes: bên trong một service, nên dễ bị nhầm lẫn. Điểm khác biệt nằm ở phía bên trái dấu hai chấm. Phần bên trái bắt đầu bằng . hoặc / là host path, nên đó là bind mount. Mọi giá trị khác là một name, nên đó là named volume. Name đó cũng phải được khai báo trong block volumes: ở cấp cao nhất.
Hai cú pháp trong file Compose
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data là một named volume. ./nginx.conf:/etc/nginx/nginx.conf là một bind mount, còn :ro mount nó ở chế độ chỉ đọc. Đây là mặc định phù hợp cho cấu hình mà container không bao giờ được phép ghi đè. Nếu quên mục volumes: ở cấp cao nhất, Compose sẽ dừng với lỗi service "db" refers to undefined volume pgdata.
Khởi động và liệt kê những gì Docker đã tạo:
docker compose up -d
docker volume lsVolume không có tên pgdata. Tên của nó là <project>_pgdata, trong đó tên project mặc định là tên của thư mục chứa file Compose. Thư mục có tên myapp sẽ tạo ra myapp_pgdata. Điều này quan trọng vì khi đổi tên thư mục, bạn sẽ nhận một volume trống mới và ứng dụng trông như đã mất dữ liệu. Thực tế không phải vậy: volume cũ vẫn được liệt kê bằng docker volume ls. Gán tên cố định bằng name: trong file Compose, hoặc đặt COMPOSE_PROJECT_NAME nếu thư mục có thể được di chuyển. Những thiết lập như vậy nên được đặt cùng các file môi trường và secret của Compose khác của bạn.
Vì sao lỗi quyền chỉ xảy ra với bind mount
Đây là khác biệt thực tế lớn nhất. Nguyên nhân nằm ở một quy tắc: named volume còn trống trong lần đầu sử dụng sẽ được khởi tạo từ image, còn bind mount thì không.
Khi Docker mount một named volume còn trống lên một thư mục đã có nội dung trong image, nó sao chép nội dung đó vào volume, cùng ownership và mode mà image đã thiết lập. Image Postgres chính thức cung cấp /var/lib/postgresql/data với ownership thuộc user postgres riêng của nó. Vì vậy, volume được tạo với cùng numeric id đó và database khởi động bình thường.
Bind mount hoạt động ngược lại. Container sẽ thấy đúng nội dung trên host, bao gồm cả ownership, còn nội dung của image tại đường dẫn đó sẽ bị che đi. Nếu thư mục trên host không tồn tại, Docker daemon sẽ tạo thư mục đó. Daemon chạy dưới root, nên thư mục sẽ có ownership là root:root. Sau đó, process trong container chạy dưới user không phải root sẽ không thể ghi vào thư mục:
PermissionError: [Errno 13] Permission denied: '/data/app.db'Cách sửa là làm cho các numeric id khớp nhau. Ownership qua bind mount được đối chiếu bằng numeric user id, không phải bằng tên, vì container có /etc/passwd riêng. User có tên app bên trong container không có ý nghĩa gì trên host. Uid 1000 có nghĩa là uid 1000 ở cả hai phía.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id in ra uid mà process trong container thực sự chạy dưới đó. Hãy đổi ownership của thư mục trên host cho khớp với uid này, hoặc cố định container chạy dưới uid của bạn bằng user: "1000:1000" trong service. Cố định user: phù hợp hơn với ứng dụng do bạn tự viết. Đổi ownership của thư mục trên host an toàn hơn với image do người khác viết, vì một số image khởi chạy entrypoint dưới root, sau đó hạ quyền và yêu cầu ownership cụ thể cho các thư mục bên dưới.
Có thêm hai bẫy cần biết. Trên Fedora, RHEL và các hệ thống khác đang enforcing SELinux (security-enhanced Linux), bind mount sẽ bị từ chối cho đến khi được relabel. Vì vậy, hãy thêm :z cho đường dẫn được chia sẻ giữa các container hoặc :Z cho đường dẫn chỉ một container sử dụng, với cú pháp - ./data:/data:Z. Ngoài ra, bind mount một file đơn lẻ thay vì một thư mục sẽ gặp lỗi khi editor thay thế file thay vì ghi trực tiếp vào file hiện tại, vì mount bám theo inode ban đầu. Container vẫn thấy nội dung cũ cho đến khi bạn restart nó. Nếu file thường xuyên được chỉnh sửa, hãy mount thư mục chứa file đó.
Hiệu năng: khác biệt thực sự nằm ở đâu
Trên Linux server, cả hai loại đều đi qua cùng một đường xử lý của kernel. Vì vậy, chênh lệch throughput nhỏ đến mức bạn không nên chọn dựa trên yếu tố này. Named volume dùng driver mặc định local nằm trên cùng filesystem với phần còn lại của Docker, bên dưới /var/lib/docker/volumes/. Bind mount nằm ở vị trí bạn đã chỉ định.
Khác biệt xuất hiện trên Docker Desktop cho macOS và Windows, nơi container chạy bên trong một virtual machine. Bind mount tại đó phải đi từ filesystem của host vào virtual machine thông qua một lớp file sharing. Các workload thực hiện nhiều thao tác với file nhỏ, như cây dependency của Node.js hoặc cache của PHP framework, sẽ chậm đi rõ rệt. Named volume nằm bên trong virtual machine nên không chịu chi phí này. Vì vậy, nhiều file compose dùng cho development bind-mount thư mục source nhưng khai báo named volume cho node_modules.
Khác biệt thực tế khác là vị trí lưu dữ liệu. Bind mount đến /mnt/backup sẽ ghi dữ liệu lên disk đó. Named volume ghi dữ liệu lên filesystem chứa /var/lib/docker, thường là root disk trên VPS. Database tăng trưởng bên trong named volume sẽ chiếm cùng disk đang chứa log của hệ thống. Hãy kiểm tra trước khi xảy ra incident:
docker system df -v
df -h /var/lib/dockerdocker system df -v liệt kê mọi volume cùng kích thước của chúng và đánh dấu những volume không còn được container nào tham chiếu.
Kiểm tra named volume
Named volume không phải là một hộp đen. Hãy hỏi Docker volume này nằm ở đâu:
docker volume inspect myapp_pgdataTrường Mountpoint cho biết đường dẫn thật trên host, thường là /var/lib/docker/volumes/myapp_pgdata/_data. Bạn có thể đọc volume bằng sudo ls; cách này hữu ích để kiểm tra nhanh. Không được xem đây là nơi để chỉnh sửa file. Ghi file tại đó bằng root sẽ tạo lại vấn đề ownership đã mô tả ở trên. Ngoài ra, đường dẫn này là chi tiết riêng của driver local, các volume driver khác không dùng chung.
Cách an toàn để xem nội dung volume là dùng một container tạm thời để mount volume:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volCách này hoạt động với mọi driver, cho cùng permission mà container thật nhìn thấy, và không để lại gì nhờ --rm.
Sao lưu từng loại
Bind mount là một thư mục thông thường, nên mọi công cụ sao lưu cấp file đều đã xử lý được nó. Trỏ công cụ sao lưu vào đường dẫn trên host là xong. Named volume cần thêm một bước vì công cụ phải truy cập vào bên trong volume. Mount volume và một thư mục trên host vào cùng một container tồn tại trong thời gian ngắn, rồi ghi một archive:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .Khôi phục bằng cách thực hiện ngược lại vào một volume mới:
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar giữ nguyên ownership dạng số khi chạy với quyền root bên trong container. Đây là yếu tố giúp volume được khôi phục tiếp tục hoạt động với ứng dụng.
Có một cảnh báo áp dụng cho cả hai loại. Sao chép file của database khi database đang chạy sẽ tạo archive từ dữ liệu đang thay đổi và có thể khôi phục thành trạng thái bị hỏng. Hãy dừng service trước hoặc dump bằng công cụ riêng của database, như trong docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. Cách này tạo ra một file thông thường, sau đó bạn có thể đưa file đó vào quy trình sao lưu restic được mã hóa thông thường cùng với các file compose.
Di chuyển bind mount sang named volume
Thao tác này là sao chép, không phải đổi tên, và mất khoảng một phút.
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a giữ nguyên owner, quyền và timestamp. Vì vậy, user trong container đã đọc được thư mục cũ thì vẫn đọc được volume mới. Sau đó, đổi service sang dùng pgdata:/var/lib/postgresql/data, thêm pgdata vào block volumes: cấp cao nhất, chạy docker compose up -d và đọc log của ứng dụng trước khi xóa thư mục cũ. Muốn làm theo chiều ngược lại, dùng cùng lệnh đó nhưng đổi chỗ /from và /to.
Trong lúc kiểm thử, hãy nhớ một điều. docker compose down không động đến các named volume, còn docker compose down -v sẽ xóa mọi named volume mà project khai báo và không thể hoàn tác. Bind mount vẫn tồn tại sau cả hai lệnh vì Docker chưa từng quản lý thư mục đó. Nếu các lệnh quản lý vòng đời này còn mới với bạn, hướng dẫn cơ bản về Docker Compose cho VPS sẽ trình bày từng bước.
Chọn theo từng service
Hãy xác định ai ghi vào file. Cấu hình do bạn chỉnh bằng text editor và commit vào git nên đặt trong bind mount, được mount :ro, vì bạn muốn nhìn thấy và version hóa nó. Trạng thái ứng dụng mà bạn không bao giờ mở thủ công nên đặt trong named volume, vì Docker thiết lập permission đúng cách và dữ liệu không phụ thuộc vào một path trên host.
Trường hợp kết hợp là media. Photo library do ứng dụng ghi nhưng cũng do bạn quản lý, và thường đủ lớn để cần một disk riêng. Hãy bind mount nó vào một path trên disk đó, rồi thiết lập ownership một lần theo cách có chủ đích. Đây là pattern mà hầu hết self-hosted stack sử dụng: named volume cho database và cache, bind mount cho config và cho directory lớn mà bạn quan tâm. Một support desk như Chatwoot chạy trên VPS cũng phù hợp chính xác với cách này: Postgres nằm trong named volume, còn uploaded attachment nằm trên một path mà bạn có thể chỉ định cho công việc backup.
FAQ
Bind mount và named volume khác nhau như thế nào?
Bind mount ánh xạ một đường dẫn trên host vào container, nên cả hai phía cùng nhìn thấy một thư mục và bạn có thể chỉnh sửa thư mục đó bằng các công cụ thông thường. Named volume là vùng lưu trữ do Docker tạo và quản lý, được tham chiếu bằng tên và khai báo trong block volumes: cấp cao nhất. Điểm khác biệt thực tế là quyền sở hữu: dùng bind mount cho cấu hình bạn tự quản lý, dùng named volume cho dữ liệu do ứng dụng quản lý.
Vì sao tôi gặp lỗi "permission denied" với bind mount nhưng không gặp lỗi này với named volume?
Named volume rỗng được khởi tạo từ image, nên kế thừa quyền sở hữu mà image đã thiết lập và user trong container có thể ghi vào đó. Bind mount hiển thị chính xác thư mục trên host; nếu Docker phải tạo thư mục đó thì thư mục sẽ thuộc về root. Chạy docker compose exec <service> id để xem numeric id mà container sử dụng, sau đó chạy sudo chown -R <uid>:<gid> trên thư mục của host, hoặc đặt user: "1000:1000" cho service.
Docker lưu named volume trên disk ở đâu?
Với driver local mặc định, chúng nằm dưới /var/lib/docker/volumes/<volume>/_data, và docker volume inspect <volume> sẽ in ra Mountpoint chính xác. Hãy đọc đường dẫn này nếu cần kiểm tra, nhưng chỉ ghi vào đó thông qua container, vì chỉnh sửa với quyền root trên host có thể thay đổi quyền sở hữu theo cách container không mong đợi.
Làm cách nào để backup named volume?
Chạy một container tồn tại trong thời gian ngắn, mount cả volume và thư mục trên host vào container, sau đó archive từ bên này sang bên kia bằng docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. Với database, hãy dump bằng công cụ riêng của database thay vì sao chép các file đang hoạt động, vì bản sao được tạo khi các thao tác ghi đang diễn ra có thể được khôi phục thành trạng thái hỏng.
docker compose down có xóa volume của tôi không?
docker compose down xóa các container và network nhưng giữ nguyên named volume. docker compose down -v cũng xóa vĩnh viễn mọi named volume mà project khai báo. Cả hai lệnh đều không xóa bind mount, vì thư mục đó thuộc về host chứ không thuộc về Docker.