Docker Bind Mount Permission Denied: UID/GID、Non-root 與 Rootless 的取捨

English Abstract: A classic Docker bind mount failure: the container writes to a host directory and gets Permission denied, even though the host user clearly owns that directory. The root cause is almost always a UID/GID mismatch between the host filesystem and the container process — Linux permission checks only look at numeric IDs, not usernames. This post compares three fixes: pinning the container to a specific UID with --user, the PUID/PGID entrypoint pattern popularized by LinuxServer.io images, and Rootless Docker / Podman's user namespace remapping (userns-remap), along with the security and portability trade-offs of each.

用 -v 把 host 目錄掛進 container,裡面的程式想寫入卻得到 Permission denied,即使在 host 上明明對這個目錄有讀寫權限,這是最常見的 bind mount 問題之一。問題幾乎都出在 host 檔案的 owner UID/GID,跟 container 內 process 實際跑的 UID/GID 對不上——Linux 的權限檢查只認數字,不認「使用者名稱」。這篇文章說明三種常見解法:用 --user 直接指定 UID、用 PUID/PGID entrypoint 動態對齊、以及 Rootless Docker/Podman 的 user namespace 重映射,並比較三者在安全性、可攜性與設定複雜度上的取捨。

(一)問題重現:host 目錄明明可寫,container 卻 Permission denied

典型情境:用以下指令把 host 目錄掛進 container:

docker run -d --name myapp -v /home/gz/data:/data myapp:1.0

進入 container 嘗試寫入:

docker exec -it myapp sh -c "echo hello > /data/test.txt"
sh: can't create /data/test.txt: Permission denied

而 host 上用自己的帳號檢查,明明有讀寫權限:

ls -la /home/gz/data
drwxr-xr-x 2 gz gz 4096 Oct 16 10:00 .

這種「host 看得到、container 寫不進去」的落差,根源幾乎都出在 UID/GID 沒對齊,下一節拆解原因。

(二)根本原因:UID/GID 數字對齊,而不是「名字」

Linux 檔案系統權限檢查只看 UID/GID 數字,不管 process 或 host 顯示出來的「使用者名稱」字串。Bind mount 跟 named volume 不一樣:它不做任何資料複製或初始化,是直接把 host 目錄原封不動掛進 container 的 mount namespace,權限位元(含 owner UID/GID)完全沿用 host 檔案系統上的原始數值。

多數 image 預設用 UID 0(root)或 Dockerfile 裡建立的某個固定 UID 跑 process,實際數值要自己查,不要用猜的:

docker run --rm myapp:1.0 id
uid=1000(node) gid=1000(node) groups=1000(node)

如果 host 目錄的 owner 是 UID 1001(你自己的帳號),而 container process 是 UID 1000,兩者對不上,就算 image 裡那個使用者「看起來」合理,一樣會 Permission denied。

先用兩道指令把兩邊的 UID 都查清楚,不要只看名字:host 用 id -u/id -g,container 用 docker run --rm <image> id。檢查 bind mount 權限問題,第一步永遠是對這兩個數字。
三種解法:如何讓 host 與 container 的 UID/GID 對齊問題Host 目錄owner UID 1001Container processUID 1000數字不符✗ Permission Denied解法一:--userHost 目錄owner UID 1001Container processUID 1001(啟動時指定)--user 1001:1001✓ 數字一致解法二:PUID/PGID entrypointHost 目錄owner UID 1001Container processUID 動態改為 1001entrypoint: usermod -u $PUID✓ 啟動時自動對齊解法三:Rootless/userns-remapHost 目錄owner UID 101001(映射後的實際 UID)Container 內看到UID 1(namespace 內)(對應 host 的 101001)kernel user namespace 映射✓ 對齊,且breakout 風險受限

圖:Bind mount 下,host owner 與 container process 的 UID/GID 對齊關係——三種解法殊途同歸,核心都是讓兩邊數字一致

(三)解法一:--user 直接指定 UID(最快,適合單機開發)

最直接的做法是執行時用 --user 把 container process 的 UID/GID 直接指定成 host 目錄 owner 的數值:

