SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-09-23

Docker Compose 中 PUID 和 PGID 是什么?

PUID 和 PGID 不是 Docker 设置,而是 linuxserver.io 镜像的入口约定。了解绑定挂载为何生成 911:911,以及如何用正确的 UID、GID 和 docker compose up -d --force-recreate 修复。

PUID 和 PGID 的实际含义

PUID 和 PGID 是某些容器镜像在启动时读取的两个环境变量。Docker 本身不会读取它们。这是一种约定,linuxserver.io 镜像和少数其他镜像会使用它们。因此,未实现相关读取逻辑的镜像会静默忽略这些变量。

在 linuxserver.io 镜像中,有一个名为 abc 的用户。该用户在构建镜像时创建,UID(用户 ID)为 911,GID(组 ID)为 911。容器以 root 身份启动并运行初始化脚本。其中一个脚本会在其他操作开始前,先重新设置该用户的 ID:

groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abc

-o 标志允许使用已在其他位置占用的 ID。随后,初始化过程会降权,并以 abc 身份运行应用。因此,PUID=1000 永远不会传递给 Docker。该变量会在应用启动前重新设置容器内用户的 ID。这意味着应用写入的所有文件都会以 1000 的属主身份落盘。未设置 PUID 时,abc 会保留 911,这就是未配置的绑定挂载会生成由 911:911 所有的文件的原因。

使用 id 获取两个数字

以拥有数据目录的用户身份在主机上运行:

id
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),988(docker)

uid 是您的 PUID,gid 是您的 PGID。在脚本中,id -u 和 id -g 会输出纯数字。大多数全新 VPS 镜像中的第一个普通用户账号为 1000:1000,但不要假定一定如此。重建服务器,或稍后添加第二个账号,可能会得到 1001 或更高的数字;这里的数字错误就是全部问题所在。如果服务以 专用服务账号而不是您自己的登录用户运行,请运行 id thatuser,并从中获取这两个数字。

为什么文件显示为 911:911

如果主机上没有与该 ID 匹配的账户,ls -l 会输出数字 ID,而不是名称。服务器上没有 UID 911,因此没有可输出的名称。使用 ls -ln 可始终显示数字,避免歧义:

ls -ln /srv/appdata/sonarr
drwxr-xr-x 2 911 911 4096 Aug  7 09:12 Backups
-rw-r--r-- 1 911 911  512 Aug  7 09:12 config.xml

该输出表示容器使用了内置默认值运行。列表中的 config.xml 同时也是保存身份验证设置的文件。首次打开 Web UI 时,如果发现 Sonarr 和 Radarr 发布时未设置默认用户名或密码,这一点尤其重要。请从容器内部确认,不要靠猜测:

docker exec sonarr id abc
docker compose logs sonarr | head -n 25

linuxserver 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 优于命名卷以及何时不是的实际原因之一。

修复已经错误的目录

设置 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,会造成严重问题。这意味着媒体目录需要由您负责处理,也是权限最容易出错的挂载点。此类故障通常不会立即显示,而是在容器看似运行正常数小时后,才会出现在应用日志中。定期执行写入测试,并通过 您自己的 VPS 上的 ntfy,将告警推送到手机 发送通知,可以低成本地在一周没有新剧集之前发现问题。

控制用户的三种方式,以及各自的适用场景

PUID 和 PGID 环境变量

此方式仅适用于其 entrypoint 会读取这些变量的镜像。它很常用,因为容器仍以 root 身份启动,先完成自身初始化,修复 /config,然后再降权运行。Docker Mods 和自定义初始化脚本仍可正常工作。代价是你依赖的是一种约定,而不是平台功能,并且不同项目使用的变量名并不统一。

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 的映射方式。容器 UID 0 会映射到运行 rootless Docker 的主机用户 UID;对于 n 为 1 或更大值的任意容器 UID n,则会映射到 subuid + (n - 1),其中 subuid 是分配给你在 /etc/subuid 中的 ID 范围起始值,/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 实例,就属于后一种情况,因为其中没有任何容器会读取 PUID,其数据库目录和上传文件的所有权由运行时决定,而不是由环境变量块中的设置决定。自托管 Chatwoot 工单系统也是如此:Rails 容器和 Sidekiq worker 都会写入同一个上传目录,但二者都不会读取 PUID,因此该目录必须匹配镜像当前运行所使用的用户。对于更新的服务栈,情况也不会改变。因此,为团队中的每个人提供各自隔离的 OneCLI agent时,每个人的工作区目录和 Postgres 数据目录仍会归各镜像当前运行所使用的用户所有,这属于 user: 和 chown 问题,而不是 PUID 问题。在 Codex、Claude Code 和 Hermes 前面放置一个自托管 API也会继承同样的设置,因为该镜像使用自身内置的用户运行,而保存数据库和密钥的 bind mount 会继承该用户的所有权。

