Docker Compose 中 PUID 和 PGID 是什么
PUID 和 PGID 不是 Docker 设置,而是 linuxserver.io 镜像的入口约定。了解绑定挂载为何生成 911:911 文件,以及如何用实际 UID、GID 和 recreate 修复。
PUID 和 PGID 实际上是什么
PUID 和 PGID 是某些容器镜像在启动时读取的两个环境变量。Docker 本身不会读取它们。这是一种约定,linuxserver.io 镜像以及少数其他镜像会使用它。因此,未实现读取逻辑的镜像会静默忽略这些变量。
在 linuxserver.io 镜像中,有一个名为 abc 的用户。该用户在构建镜像时创建,其 UID(用户 ID)为 911,GID(组 ID)为 911。容器以 root 身份启动并运行初始化脚本,其中一个脚本会在其他操作开始前重新编号该用户:
groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abc-o 标志允许使用已在其他位置占用的 ID。随后,初始化过程会降权,并以 abc 身份运行应用。因此,PUID=1000 不会传递到 Docker。该变量会在应用启动前重新编号容器内的用户,这意味着应用写入的每个文件都会以 1000 作为所有者写入您的磁盘。未设置 PUID 时,abc 会保留 911,这就是未配置的绑定挂载会产生由 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 镜像中,第一个普通用户的 UID:GID 是 1000:1000,但不要依赖这一点。重建服务器,或之后添加第二个账户,都可能得到 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,同样属于 UID 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 创建空的命名卷并将其挂载到镜像中已有的路径上时,会把该路径的内容(包括所有权和权限位)复制到卷中,因此应用会找到一个已经属于自己的目录。bind mount 不会进行这些处理:Docker 会完全按照主机目录的原样进行挂载。这种差异也是了解何时 bind mount 优于命名卷以及何时不应使用 bind mount的实际原因之一。
修复已经错误的目录
设置 PUID 和 PGID 只会改变应用后续的行为,不会追溯修复磁盘上已有的文件。停止服务栈,手动修正所有权,然后重新启动:
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 初始化脚本在启动时只会对以下 3 个路径执行 chown:/app、/config 和 /defaults。您的媒体挂载路径不在其中。/data、/downloads 和 /tv 会原样交给应用程序处理。因此,如果这些挂载在主机端的所有者不是容器用户,容器仍会正常启动,在启动横幅中显示正确的 UID,然后在首次导入时失败。
这是正确的行为。每次容器启动时,都对 12 TB 的媒体库递归执行 chown 将导致严重问题。这意味着媒体目录需要由您负责管理;权限问题实际发生的也是这些挂载路径。
控制用户的3种方式,以及各自的适用场景
PUID 和 PGID 环境变量
这只适用于其 entrypoint 会读取这些变量的镜像。这种方式很常见,因为容器仍以 root 启动,完成自身初始化,修复 /config,然后再降权运行。Docker Mods 和自定义 init 脚本仍可正常工作。代价是,您依赖的是约定,而不是平台功能;不同项目使用的变量名也不统一。
Compose 中的 user: 键
这是 Docker 的原生功能,适用于所有镜像,因为容器运行时会在镜像自身代码运行前应用该设置:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
user: "1000:1000"进程从不以 root 运行,哪怕只有片刻,因此确实能提升安全性。但 entrypoint 中任何需要 root 权限的操作也会失效。对于 linuxserver 镜像,项目仅在合理努力范围内提供支持,而且只支持经过测试的镜像。具体限制包括:PUID 和 PGID 不再生效,Docker Mods 不会运行,自定义服务不会运行,并且您需要负责每个挂载卷的权限。其文档中的配置模式会将该选项与可写的 /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;对于 n 为 1 或更高值的任意 n,容器 UID 会映射到 subuid + (n - 1),其中 subuid 是在 /etc/subuid 中为您分配的 ID 范围起始值,而 /etc/subgid。Docker 要求其中至少有 65,536 个从属 ID。
请重新理解这个映射,因为它与通常的建议相反。在 Rootless Docker 下,容器以 root 身份写入文件时,文件所有者会是您。容器以 UID 1000 写入文件时,文件所有者会是约为 100999 的从属 ID,您的 shell 无法访问这些文件。因此,在 rootful daemon 上正确的 PUID 值,在这里反而是错误的。两种机制在不同层面解决同一个问题;未检查就叠加使用,最终可能导致您需要 sudo 才能删除某个目录。如果使用 Rootless Docker,请先在自己的服务器上测试一个写入文件的所有权,再将数据目录迁移进去。
对于单台 VPS 上的大多数自托管堆栈,在 rootful daemon 上使用 PUID 和 PGID 是更实际的选择,因为镜像就是按这种方式构建和编写文档的。只有在镜像 README 明确说明该镜像经过相应测试,或您运行的是完全不支持 PUID 的官方上游镜像时,才应使用 user:。
媒体栈示例:所有容器共享一个组
包含 Sonarr、Radarr 和下载客户端的 arr 媒体栈是这一问题从理论变为实际的场景。下载客户端将已完成的文件写入 /data/downloads。Sonarr 随后会将该文件创建硬链接,或移动到 /data/media。要创建硬链接,两个容器必须对同一目录树具有写入权限。如果下载客户端以 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 组的 GID 改为 13000,因此 abc 写入文件时使用的 GID 与主机上的 media 组相同。栈中的每个容器都保留自己的 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 中读取这些值。许多官方上游镜像(包括常见的数据库和 Web 服务器镜像)使用固定的内置用户,并要求您使用 user: 或保持默认设置。
因此,在不同项目之间复制环境变量配置块之前,请先查看每个镜像的 README。Docker 会将您设置的任何环境变量传递到容器中,无论容器内是否有程序读取该变量。如果没有任何程序使用 PUID,Docker 不会产生错误或警告,该变量也不会产生任何效果。容器会以其 Dockerfile 最终指定的用户运行。您可以通过容器写入文件的所有权确定该用户。
FAQ
我的 Docker 文件为什么归 911:911 所有?
911 是 linuxserver.io 镜像内置的 abc 用户的 UID 和 GID。出现这个结果,说明容器启动时没有设置 PUID 和 PGID,因此其初始化脚本保留了内置默认值。由于主机上没有 ID 为 911 的账户,ls -l 只能显示原始数字,无法显示名称。将 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。对于既不读取 PUID 也不读取 PGID 的镜像,这些变量会被接受并忽略,且不会发出警告。
在 Docker Compose 中,应使用 PUID 和 PGID,还是使用 user: 键?
镜像支持时,使用 PUID 和 PGID。这样 entrypoint 仍会以 root 身份运行足够长的时间,以正确修复 /config 并启动自身的服务。镜像不支持 PUID,或者镜像 README 声明已针对非 root 运行进行测试时,使用 user:。对于 linuxserver 镜像,设置 user: 会使 PUID 和 PGID 失效,阻止 Docker Mods 和自定义服务运行,并使所有挂载卷的权限都由您负责。
Sonarr 的 PUID 正确,但仍无法移动文件。问题在哪里?
按以下顺序检查 3 项。首先检查媒体挂载本身:初始化脚本只会对 /app、/config 和 /defaults 执行 chown,因此 /data 或 /downloads 会继续保留其在主机上的所有权。其次检查共享组:如果下载客户端和 Sonarr 使用不同的 GID 运行,双方都无法修改对方的文件,因此应为堆栈中的每个容器设置相同的 PGID。最后检查 umask:镜像默认值 UMASK=022 会将文件创建为 0644,且不设置组写入权限,这会使共享组完全无法发挥作用。设置 UMASK=002,并使用 chmod 2775 为目录设置 setgid 位,使新文件继承该组。