SSD Nodes Learn 8GB RAM — $66/năm
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-01

Docker Compose: bind mount hay named volume?

Phân biệt bind mount và named volume trong Docker Compose, tránh lỗi quyền file, rồi biết cách kiểm tra, backup và migrate config cùng dữ liệu.

Bind mount hay named volume: câu trả lời ngắn

Volume trong Docker Compose có 2 loại. Lựa chọn phụ thuộc vào bên 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à site tĩnh. Dùng named volume cho dữ liệu do ứng dụng quản lý, chẳng hạn file cơ sở dữ liệu, search index và media đã upload. Bind mount trỏ đến một path trên host mà bạn có thể mở bằng trình soạn thảo. Named volume là vùng lưu trữ do Docker tạo và quản lý; bạn truy cập vùng này thông qua Docker.

Cả hai đều nằm dưới cùng key volumes: trong một service, nên chúng thường 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ía bên trái bắt đầu bằng . hoặc / là một host path, nên đó là bind mount. Bất kỳ giá trị nào khác đều là một tên, nên đó là named volume; tên này cũng phải được khai báo trong block volumes: cấp cao nhất.

Hai cú pháp trong tệp 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 ghi đè. Nếu bỏ 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 các dịch vụ và liệt kê những gì Docker đã tạo:

docker compose up -d
docker volume ls

Volume này 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 tệp 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 có vẻ như đã mất dữ liệu. Thực tế không phải vậy: volume cũ vẫn được docker volume ls liệt kê. Ghim tên bằng name: trong tệp compose, hoặc đặt COMPOSE_PROJECT_NAME, nếu thư mục có thể được di chuyển. Các cài đặt như vậy nên đặt cùng với các tệp 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 là một quy tắc: named volume trống ở 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 trống lên một thư mục đã có nội dung trong image, Docker sao chép nội dung đó vào volume, cùng ownership và mode do image thiết lập. Official Postgres image cung cấp /var/lib/postgresql/data thuộc về 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.

Bind mount hoạt động ngược lại. Container sẽ thấy mọi thứ có trên host, bao gồm cả ownership, còn nội dung của image tại đường dẫn đó sẽ bị che khuất. 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 được tạo sẽ thuộc về 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 số này 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 bên.

id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./data

docker compose exec web id in ra uid mà process trong container thực sự sử dụng. Hãy đổi ownership của thư mục trên host cho khớp với số đó, hoặc cố định container chạy bằng uid của bạn bằng user: "1000:1000" trong service. Cố định user: phù hợp hơn với application do bạn tự viết. Đổi ownership của thư mục trên host an toàn hơn khi dùng image do người khác viết, vì một số image khởi chạy entrypoint bằng root, sau đó hạ quyền và yêu cầu ownership cụ thể ở 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 có SELinux (security-enhanced Linux) ở chế độ enforcing, bind mount sẽ bị từ chối cho đến khi được relabel. Vì vậy, hãy thêm :z cho path được chia sẻ giữa các container hoặc :Z cho path chỉ một container được dùng, với cú pháp - ./data:/data:Z. Ngoài ra, bind mount một file riêng lẻ thay vì một thư mục sẽ bị lỗi khi editor thay thế file thay vì ghi trực tiếp vào file, vì mount gắn với inode ban đầu. Container vẫn thấy nội dung cũ cho đến khi bạn restart nó. Khi file thường xuyên được chỉnh sửa, hãy mount thư mục cha.

Hiệu năng: điểm khác biệt thực sự nằm ở đâu

Trên máy chủ Linux, cả hai loại đều đi qua cùng một đường xử lý của kernel. Vì vậy, chênh lệch thông lượng đủ nhỏ để 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, trong /var/lib/docker/volumes/. Bind mount nằm ở vị trí bạn chỉ định.

Điểm 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 máy ảo. Bind mount ở đó phải đi từ filesystem của máy host vào máy ảo thông qua một lớp chia sẻ file. Các workload thực hiện nhiều thao tác với file nhỏ, chẳng hạn cây dependency của Node.js hoặc cache của framework PHP, sẽ chậm đi rõ rệt. Named volume nằm bên trong máy ảo 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 mã nguồn nhưng khai báo một named volume cho node_modules.

Khác biệt thực tế còn lại là vị trí lưu dữ liệu. Bind mount tới /mnt/backup 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 phát triển bên trong named volume sẽ làm đầy cùng disk đang chứa log hệ thống. Hãy kiểm tra trước khi xảy ra sự cố:

