Docker Compose volume:bind mount 與 named volume
比較 Docker Compose 的 bind mount 與 named volume,說明設定檔與資料的適用情境、權限錯誤陷阱,以及如何檢查、備份與遷移。
Bind mount 或 named volume:簡短答案
Docker Compose volume 分成兩種類型,選擇取決於檔案由誰管理。對於需要自行讀寫的檔案,例如設定、範本和靜態網站,請使用 bind mount。對於由應用程式管理的資料,例如資料庫檔案、搜尋索引和上傳媒體,請使用 named volume。bind mount 指向主機上的路徑,您可以直接在編輯器中開啟。named volume 是由 Docker 建立並追蹤的儲存空間,您必須透過 Docker 存取。
兩者都會出現在服務內相同的 volumes: 索引下,因此容易混淆。差異在於冒號左側。左側以 . 或 / 開頭時,代表主機路徑,因此是 bind mount。其他內容則是名稱,因此是 named volume;該名稱也必須在頂層的 volumes: 區塊中宣告。
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 是命名 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 檔案所在目錄的名稱。若目錄名稱為 myapp,則會產生 myapp_pgdata。這點很重要,因為重新命名目錄會產生一個新的空 volume,應用程式看起來就像遺失了資料。實際上資料並未遺失:舊的 volume 仍會由 docker volume ls 列出。請在 compose 檔案中使用 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 擁有,資料庫也就能啟動。
這項初始化是 named volume 唯一享有的優勢。Docker 會管理 volume 的儲存位置,但容器仍以一般檔案系統操作讀寫,因此擁有者與模式的限制,和 bind mount 完全相同。若 named volume 掛載到 image 中不存在的路徑,或最初由不同的 image 或使用者填入內容,就不會繼承 image 的擁有權,非 root 程序同樣可能遭到拒絕。下方的 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,除非 daemon 啟用了 user-namespace remapping(userns-remap,設定於 /etc/docker/daemon.json 或 dockerd 命令列)。Remapping 會把所有容器 id 位移到主機上的 subordinate range。在 Docker 範例中,若 range 從 231072 開始,容器 uid 0 就是主機 uid 231072,因此容器 uid 1000 會是主機 uid 232072。
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id 會顯示容器程序實際使用的 uid。未啟用 remapping 時,讓主機目錄使用該數值,或在 service 中以 user: "1000:1000" 將容器固定為你的數值。啟用 remapping 時,user: 無法將容器固定為你的主機數值,因為你設定的 id 也會被位移:在上述範例 range 中,user: "1000:1000" 仍會對應到主機 uid 232072。此時應將主機目錄的擁有者改為對應的主機 id(你自己的 range 位於 /etc/subuid 與 /etc/subgid)。Docker 文件指出,在 docker container create、run 或 exec 上,只有 --userns=host 能將單一容器的 remapping 關閉。對於自行撰寫的應用程式,固定 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 會列出所有磁碟區及其大小,並標示目前沒有任何容器再參照的磁碟區。
檢查具名 volume
具名 volume 不是黑箱。使用 Docker 查詢其位置:
docker volume inspect myapp_pgdataMountpoint 欄位會提供實際的主機路徑,通常是 /var/lib/docker/volumes/myapp_pgdata/_data。您可以使用 sudo ls 讀取該路徑,這適合快速檢查。但不要將其視為可直接編輯檔案的位置。以 root 身分在該處寫入檔案,會再次造成上述的擁有權問題;而且該路徑是 local driver 的實作細節,其他 volume driver 不一定具備相同配置。
安全的檢視方式是使用掛載該 volume 的一次性容器:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /vol這種方式適用於任何 driver,看到的權限與實際容器相同,並且不會留下任何內容,因為使用了 --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 備份流程。
將 bind mount 遷移至 named volume
這項作業是複製,而不是重新命名,約需 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 會保留擁有者、模式與時間戳記,因此原本能讀取舊目錄的容器使用者,仍能讀取新的 volume。接著將服務改為使用 pgdata:/var/lib/postgresql/data,在頂層的 volumes: 區塊中加入 pgdata,執行 docker compose up -d,並在刪除舊目錄前查看應用程式的日誌。反向操作時,使用相同的指令,但交換 /from 與 /to。
測試時請注意一點。docker compose down 不會處理 named volume,但 docker compose down -v 會刪除專案宣告的所有 named volume,而且無法復原。bind mount 在這兩種操作後都會保留,因為 Docker 從未擁有該目錄。若你尚不熟悉這些生命週期指令,請參閱 VPS 的 Docker Compose 基礎指南,其中會逐步說明這些指令。
逐項服務選擇
先確認檔案由誰寫入。在文字編輯器中修改並提交至 git 的設定,應使用 bind mount,並掛載為 :ro,因為你需要查看內容並納入版本控制。從不手動開啟的應用程式狀態,應使用 named volume,因為 Docker 會依據 image 設定權限,資料也不會依賴主機路徑。
媒體檔案屬於混合情況。相片資料庫由應用程式寫入,但也由你管理,而且通常容量很大,適合放在指定磁碟上。請將它 bind mount 至該磁碟上的路徑,並一次設定正確的擁有者。多數自架服務堆疊最後都採用這種配置:資料庫與快取使用 named volume,設定使用 bind mount,重要的大型目錄也使用 bind mount。像 在 VPS 上執行 Chatwoot 的支援服務台 就是如此:Postgres 使用 named volume,而上傳附件則放在可指定備份目標的路徑上。
FAQ
bind mount 與 named volume 有什麼差異?
bind mount 會將主機上的路徑映射到容器,因此兩側看到的是同一個目錄,你也能使用一般工具編輯內容。named volume 是由 Docker 建立並管理的儲存空間,透過名稱參照,並在頂層的 volumes: 區塊中宣告。實務上的區分是擁有權:bind mount 適合由你維護的設定,named volume 適合由應用程式維護的資料。
為什麼 bind mount 會出現 "permission denied",但 named volume 不會?
空的 named volume 會從 image 初始化,因此通常會繼承 image 設定的擁有權,容器使用者也能寫入。不過它仍會套用權限限制,因此若 image 在該路徑沒有內容,仍可能以相同方式失敗。bind mount 會原樣呈現主機目錄;若該目錄是由 Docker 建立,Docker 會讓 root 成為其擁有者。執行 docker compose exec <service> id 查看容器使用的數值 id,再對主機目錄執行 sudo chown -R <uid>:<gid>;或者在 daemon 未進行 user namespace remapping 時,在 service 上設定 user: "1000:1000"。若 daemon 使用 user-namespace remapping,請改為將擁有者設為對應的主機 id,因為容器的 id 在主機上會被位移,而 user: 不會改變這點。
Docker 會將 named volume 儲存在哪裡?
使用預設的 local driver 時,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 . 將資料從一處封存到另一處。若是資料庫,應使用資料庫本身的工具執行 dump,而不是複製執行中的檔案,因為寫入進行期間取得的檔案副本,還原後可能處於損毀狀態。
docker compose down 會刪除我的 volume 嗎?
docker compose down 會移除容器與 network,並保留 named volume。docker compose down -v 也會永久刪除專案宣告的所有 named volume。兩個命令都不會移除 bind mount,因為該目錄屬於主機,而不是 Docker。