Docker Compose volumes:bind mount 與具名 volume 怎麼選
了解 Docker Compose 中 bind mount 與具名 volume 的差異:設定檔與資料各用哪種、權限錯誤成因,以及如何檢查、備份與遷移。
Bind mount 或具名磁碟區:簡短答案
Docker Compose 磁碟區分為兩種類型,選擇取決於檔案的管理者。對於由您自行寫入和讀取的檔案,請使用 bind mount,例如設定檔、範本和靜態網站。對於由應用程式管理的資料,請使用具名磁碟區,例如資料庫檔案、搜尋索引和上傳的媒體檔案。bind mount 指向主機上的路徑,您可以在編輯器中開啟該路徑。具名磁碟區是由 Docker 建立並追蹤的儲存空間,您必須透過 Docker 存取它。
兩者都會出現在服務內相同的 volumes: 索引鍵下,因此容易混淆。差異在於冒號左側。左側以 . 或 / 開頭時,表示主機路徑,因此是 bind mount。其他內容則是名稱,因此是具名磁碟區,而且該名稱也必須在頂層的 volumes: 區塊中宣告。
compose file 中的兩種語法
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 是具名 volume。./nginx.conf:/etc/nginx/nginx.conf 是 bind mount,而 :ro 會以唯讀方式掛載,這是容器不應改寫的設定檔所適用的正確預設值。若忘記頂層的 volumes: 項目,Compose 會因 service "db" refers to undefined volume pgdata 而停止。
啟動它,並列出 Docker 建立的項目:
docker compose up -d
docker volume ls此 volume 不叫做 pgdata,而是 <project>_pgdata;專案名稱預設為存放 compose file 的目錄名稱。名為 myapp 的目錄會產生 myapp_pgdata。這點很重要,因為重新命名目錄會建立新的空 volume,應用程式看起來就像遺失了資料。實際上並沒有遺失:舊 volume 仍會由 docker volume ls 列出。若目錄可能移動,請在 compose file 中使用 name: 固定名稱,或設定 COMPOSE_PROJECT_NAME。這類設定應與其他 Compose 環境檔案與 secrets 一起管理。
權限錯誤為何只會影響 bind mount
這是實務上最大的差異,原因來自一項規則:第一次使用時為空的 named volume 會從 image 複製初始內容,但 bind mount 不會。
當 Docker 將空的 named volume 掛載到 image 中已有內容的目錄上時,會把該內容複製到 volume,並保留 image 設定的擁有權和模式。官方 Postgres image 提供的 /var/lib/postgresql/data 由其專用的 postgres 使用者擁有,因此 volume 會由相同的數值 id 擁有,資料庫也能啟動。
bind mount 的行為相反。主機上的內容就是容器所看到的內容,包含擁有權,而 image 在該路徑上的內容會被隱藏。如果主機上的目錄不存在,Docker daemon 會建立該目錄,而 daemon 以 root 身分執行,因此目錄會由 root:root 擁有。以非 root 使用者執行的容器程序就無法寫入:
PermissionError: [Errno 13] Permission denied: '/data/app.db'修正方式是讓數值一致。bind mount 兩端會依數值 user id 比對擁有權,而不是依名稱比對,因為容器有自己的 /etc/passwd。容器內名為 app 的使用者,對主機沒有任何意義。兩端的 uid 1000 都代表 uid 1000。
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id 會顯示容器程序實際執行時使用的 uid。將主機目錄的擁有權設為該數值,或在 service 中使用 user: "1000:1000",讓容器固定使用你的數值。對於自行撰寫的應用程式,固定 user: 通常更簡潔。對於非自行撰寫的 image,變更主機目錄擁有權更安全,因為某些 image 會先以 root 身分啟動 entrypoint,再降低權限,並要求底層檔案具有特定擁有權。
另外還有兩個容易踩到的問題。在 Fedora、RHEL 及其他啟用 SELinux(security-enhanced Linux)強制模式的系統上,bind mount 必須先重新標記,否則會遭到拒絕。因此,容器之間共用的路徑請加上 :z;只有單一容器使用的路徑請加上 :Z,寫法為 - ./data:/data:Z。此外,掛載單一檔案而非目錄時,如果編輯器以替換檔案的方式編輯,而不是直接寫入原檔案,bind mount 就會失效,因為 mount 會追蹤原始 inode。容器會持續看到舊內容,直到重新啟動。若檔案經常編輯,請改為掛載其上層目錄。
效能:差異確實存在的情況
在 Linux 伺服器上,這兩種方式都會經過相同的核心路徑,因此輸送量差異很小,不應以此作為選擇依據。使用預設 local 驅動程式的具名磁碟區,位於 Docker 其餘資料所在的相同檔案系統中,也就是 /var/lib/docker/volumes/;繫結掛載則位於您指定的位置。
在 macOS 和 Windows 的 Docker Desktop 上,差異就會出現,因為容器是在虛擬機器內執行。繫結掛載必須透過檔案共用層,將主機檔案系統連接到該虛擬機器。對於包含大量小型檔案作業的工作負載,例如 Node.js 相依套件樹或 PHP framework 快取,速度會明顯變慢。具名磁碟區留在虛擬機器內,不會產生這項成本。因此,許多開發用 compose 檔案會繫結掛載原始碼目錄,但在 node_modules 上宣告具名磁碟區。
另一項實際差異是資料的儲存位置。繫結掛載到 /mnt/backup 會將資料寫入該磁碟。具名磁碟區則會寫入存放 /var/lib/docker 的檔案系統;在 VPS 上,這通常是 root 磁碟。具名磁碟區中的資料庫若持續成長,會填滿與系統日誌共用的同一個磁碟。請在問題演變成事件前確認:
docker system df -v
df -h /var/lib/dockerdocker system df -v 會列出所有磁碟區及其大小,並標記不再被任何容器參照的磁碟區。
檢查具名磁碟區
具名磁碟區不是黑箱。請讓 Docker 顯示其位置:
docker volume inspect myapp_pgdataMountpoint 欄位會提供主機上的實際路徑,通常是 /var/lib/docker/volumes/myapp_pgdata/_data。您可以使用 sudo ls 讀取該路徑,這適合快速檢查。請勿將其視為可用來編輯檔案的位置。以 root 身分在該處寫入檔案,會再次造成上述的擁有權問題;而且該路徑是 local 驅動程式的細節,其他磁碟區驅動程式不一定具有相同設定。
安全的檢查方式,是使用掛載該磁碟區的一次性容器:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /vol這種方式適用於任何驅動程式,並且會看到與實際容器相同的權限。由於 --rm,操作結束後不會留下任何內容。
備份各種類型
bind mount 是一般目錄,因此任何檔案層級的備份工具都能處理。將備份目標指定為主機路徑即可完成。named volume 則需要額外步驟,因為工具必須進入其中。將 volume 和主機目錄掛載至同一個短暫容器,然後建立封存檔:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .將資料還原至新的 volume,即可反向執行:
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 會讓容器內以 root 身分執行時保留數字所有權,這能確保還原的 volume 可供應用程式使用。
以下警告適用於這兩種類型。在資料庫執行時複製其檔案,會得到一份內容持續變動中的封存檔,還原後可能處於損毀狀態。請先停止服務,或透過資料庫本身的工具傾印資料,如 docker compose exec -T db pg_dump -U postgres appdb > appdb.sql 所示。這會產生純文字檔,之後即可將其與 compose 檔案一併納入一般的 加密 restic 備份程序。
將繫結掛載遷移至具名磁碟區
這項移動是複製,不是重新命名,約需 1 分鐘。
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 會保留所有權、模式和時間戳記,因此原本能讀取舊目錄的容器使用者,仍能讀取新的磁碟區。接著將服務改為使用 pgdata:/var/lib/postgresql/data,在頂層 volumes: 區塊中加入 pgdata,執行 docker compose up -d,並在刪除舊目錄前讀取應用程式的日誌。反向操作時,使用相同的命令,但交換 /from 和 /to。
測試期間請記住一點。docker compose down 不會處理具名磁碟區,但 docker compose down -v 會刪除專案宣告的所有具名磁碟區,而且無法復原。繫結掛載在這兩種操作後都會保留,因為 Docker 從未管理該目錄。如果您還不熟悉這些生命週期命令,VPS 的 Docker Compose 基礎指南會逐步說明這些命令。
逐項選擇
先確認檔案由誰寫入。您在文字編輯器中修改並提交至 git 的設定,應使用繫結掛載,掛載為 :ro,因為您需要查看檔案並進行版本管理。您不會手動開啟的應用程式狀態,應使用具名磁碟區,因為 Docker 會正確設定權限,且資料不會依賴主機路徑。
媒體檔案屬於混合情況。相片庫由應用程式寫入,但也由您管理,而且通常夠大,需要使用特定磁碟。請將它繫結掛載至該磁碟上的路徑,並一次明確設定擁有權。多數自架服務堆疊最後都採用這種模式:資料庫與快取使用具名磁碟區,設定與您所關注的大型目錄則使用繫結掛載。
FAQ
bind mount 與 named volume 有何不同?
bind mount 會將主機上的路徑對應至容器,因此兩端會看到相同的目錄,您也可以使用一般工具編輯該目錄。named volume 是由 Docker 建立和管理的儲存空間,透過名稱參照,並在頂層的 volumes: 區塊中宣告。實務上的差異在於管理責任:bind mount 適合由您維護的設定,named volume 適合由應用程式維護的資料。
為什麼使用 bind mount 會出現「permission denied」,但使用 named volume 不會?
空的 named volume 會從映像檔初始化,因此會繼承映像檔設定的擁有權,容器使用者可以寫入其中。bind mount 會如實呈現主機目錄的狀態;如果 Docker 必須建立該目錄,則會將其擁有者設為 root。執行 docker compose exec <service> id 以查看容器使用的數值 ID,接著執行 sudo chown -R <uid>:<gid> 變更主機目錄的擁有權,或在服務上設定 user: "1000:1000"。
Docker 會將 named volume 儲存在哪個磁碟位置?
使用預設的 local driver 時,named volume 會位於 /var/lib/docker/volumes/<volume>/_data 下方,而 docker volume inspect <volume> 會輸出確切的 Mountpoint。如果需要檢查內容,可以讀取該路徑;但只能透過容器寫入,因為在主機上以 root 編輯會以容器未預期的方式變更擁有權。
如何備份 named volume?
執行一個短期容器,同時掛載該 volume 和主機目錄,再使用 docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data . 將資料從一端封存至另一端。對於資料庫,請使用資料庫本身的工具進行傾印,不要複製正在使用的檔案,因為在寫入進行中取得的檔案副本,還原後可能會處於損毀狀態。
docker compose down 會刪除我的 volumes 嗎?
docker compose down 會移除容器和網路,並保留 named volumes。docker compose down -v 還會永久刪除專案宣告的所有 named volumes。兩個命令都不會移除 bind mounts,因為該目錄屬於主機,而不是 Docker。