docker system df -v
df -h /var/lib/docker

docker system df -v liệt kê mọi volume kèm kích thước và đánh dấu các volume không còn được container nào tham chiếu.

Kiểm tra volume có tên

Volume có tên không phải là một hộp đen. Dùng Docker để xem volume nằm ở đâu:

docker volume inspect myapp_pgdata

Trường Mountpoint cung cấp đường dẫn thật trên host, thường là /var/lib/docker/volumes/myapp_pgdata/_data. Bạn có thể đọc trường này bằng sudo ls để kiểm tra nhanh. Không dùng đường dẫn đó để chỉnh sửa file. Ghi file tại đó bằng root sẽ tạo lại vấn đề về quyền sở hữu đã 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 là dùng một container tạm thời để mount volume:

docker run --rm -v myapp_pgdata:/vol alpine ls -la /vol

Cách này hoạt động với mọi driver, nhìn thấy đúng các quyền mà container thực 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ỏ bản sao lưu đến đườ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 /data

tar bảo toàn ownership dạng số khi chạy dưới quyền root bên trong container. Đây là yếu tố giúp ứng dụng tiếp tục sử dụng volume đã khôi phục.

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à archive đó 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. Lệnh này tạo ra một file 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.

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 quyền sở hữu, permission và timestamp, nên user của container đã đọc được thư mục cũ vẫn có thể đọc volume mới. Sau đó, thay đổi service để 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, rồi đọc log của ứng dụng trước khi xóa thư mục cũ. Chiều ngược lại dùng cùng lệnh, nhưng hoán đổi /from/to.

Trong lúc kiểm thử, hãy nhớ một điều. docker compose down không tác động đến named volume, nhưng docker compose down -v xóa mọi named volume được 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 sở hữu thư mục đó. Nếu các lệnh quản lý vòng đời vẫn còn mới với bạn, hướng dẫn cơ bản về Docker Compose cho VPS trình bày từng bước.

Lựa chọn theo từng service

Hãy xác định tiến trình nào ghi file. Cấu hình mà bạn chỉnh sửa trong trình soạn thảo văn bản và commit vào git nên dùng bind mount, được mount tại :ro, vì bạn muốn nhìn thấy và quản lý phiên bản của nó. Dữ liệu trạng thái của ứng dụng mà bạn không bao giờ mở thủ công nên dùng named volume, vì Docker thiết lập quyền đúng cách và dữ liệu không phụ thuộc vào đường dẫn trên host.

Trường hợp kết hợp là media. Ứng dụng ghi thư viện ảnh, nhưng bạn cũng quản lý thư viện này và thư viện thường đủ lớn để cần một disk cụ thể. Hãy bind mount thư viện vào một đường dẫn trên disk đó, rồi thiết lập owner một cách chủ động một lần. Đây là mô hình mà hầu hết self-hosted stack sử dụng: named volume cho database và cache, bind mount cho config và thư mục lớn mà bạn cần quản lý.

FAQ

Sự khác nhau giữa bind mount và named volume là gì?

Bind mount ánh xạ một đường dẫn trên host vào container, nên cả hai phía cùng 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 do bạn quản lý, dùng named volume cho dữ liệu do ứng dụng quản lý.

Tại sao tôi nhận được lỗi "permission denied" với bind mount nhưng không gặp lỗi này với named volume?

Named volume trống được khởi tạo từ image, nên kế thừa quyền sở hữu do 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 đó, Docker sẽ tạo thư mục với chủ sở hữu là root. Chạy docker compose exec <service> id để xem id dạng số 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 đĩa ở đâu?

Với driver mặc định local, các volume nằm dưới /var/lib/docker/volumes/<volume>/_datadocker 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. Nếu chỉnh sửa với quyền root trên host, bạn có thể thay đổi quyền sở hữu theo cách mà container không mong đợi.

Làm cách nào để sao lưu named volume?

Chạy một container tồn tại trong thời gian ngắn, đồng thời mount volume và một thư mục trên host, rồi lưu trữ dữ liệu 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 cơ sở dữ liệu, hãy dump bằng công cụ riêng của cơ sở dữ liệu thay vì sao chép các file đang hoạt động. Bản sao file được tạo trong khi dữ liệu đang được ghi 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 được 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.