SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-10-28

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 ls

Volume 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ủ yếu xảy ra với bind mount

Đây là khác biệt thực tế lớn nhất và chủ yếu xuất phát từ một quy tắc: named volume trống khi được dùng lần đầu 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ó, nên volume được tạo với cùng numeric id đó và database khởi động.

Việc khởi tạo này là lợi thế duy nhất của named volume. Docker quản lý vị trí lưu volume, nhưng container đọc và ghi volume bằng các thao tác filesystem thông thường, nên ownership và mode vẫn được áp dụng như với bind mount. Named volume được mount vào một path không tồn tại trong image, hoặc được một image hay user khác ghi dữ liệu đầu tiên, sẽ không nhận ownership từ image. Khi đó, process không chạy dưới root cũng có thể bị từ chối truy cập. Lệnh kiểm tra id bên dưới hoạt động với cả hai loại.

Bind mount hoạt động theo hướng ngược lại. Những gì có trên host sẽ được container nhìn thấy, bao gồm cả ownership, còn nội dung image tại path đó 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. Khi process trong container chạy dưới một user không phải root, process đó 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ố khớp nhau. Ownership qua bind mount được so sánh bằng numeric user id, không phải bằng tên, vì container có /etc/passwd riêng. User 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, trừ khi daemon chạy với user-namespace remapping (userns-remap, được đặt trong /etc/docker/daemon.json hoặc trên dòng lệnh dockerd). Remapping chuyển mọi id của container vào một dải subordinate trên host. Trong dải mẫu của Docker bắt đầu từ 231072, container uid 0 tương ứng với host uid 231072, nên container uid 1000 tương ứng với host uid 232072.

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. Nếu không có remapping, 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 với user: "1000:1000" trong service. Khi bật remapping, user: không thể cố định container theo uid trên host của bạn, vì id bạn đặt cũng bị dịch chuyển: trong dải mẫu đó, user: "1000:1000" vẫn tương ứng với host uid 232072. Thay vào đó, hãy chown thư mục trên host về mapped host id (dải riêng của bạn nằm trong /etc/subuid và /etc/subgid). Tài liệu Docker chỉ nêu --userns=host trên docker container create, run hoặc exec là cách tắt remapping cho một container riêng lẻ. Cố định user: phù hợp hơn với ứng dụng do bạn tự viết. Chown 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ể bên dưới.

Có thêm 2 điểm dễ nhầm. 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 path dùng chung giữa các container hoặc :Z cho path chỉ một container được sử dụng, viết dưới dạng - ./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 bám theo inode ban đầu. Container sẽ tiếp tục nhìn thấy nội dung cũ cho đến khi bạn restart container. Nếu file thường xuyên được chỉnh sửa, hãy mount thư mục cha.

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/docker

docker 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_pgdata

Trườ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 /vol

Cá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 /data

tar 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 bạn chỉnh bằng text editor và commit vào git nên đặt trong 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 cấu hình đó. Application state mà bạn không bao giờ mở thủ công nên đặt trong named volume, vì Docker lấy quyền từ image và dữ liệu không phụ thuộc vào path trên host.

Media là trường hợp kết hợp. Application ghi vào thư viện ảnh, nhưng bạn cũng quản lý thư viện đó và thư viện thường đủ lớn để cần một disk riêng. Hãy bind mount thư viện vào một path trên disk đó và chủ động đặt ownership một lần. Đây là mô hình mà phần lớn 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ý. Một support desk như Chatwoot chạy trên VPS cũng phù hợp với mô hình này: Postgres dùng named volume, còn attachment đã upload nằm tại một path mà bạn có thể trỏ công cụ backup vào.

FAQ

Bind mount khác named volume ở điểm nào?

Bind mount ánh xạ một đường dẫn trên host vào container, nên cả hai phía đều thấy cùng 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. Về thực tế, điểm khác nhau nằm ở ownership: dùng bind mount cho config bạn tự quản lý, dùng named volume cho dữ liệu do ứng dụng quản lý.

Vì sao bind mount báo "permission denied" nhưng named volume thì không?

Named volume rỗng được seed từ image, nên thường kế thừa ownership do image thiết lập và user trong container có thể ghi vào đó. Volume vẫn áp dụng permission, nên vẫn có thể gặp lỗi tương tự nếu image không có gì tại đường dẫn đó. Bind mount hiển thị chính xác thư mục trên host, và nếu Docker phải tạo thư mục đó thì Docker đã tạo nó với ownership là 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, khi daemon không remap user namespace, đặt user: "1000:1000" trên service. Nếu daemon sử dụng user-namespace remapping, hãy chown sang host id đã được map, vì id của container bị dịch trên host và user: không thay đổi điều đó.

Docker lưu named volume trên disk ở đâu?

Với driver mặc định local, chúng nằm dưới /var/lib/docker/volumes/<volume>/_data, và docker volume inspect <volume> in ra chính xác Mountpoint. Hãy đọc nội dung đó nếu cần kiểm tra, nhưng chỉ ghi vào đó thông qua container, vì chỉnh sửa bằng root trên host sẽ thay đổi ownership 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à một 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 được sử dụng, vì bản sao file được tạo trong lúc dữ liệu đang được ghi có thể được restore 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, đồng thời 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. Bind mount không bị lệnh nào trong hai lệnh này xóa, vì thư mục đó thuộc về host chứ không thuộc về Docker.

Bài viết này có hữu ích không?