id -u gz
1001
id -g gz
1001

docker run -d --name myapp --user 1001:1001 -v /home/gz/data:/data myapp:1.0

這樣 container 內 process 的 UID 跟 host 檔案 owner 完全一致,讀寫自然沒有權限問題。

缺點:image 裡如果沒有這個 UID 對應的使用者(/etc/passwd 查不到),某些依賴 $HOME、getpwuid() 的工具可能出錯,常見於需要寫 .cache、.config 的應用;而且這個數值綁死在某台 host 的帳號 UID,換一台機器、換一個使用者,UID 不同就要重新指定,不利於可攜性。

(四)解法二:Non-root 固定 UID + Entrypoint 動態對齊(PUID/PGID 模式)

如果不想每次都手動指定 --user,可以讓 image 自己在啟動時用 entrypoint script 把內部使用者的 UID/GID 動態改成外部傳入的環境變數,這是 LinuxServer.io 系列 image 普遍採用的做法。

Dockerfile 片段:

FROM alpine:3.20
RUN addgroup -g 1000 appgroup && adduser -D -u 1000 -G appgroup appuser
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh:

#!/bin/sh
set -e
PUID=${PUID:-1000}
PGID=${PGID:-1000}
groupmod -o -g "$PGID" appgroup
usermod -o -u "$PUID" appuser
exec su-exec appuser "$@"

啟動時只要傳入對應的 UID/GID:

docker run -d --name myapp -e PUID=1001 -e PGID=1001 -v /home/gz/data:/data myapp:1.0

優點:不用每次重新指定 docker run 參數,image 本身可攜,使用者只要帶兩個環境變數;而且 image 內 /etc/passwd 一定有這個使用者,不會有 getpwuid() 查不到的問題。

缺點:此機制需要 image 本身支援——若為自建 image,須自行撰寫 entrypoint;若使用現成 image,則須先確認官方文件是否已實作此模式,並非所有 image 皆支援。

(五)解法三:Rootless Docker/Podman 的 User Namespace 重映射

前兩種做法有個共同前提:container 裡的 process 本來就有能力變成跟 host 數值相同的 UID/GID,不管是直接指定還是啟動時 usermod。但這代表如果 container 被攻破,attacker 在 container 裡拿到的 UID,在 host 上就是同一個 UID——如果剛好是 host 上有意義的帳號,甚至是 UID 0,風險就被放大了。

Rootless 模式(Docker 的 userns-remap,或改用 Podman 的 rootless 預設模式)做法不同:container 內看到的 UID,跟 host 上實際對應的 UID,透過 /etc/subuid、/etc/subgid 做一層間接映射,container 內的 UID 0(root)實際上對應到 host 上一個完全沒有特殊權限的高位 UID(例如從 100000 起算的區間)。

啟用 Docker userns-remap(/etc/docker/daemon.json):

{
  "userns-remap": "default"
}

重啟 daemon 後,確認映射範圍:

cat /etc/subuid
dockremap:100000:65536

這代表 container 內的 UID 0~65535,實際對應到 host 的 100000~165535。

Bind mount 的影響:host 目錄要能被 container 寫入,必須把 owner 改成映射後的對應 UID,而不是原本的邏輯 UID:

sudo chown -R 100000:100000 /home/gz/data

Podman 預設走 rootless,不需要額外設定 userns-remap,但同樣要注意 bind mount 目錄的 UID 要對應到 podman unshare cat /proc/self/uid_map 顯示的映射範圍,用 podman unshare chown 在正確的 namespace 下調整 owner,而不是直接在 host shell 下 chown。

優點是 container 內的 root 不等於 host 的 root,就算 container 被攻破、拿到 container 內的「root」,對 host 檔案系統實際上仍受限於一個低權限 UID,大幅降低 breakout 後的衝擊範圍。代價是 bind mount 的 UID 對齊變得不直覺,要先查映射表才知道該 chown 成什麼;部分需要跟 host network namespace、特殊 capability 互動的工作負載,在 rootless 模式下會受限或需要額外設定。

