Docker Compose 的 PUID 和 PGID 是什麼?
PUID 和 PGID 不是 Docker 設定,而是 linuxserver.io image 的 entrypoint 慣例。了解 bind mount 為何產生 911:911,以及如何修正檔案擁有者。
PUID 和 PGID 的實際用途
PUID 和 PGID 是某些 container image 在啟動時讀取的兩個環境變數。Docker 本身不會讀取這兩個變數。這是 linuxserver.io image 與少數其他 image 採用的慣例。如果 image 沒有實作讀取這些變數的功能,就會直接忽略它們。
在 linuxserver.io image 中,有一個名為 abc 的使用者。該使用者在 build 時建立,UID(user ID)和 GID(group ID)都是 911。container 會以 root 身分啟動並執行 init script。其中一個 script 會在其他程序開始前,先重新設定該使用者的 ID:
groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abc-o 旗標允許使用其他地方已經使用中的 ID。完成後,init 會降級權限,並以 abc 身分執行應用程式。因此,PUID=1000 不會傳到 Docker。該變數會在應用程式啟動前,重新設定 container 內使用者的 ID。這表示應用程式寫入的所有檔案,在主機磁碟上都會由 1000 擁有。若未設定 PUID,abc 會保留 911,這也是未設定的 bind mount 會產生大量由 911:911 擁有之檔案的原因。
使用 id 取得兩個數字
以擁有資料目錄的使用者身分,在主機上執行:
iduid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),988(docker)uid 是您的 PUID,gid 是您的 PGID。若要在指令碼中使用,id -u 和 id -g 會輸出純數字。大多數全新 VPS 映像中的第一個一般使用者帳號為 1000:1000,但不要直接假設如此。重建伺服器,或之後新增第二個帳號時,UID 和 GID 可能會是 1001 或更高;此處的數字錯誤就是整個問題的原因。若服務是以 專用服務帳號,而不是您自己的登入使用者 執行,請執行 id thatuser,並從輸出中取得這些數字。
檔案為何顯示為 911:911
ls -l在主機上找不到與該 ID 相符的帳號時,會顯示數字 ID,而不是名稱。伺服器上沒有 UID 911,因此沒有可顯示的名稱。使用 ls -ln,讓輸出一律顯示數字,避免產生歧義:
ls -ln /srv/appdata/sonarrdrwxr-xr-x 2 911 911 4096 Aug 7 09:12 Backups
-rw-r--r-- 1 911 911 512 Aug 7 09:12 config.xml這表示容器使用了內建的預設值。請直接在容器內確認,不要只憑猜測:
docker exec sonarr id abc
docker compose logs sonarr | head -n 25linuxserver init 會在啟動日誌中以兩行輸出結果:
User UID: 911
User GID: 911如果你在 Compose 檔案中設定 PUID=1000 後,這兩行仍顯示 911,表示該變數從未傳入容器。最常見的原因是你編輯了 docker-compose.yml,接著執行 docker compose restart;此命令會沿用原有容器及其初始環境。環境變數變更需要使用 docker compose up -d,該命令會重新建立容器。
為什麼無法刪除容器建立的檔案
核心只比對數字,不會比對名稱。您的 shell 以 UID 1000 執行。檔案屬於 UID 911。存放該檔案的目錄是 drwxr-xr-x,同樣屬於 911,因此群組與其他使用者只有讀取和執行權限,沒有寫入權限。刪除檔案需要對其所在目錄具備寫入權限,而不是對檔案本身具備寫入權限。因此,即使檔案本身看似沒有問題,仍會出現以下結果:
rm: cannot remove '/srv/appdata/sonarr/config.xml': Permission denied寫入檔案的容器也會從另一端遇到相同限制。如果主機目錄屬於您的使用者,權限模式為 755,而應用程式以 911 執行,第一次寫入就會因 Permission denied 而失敗,應用程式則會以自身的訊息回報錯誤。在 Sonarr 或 Radarr 等 .NET 應用程式中,這會顯示為 UnauthorizedAccessException: Access to the path '/data/downloads' is denied。檔案前方的權限字串會告訴您實際套用的是三組權限中的哪一組,而正確讀懂 drwxr-xr-x能讓這個錯誤從難以理解變得一目了然。
這是 bind mount 特有的問題。當 Docker 建立空的 named volume,並將其掛載到 image 中已存在的路徑時,Docker 會將該路徑的內容複製到 volume,包括擁有者與權限位元,因此應用程式會取得一個原本就由它擁有的目錄。bind mount 不會獲得這項處理:Docker 會完全按照原樣掛載您的主機目錄。這項差異也是了解何時 bind mount 優於 named volume,以及何時不是的實際原因之一。
修正已經錯誤的目錄
設定 PUID 和 PGID 只會影響應用程式後續的行為,不會追溯修正磁碟上已存在的檔案。先停止 stack,手動修正擁有者,再重新啟動:
docker compose down
sudo chown -R 1000:1000 /srv/appdata/sonarr
docker compose up -d如果不想輸入數字,可以使用 sudo chown -R "$(id -u):$(id -g)" /srv/appdata/sonarr。請在容器停止時執行,因為應用程式若在遞迴執行 chown 期間仍持續寫入,可能導致目錄樹只完成部分修正,並引發難以判讀的第二輪錯誤。
PUID 和 PGID 無法修正的問題
以下是最容易讓已正確完成設定的使用者遇到問題的部分。linuxserver init 在啟動時只會對 3 個路徑執行 chown:/app、/config 和 /defaults。媒體掛載不在這份清單中。/data、/downloads 和 /tv 會原封不動地交給應用程式處理。因此,如果這些掛載在主機端的擁有者設定,導致容器使用者無法寫入,容器仍會正常啟動,並在啟動畫面顯示正確的 UID,接著在第一次匯入時失敗。
這是正確的行為。每次容器啟動時,若要對 12 TB 的媒體庫遞迴執行 chown,將會造成嚴重問題。這表示媒體目錄必須由你自行管理,而這些掛載也是權限實際發生問題的地方。
控制使用者的 3 種方式,以及各自適用的時機
PUID 和 PGID 環境變數
這只適用於其 entrypoint 會讀取這些變數的映像檔。這種方式很常見,因為容器仍會以 root 啟動,先完成自身設定、修正 /config,最後才降權執行。Docker Mods 和自訂 init script 也能繼續運作。代價是必須依賴慣例,而不是平台功能;此外,不同專案使用的變數名稱也不一致。
Compose 中的 user: 鍵
這是 Docker 的原生功能,適用於所有映像檔,因為容器 runtime 會在映像檔自身的程式碼執行前套用設定:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
user: "1000:1000"程序從未以 root 身分執行,哪怕只有一瞬間,這確實提升了安全性。但 entrypoint 中任何需要 root 權限的操作也會失效。對 linuxserver 映像檔而言,專案僅在合理努力的範圍內支援此功能,而且只支援經過測試的映像檔;具體限制包括:PUID 和 PGID 不再生效、Docker Mods 不會執行、自訂服務不會執行,且每個掛載 volume 的權限都由你負責。其文件中的模式會將此旗標搭配可寫入的 /run:
user: 1000:1000
tmpfs:
- /run:uid=1000,gid=1000,exec
security_opt:
- no-new-privileges=true有一個外觀上的副作用常讓人困惑。數值型 user: 在容器的 /etc/passwd 中沒有對應項目,因此容器內的工具會顯示 whoami: cannot find name for user ID 1000。該 ID 有效,檔案存取也能正常運作。只有名稱查詢會失敗。
Rootless Docker
Rootless Docker 會讓 daemon 本身以你的非特權使用者身分執行,因此主機上沒有任何元件以真正的 root 身分執行。這會完全改變擁有權的計算方式。容器 UID 0 會對應至執行 rootless Docker 的主機使用者 UID;容器 UID n 的任何 n(1 或以上)則會對應至 subuid + (n - 1),其中 subuid 是在 /etc/subuid 中分配給你的範圍起點,而 /etc/subgid。Docker 要求該處至少有 65,536 個 subordinate ID。
請重新閱讀這項對應關係,因為它與通常的建議相反。在 rootless Docker 下,以 root 身分寫入的容器會建立由你擁有的檔案。以 UID 1000 寫入的容器則會建立由約 100999 的 subordinate ID 擁有的檔案,而你的 shell 無法存取這些檔案。因此,在 rootful daemon 上正確的 PUID 值,在這裡反而是錯的。這兩種機制在不同層級解決相同問題;未經確認就疊加使用,便可能導致你需要 sudo 才能刪除某個目錄。如果採用 rootless Docker,請先在自己的伺服器上測試一個寫入檔案的擁有權,再將資料庫或檔案庫遷移進去。
對於在單一 VPS 上執行的大多數自架服務堆疊而言,在 rootful daemon 上使用 PUID 和 PGID 是務實的選擇,因為映像檔就是依此建置並撰寫文件。當映像檔 README 明確表示已針對 user: 進行測試,或你使用完全不支援 PUID 的官方上游映像檔時,再考慮採用該方式。像 在單一 VPS 上自架 AFFiNE instance 這類文件工作區便屬於後者,因為其中沒有任何容器會讀取 PUID;其資料庫目錄與上傳檔案的擁有權,是由 runtime 決定,而不是由環境區塊中的設定決定。
媒體堆疊案例:所有容器共用同一個群組
包含 Sonarr、Radarr 與下載用戶端的 arr 媒體堆疊是這項設定不再只是理論的實際案例。下載用戶端會將完成的檔案寫入 /data/downloads。Sonarr 接著會將該檔案建立 hardlink,或移動至 /data/media。若要讓 hardlink 正常運作,兩個容器都必須能寫入同一個目錄樹;如果下載用戶端以 1000 執行,而 Sonarr 以 1001 執行,其中一個容器建立的檔案就可能歸另一個容器只能讀取。
修正方式是建立一個由堆疊中所有容器以 PGID 使用的共用群組:
sudo groupadd -g 13000 media
sudo usermod -aG media deploy
sudo chown -R deploy:media /srv/media
sudo find /srv/media -type d -exec chmod 2775 {} +
sudo find /srv/media -type f -exec chmod 0664 {} +2775 開頭的 2 是 setgid 位元。套用在目錄上時,表示其中建立的每個新檔案與子目錄都會繼承群組 media,而不是建立者自己的主要群組。因此,新增下載後不必重新執行 chown,這項設定仍會有效。執行 newgrp media,或登出後重新登入,再檢查自己的存取權限:使用 usermod -aG 新增的群組不會立即出現在已開啟的 shell 工作階段中。
在容器內,groupmod -o -g 13000 abc 會將 abc 群組重新編號為 13000,使 abc 寫入的檔案使用與主機 media 群組相同的 GID。堆疊中的每個容器都保留自己的 PUID,並共用這個 PGID。
接著,為堆疊中的每個 linuxserver 容器設定 UMASK=002。這是最容易遺漏的步驟。這些映像檔的預設值是 UMASK=022,會移除每個新檔案的群組寫入位元,因此檔案會以 0644 建立,剛才設定的共用權限也就無法運作。002 會建立 0664 檔案與 0775 目錄,群組也能寫入:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=Etc/UTC
volumes:
- /srv/appdata/sonarr:/config
- /srv/media:/data
restart: unless-stopped這兩個值應放在 Compose 檔案旁的 .env 檔案中,讓整個堆疊使用同一份定義:
PUID=1000
PGID=13000Compose 會自動讀取該檔案,進行 ${PUID} 樣式的替換;憑證也使用相同機制。不要將值放入 docker-compose.yml,而是放入 .env 檔案的做法同樣適用於此處,差別是這兩個數字不是機密資料。
不要只相信設定檔,應從頭到尾實際驗證。先在一個容器內寫入檔案,再從主機讀取:
docker exec sonarr touch /data/downloads/permtest
ls -ln /srv/media/downloads/permtest正常結果會顯示你的 PUID 為擁有者、13000 為群組,且模式為 -rw-rw-r--。如果群組顯示為 1000,表示該目錄缺少 setgid 位元。如果模式顯示為 -rw-r--r--,表示 UMASK 變數未生效;請確認你重新建立了容器,而不是只重新啟動容器。完成後使用 rm /srv/media/downloads/permtest 移除測試檔案。
哪些映像使用哪些變數
linuxserver.io 映像使用 PUID、PGID 和 UMASK。Paperless-ngx 對相同概念使用不同名稱:USERMAP_UID 和 USERMAP_GID,兩者預設值都是 1000;其文件則指示你從 id -u 和 id -g 讀取這些值。相片伺服器也有相同差異:PhotoPrism 使用專屬的 PHOTOPRISM_UID 和 PHOTOPRISM_GID 組合,Immich 則沒有對應設定,而是讓容器使用 Docker 的 user: 鍵。因此,選擇 PhotoPrism 或 Immich 也會決定你要維護哪一種機制來管理這台伺服器上最大的相片庫。許多官方上游映像,包括常見的資料庫與 Web 伺服器映像,都使用固定的內建使用者,並要求你使用 user:,或維持預設值不變。之後加入的基礎架構也一樣。因此,在應用程式前方加入 Authentik,為應用程式提供單一登入,代表你會執行官方的 server、Postgres 和 Redis 映像;這些映像完全不讀取 PUID,且磁碟區的擁有權由 runtime 決定,而不是由可設定的 entrypoint 決定。
因此,在不同專案之間複製環境變數區塊前,請先查看每個映像的 README。Docker 會將你設定的任何環境變數傳入容器,不論容器內是否有元件讀取該變數;未被使用的 PUID 不會產生錯誤、警告或任何作用。容器會以其 Dockerfile 最後設定的使用者身分執行,而你可以透過它寫入檔案的擁有權確認實際使用的使用者。
FAQ
為什麼我的 Docker 檔案擁有者是 911:911?
911 是內建於 linuxserver.io 映像中的 abc 使用者 UID 和 GID。這表示容器啟動時未設定 PUID 和 PGID,因此其 init script 保留了內建預設值。ls -l 顯示原始數字,因為主機上沒有 ID 為 911 的帳號可對應,因此無法顯示名稱。將 PUID 和 PGID 設為 id 的輸出,使用 docker compose up -d 重新建立容器,然後在受影響的目錄上使用 sudo chown -R 1000:1000 修正現有檔案。
PUID 和 PGID 適用於每個 Docker 映像嗎?
不適用。它們不是 Docker 功能,Docker 也不會讀取這些變數。只有映像本身的 entrypoint 會讀取這些變數,並在啟動應用程式前呼叫 usermod 和 groupmod 時,這些變數才會生效。這類映像包括 linuxserver.io 系列,以及少數採用相同模式的專案。其他專案使用不同名稱,例如 paperless-ngx 中的 USERMAP_UID 和 USERMAP_GID。對於不讀取這些變數的映像,變數會被接受後忽略,且不會顯示警告。
在 Docker Compose 中,我應該使用 PUID 和 PGID,還是 user: key?
映像支援 PUID 和 PGID 時,請使用 PUID 和 PGID,因為 entrypoint 仍會以 root 身分執行一段時間,以正確修正 /config 並啟動自身的服務。映像不支援 PUID,或映像 README 表示已測試非 root 執行時,請使用 user:。在 linuxserver 映像中設定 user: 會使 PUID 和 PGID 失效、停止 Docker Mods 和自訂服務執行,並讓所有掛載磁碟區的權限都由你負責。
Sonarr 的 PUID 正確,但仍無法移動檔案。問題在哪裡?
請依序檢查 3 項。第一,媒體掛載本身:init 只會對 /app、/config 和 /defaults 執行 chown,因此 /data 或 /downloads 會保留主機上的既有擁有權。第二,共用群組:如果下載用戶端和 Sonarr 使用不同的 GID 執行,兩者都無法修改對方的檔案,因此請讓堆疊中的每個容器使用相同的 PGID。第三,umask:映像的預設值 UMASK=022 會將檔案建立為 0644,且不設定群組寫入位元,導致共用群組完全無法運作。設定 UMASK=002,並使用 chmod 2775 在目錄上設定 setgid 位元,讓新檔案繼承該群組。