媒体栈案例:容器共享同一个组

包含 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 组重新编号为 13000,因此 abc 写入文件时使用的 GID 与主机上的 media 组相同。栈中的每个容器保留自己的 PUID,并共享这一个 PGID。这也包括链路中更靠后的、只读取已完成媒体库的容器,例如 Jellyfin 本身,以及人们为其添加的前端,例如 Halcyon,它将该媒体库呈现为可浏览的 90 年代视频商店。

然后为栈中的每个 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=13000

Compose 会自动读取该文件,用于 ${PUID} 风格的替换。这与凭据使用的机制相同。将值保存在 .env 文件中,而不是写入 docker-compose.yml的做法在这里同样适用,不同之处是这两个数字不是机密。

不要只相信配置,应端到端验证。先在一个容器内写入文件,再从主机读取:

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: 或保持默认设置。小型单应用部署也会遇到同样的问题。因此,部署 自托管的 openGym 锻炼跟踪器 时,先确认其容器实际以哪个用户运行,再为它指定 bind mount。无论是否设置 PUID,存放其数据库的目录都会继承这个用户设置。远程访问中继服务也属于同一类。因此,运行 自己的 RustDesk 中继服务器 时,hbbs 首次启动时写入的 Ed25519 密钥对会以该镜像最终使用的用户身份出现在 bind mount 中;主机端的 chown 是你唯一可用的修正方式。之后添加的基础设施也一样。例如,在应用前端部署 Authentik 以实现统一登录,意味着运行官方的 server、Postgres 和 Redis 镜像。这些镜像完全不读取 PUID,卷的所有权由运行时决定,而不是由可配置的 entrypoint 决定。

因此,在将某个环境变量配置块复制到其他项目之前,请先查看每个镜像的 README。无论容器内部是否读取,Docker 都会将你设置的环境变量传入容器;未被任何组件使用的 PUID 不会产生错误、警告或任何效果。容器会以其 Dockerfile 最终指定的用户运行,文件所有权可以反映这一点。在向主机添加任何新服务之前,先完成这项检查,包括部署 自托管的 open-kritt 安全扫描栈。其中的 Compose 文件会说明这些镜像是否支持 PUID,或者你挂载的目录所有权是否由镜像本身固定。

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,请使用 PUID 和 PGID,因为 entrypoint 仍会以 root 身份运行足够长的时间,以修复 /config 并正确启动自身的服务。如果镜像不支持 PUID,或者镜像 README 声明已针对非 root 运行进行测试,请使用 user:。对于 linuxserver 镜像,设置 user: 会使 PUID 和 PGID 不再生效,阻止 Docker Mods 和自定义服务运行,并使所有挂载卷的权限都由您负责。

Sonarr 使用了正确的 PUID,但仍无法移动文件。问题是什么?

请按以下顺序检查三项内容。首先检查媒体挂载本身:初始化过程只会对 /app、/config 和 /defaults 执行 chown,因此 /data 或 /downloads 会保留其在主机上的所有权。其次检查共享组:如果下载客户端和 Sonarr 使用不同的 GID 运行,二者都无法修改对方的文件,因此应为堆栈中的每个容器设置相同的 PGID。最后检查 umask:镜像默认值 UMASK=022 会将文件创建为 0644,且不设置组写入位,这会使共享组完全无法发挥作用。设置 UMASK=002,并使用 chmod 2775 为目录设置 setgid 位,使新文件继承该组。