參考:Docker Docs - Isolate containers with a user namespace

(六)三種做法怎麼選

做法 安全性 可攜性 設定複雜度 適合情境
--user 指定 UID中低低本機開發、CI pipeline 裡短暫跑的 job
PUID/PGID entrypoint中高中家用/小型自架服務,多台機器部署
Rootless/userns-remap高中高多租戶環境、對外服務、資安要求較高的 production

結論

遇到 bind mount permission denied,照這個順序檢查:

  1. 先查清楚兩邊的 UID/GID 數字,host 用 id -u/id -g,container 用 docker run --rm <image> id,不要只看使用者名稱
  2. 單機開發、CI 裡短暫跑的 job,用 --user 直接指定最快
  3. 要長期跑、換機器部署的服務,PUID/PGID entrypoint 模式比較省心,但要先確認 image 有沒有支援
  4. 對外服務或多租戶環境,優先考慮 rootless/userns-remap,用一點設定複雜度換取 container root 不等於 host root 的安全邊界
  5. 用 rootless 模式時,bind mount 目錄的 owner 記得改成映射後的實際 UID,不是原本的邏輯 UID

延伸閱讀:Docker Container 錯誤排查指南・Docker 環境下的 Proxy 配置

常見問題 FAQ

為什麼用 named volume(docker volume create)就不會有這個問題,bind mount 卻會?

named volume 由 Docker 自己管理儲存位置與初始化,第一次掛載空的 named volume 時,Docker 會把 image 內該路徑既有的檔案複製過去,並且沿用 image 內該路徑原本設定的 owner(通常是 image 裡建立使用者時就設好的 UID),所以天生就是對齊的。bind mount 掛的是 host 既有目錄,owner 是 host 建立當下的 UID/GID,Docker 不會幫你做任何調整,對不對齊完全取決於你自己管理。

設定了 --user 之後,container 內還是有些檔案操作失敗,是什麼原因?

常見是 image 內其他目錄(例如 /tmp、使用者自己的 home、/var/run 底下的某些路徑)本身的 owner 是 build 時期設定好的其他 UID(通常是 root 或 image 預設使用者),跟你用 --user 指定的 UID 對不上,這些路徑本身也需要在 Dockerfile 裡用 RUN chown 處理,或是用 --user 以外再搭配 tmpfs 等方式避開。

可以同時用 rootless 模式又用 --user 指定 UID 嗎?

可以,兩者是不同層次的機制可以疊加:--user 決定的是 container 內 process 實際跑在哪個(namespace 內的)UID,userns-remap/rootless 決定的是這個 namespace 內的 UID 對應到 host 上的哪個實際 UID。疊加使用時,bind mount 要對齊的 host owner,要用「--user 指定的 UID 經過映射表轉換後」的數值,而不是原始數值,檢查起來要兩層一起看。

已經用 PUID/PGID 模式了,換了一台新機器後 bind mount 還是 permission denied,為什麼?

最常見原因是新機器上對應帳號的 UID 跟舊機器不同(同樣叫 gz 的帳號,在不同機器上是 UID 1000 還是 1001,取決於建立順序),而 docker run 指令裡的 PUID/PGID 環境變數值是沿用舊機器複製過來的,沒有依新機器實際 id -u/id -g 的結果更新。換機器部署時務必重新查一次目標機器的 UID/GID,而不是直接複製舊的啟動指令。

本文含聯盟連結。As an Amazon Associate I earn from qualifying purchases.

📚 延伸閱讀・Kindle 電子書(2025 年 10 月推出)
Container Security: Fundamental Technology Concepts That Protect Cloud Native Applications
Liz Rice 的 Container Security 全新第二版。從 namespace、cgroup、capability 到本文談的 UID/GID 與 rootless 原理,是少數把「container 的安全邊界到底從哪裡來」講清楚的書,適合想把 Permission denied 這類表面症狀連回底層機制的讀者。
在 Amazon 訂購 Kindle 版 →

留言

熱門文章

Docker 環境下的 Proxy 配置