Что такое PUID и PGID в Docker Compose
PUID и PGID не являются настройками Docker. Это переменные образов linuxserver.io для смены прав доступа. Узнайте, почему файлы получают UID 911 и как задать владельца хоста.
Что на самом деле представляют собой PUID и PGID
PUID и PGID — это две переменные окружения, которые считываются некоторыми образами контейнеров при запуске. Сам Docker их не использует. Это соглашение, применяемое в образах linuxserver.io и ряде других; если образ не был спроектирован для их обработки, он просто проигнорирует эти переменные.
Внутри образа linuxserver.io существует пользователь abc, созданный на этапе сборки с UID (идентификатором пользователя) 911 и GID (идентификатором группы) 911. Контейнер запускается от имени root, выполняет свои скрипты инициализации, и один из этих скриптов переназначает идентификаторы этого пользователя перед запуском приложения:
groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abcФлаг -o позволяет использовать идентификатор, который уже занят где-то еще. После этого процесс инициализации сбрасывает привилегии и запускает приложение от имени abc. Таким образом, Docker передает PUID=1000 в контейнер как любую другую переменную окружения и никак не взаимодействует с ней. Эта переменная меняет идентификатор пользователя внутри контейнера до запуска приложения. Это означает, что на стандартном rootful-демоне без использования user namespace remapping, вновь созданные файлы обычно сохраняются на диске с владельцем 1000. Запись в уже существующий файл не меняет его владельца. Rootless Docker смещает этот номер, как объясняется далее. Если оставить 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, но не стоит полагаться на это по умолчанию. Распределение зависит от существующих учетных записей и конфигурации: на переустановленном сервере идентификатор 1000 может быть назначен снова, в то время как для последующих учетных записей будут выделены более высокие значения. Если ваши сервисы работают под отдельной сервисной учетной записью, а не под вашим основным логином, выполните id thatuser и возьмите идентификаторы из вывода этой команды.
Почему ваши файлы отображаются как 911:911
ls -l выводит числовой идентификатор вместо имени, если в системе нет учетной записи, соответствующей этому 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Этот вывод соответствует встроенным настройкам контейнера по умолчанию, но сами по себе существующие файлы не доказывают, от имени какого пользователя приложение работает сейчас. Файл config.xml в этом списке также является файлом, в котором хранятся настройки аутентификации, что важно при первом открытии веб-интерфейса, когда вы обнаруживаете, что Sonarr и Radarr поставляются без имени пользователя или пароля по умолчанию. Подтвердите это изнутри контейнера, вместо того чтобы гадать:
docker exec sonarr id abc
docker compose logs sonarr | head -n 25Скрипт инициализации linuxserver выводит результат в лог запуска двумя строками:
User UID: 911
User GID: 911Если после установки PUID=1000 в вашем файле Compose эти строки содержат 911, значит, запущенный контейнер не использует заданное вами значение. Обычно это происходит из-за того, что вы отредактировали docker-compose.yml и выполнили docker compose restart, который повторно использует существующий контейнер с его первоначальными переменными окружения. Для применения изменений в переменных окружения требуется docker compose up -d, который пересоздает контейнер. Другая причина — намеренная: когда образ работает в режиме read-only или non-root, скрипт инициализации полностью пропускает переназначение ID, даже если переменная присутствует в контейнере. В режиме read-only баннер сообщает неизменный ID пользователя abc; в режиме non-root он сообщает владельца /run.
Почему невозможно удалить файл, созданный контейнером
Ядро сравнивает идентификаторы, а не имена. Ваш shell работает от имени UID 1000. Файл принадлежит UID 911. Директория, содержащая его, имеет путь drwxr-xr-x и также принадлежит пользователю 911, поэтому для группы и остальных пользователей доступны только чтение и выполнение, но не запись. Удаление файла требует прав на запись в директорию, в которой он находится, а не в сам файл, поэтому вы получаете ошибку, даже если сам файл выглядит безобидно:
rm: cannot remove '/srv/appdata/sonarr/config.xml': Permission deniedКонтейнер, пытающийся записать данные, сталкивается с той же проблемой с другой стороны. Если хостовая директория принадлежит вашему пользователю с правами 755, а приложение работает от имени 911, первая же попытка записи завершится ошибкой Permission denied, и приложение сообщит об этом в своих логах. В .NET-приложениях, таких как Sonarr или Radarr, это выглядит как UnauthorizedAccessException: Access to the path '/data/downloads' is denied. Строка прав доступа перед именем файла показывает, какой из трех наборов прав применяется к вам в данный момент, и правильное чтение drwxr-xr-x превращает эту ошибку из загадочной в очевидную.
Это проблема, характерная именно для bind mount. Когда Docker создает пустой именованный том (named volume) и монтирует его поверх пути, существующего в образе, он копирует содержимое этого пути в том, включая владельца и биты прав доступа, поэтому приложение получает директорию, которой оно уже владеет. 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 при запуске выполняет chown для трёх путей: /app, /config и /defaults. Ваши точки монтирования медиафайлов в этот список не входят. /data, /downloads и /tv передаются приложению без изменений. Если на хостовой стороне этих монтирований установлены права доступа, не позволяющие пользователю контейнера выполнять запись, контейнер запустится без ошибок, выведет корректный UID в приветственном сообщении, но завершится с ошибкой при первой попытке импорта.
Это правильное поведение. Рекурсивный chown для медиабиблиотеки объемом двенадцать терабайт при каждом запуске контейнера стал бы катастрофой. Это означает, что за права доступа к медиадиректориям отвечаете вы, и именно в этих точках монтирования чаще всего возникают проблемы. Поскольку такие ошибки проявляются в логах приложения лишь спустя часы после того, как контейнер перешел в состояние «здоров», периодический тест записи, настроенный через ntfy на вашем собственном VPS с отправкой уведомлений на телефон, является простым способом узнать о проблеме до того, как вы обнаружите отсутствие эпизодов за целую неделю.
Три способа управления пользователем и области их применения
Переменные окружения PUID и PGID
Этот метод работает только с образами, точка входа (entrypoint) которых считывает данные переменные. Он популярен, так как контейнер всё равно запускается от имени root, выполняет собственную настройку, исправляет /config и только после этого понижает привилегии. Docker Mods и пользовательские скрипты инициализации продолжают работать. Минус в том, что вы полагаетесь на соглашение, а не на встроенную функцию платформы, к тому же имена переменных различаются в разных проектах.
Параметр user: в Compose
Это полноценная функция Docker, работающая с любым образом, так как среда выполнения контейнера применяет её до запуска кода самого образа:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
user: "1000:1000"Процесс никогда не запускается от имени root, даже на мгновение, что даёт реальное преимущество в безопасности. Однако это ломает всё в точке входа, что требовало прав 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 запускает сам демон от имени вашего непривилегированного пользователя, поэтому демон и контейнеры работают без прав root на хосте. Это полностью меняет арифметику владения файлами. UID 0 в контейнере отображается на UID пользователя хоста, который запустил Rootless Docker, а UID n в контейнере для любого n от 1 и выше отображается на subuid + (n - 1), где subuid — это база диапазона, выделенного вам в /etc/subuid; групповые ID следуют тому же правилу с использованием /etc/subgid. Docker ожидает наличия там как минимум 65,536 подчинённых ID.
Перечитайте правила отображения, так как они инвертируют привычные рекомендации. В Rootless Docker контейнер, создающий файл от имени root, обычно создаёт файл, владельцем которого являетесь вы. Контейнер, создающий файл с UID 1000, обычно создаёт файл, принадлежащий подчинённому ID, например 100999, если диапазон начинается с 100000. Доступ из вашей оболочки зависит от прав доступа и ACL; удаление обычно зависит от прав на родительскую директорию, поэтому вы можете удалить файл из директории, которую контролируете. Таким образом, значение PUID, корректное для обычного демона, здесь будет неверным. Эти два механизма меняют владельца на разных уровнях, и их совместное использование без проверки может привести к тому, что вы не сможете записывать данные в смонтированные тома. Если вы переходите на Rootless, проверьте владельца созданного файла на своём сервере, прежде чем переносить туда библиотеку.
Для большинства self-hosted стеков на одном VPS использование PUID и PGID на обычном демоне является прагматичным выбором, так как именно под это образы созданы и задокументированы. Используйте user:, когда в README образа указано, что он протестирован для этого, или когда вы запускаете официальный upstream-образ, в котором вообще нет поддержки PUID. Рабочее пространство с документами, такое как self-hosted экземпляр AFFiNE на одном VPS, относится к последнему случаю, так как ни один из его контейнеров не считывает PUID, а владение директорией базы данных и загруженными файлами определяется средой выполнения, а не блоком переменных окружения. То же самое верно для self-hosted службы поддержки Chatwoot, где контейнер Rails и его воркер Sidekiq записывают данные в одну директорию загрузок, и ни один из них не считывает PUID, поэтому права на эту директорию должны соответствовать пользователю, от имени которого образ уже запущен. Это не меняется и для новых стеков, поэтому предоставление каждому члену команды собственного изолированного агента OneCLI приводит к тому, что директории рабочих пространств и директория данных Postgres принадлежат пользователю, от имени которого уже запущен каждый образ, что делает это проблемой user: и chown, а не PUID. Разместите единый self-hosted API перед Codex, Claude Code и Hermes, и его точка входа будет управлять несколькими идентификаторами: контейнер должен запускаться от root, консоль и шлюз работают от непривилегированного пользователя, а каждая сессия агента — от собственного пользователя. Не добавляйте user: в это развёртывание; его точка входа сама управляет правами доступа к тому с данными.
Сценарий медиа-стека: общая группа для нескольких контейнеров
Медиа-стек arr с Sonarr, Radarr и клиентом для загрузки — это тот случай, когда теория переходит в практику. Клиент для загрузки записывает готовый файл в /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 {} +Первый 2 в 2775 — это бит setgid. Для каталога это означает, что каждый новый файл и подкаталог внутри него наследует группу media, а не основную группу создателя. Благодаря этому настройка сохраняется для новых загрузок без необходимости повторного запуска chown. Выйдите из системы и войдите снова или выполните newgrp media перед проверкой доступа: группа, добавленная через usermod -aG, не появится в уже открытом сеансе оболочки.
Внутри контейнера groupmod -o -g 13000 abc переназначает группу abc на 13000, поэтому abc выполняет запись с тем же GID, что и ваша группа media на хосте. Каждый контейнер linuxserver, обращающийся к этому дереву, сохраняет свой PUID и использует этот общий PGID. Это относится и к контейнерам, расположенным далее по цепочке, которые только читают готовую библиотеку, например, к самому Jellyfin. Фронтенд, такой как Halcyon, который представляет библиотеку в виде каталога видео 90-х, подключается к Jellyfin по HTTP с использованием HALCYON_JELLYFIN_URL; его стандартное развертывание не требует ни общего PGID для медиа, ни монтирования медиа-каталога.
Затем установите UMASK=002 для каждого контейнера linuxserver в стеке. Это шаг, который часто пропускают. Если значение не задано, приложение сохраняет унаследованную umask, если только что-то другое не изменит её; пример значения в README образов — UMASK=022. При эффективной umask 022 приложение, запрашивающее 0666 для нового файла, получает 0644 при отсутствии ACL по умолчанию: остальные члены группы могут читать файл, но не могут его изменять, что сводит на нет большую часть настроенного общего доступа. При 002 и отсутствии ACL по умолчанию приложение, запрашивающее обычные 0666 для файлов и 0777 для каталогов, получает файлы 0664 и, внутри дерева с setgid, каталоги 2775, что позволяет группе выполнять запись. Umask только удаляет биты, поэтому приложение, запрашивающее 0600, всё равно получит 0600, а каталог с ACL по умолчанию полностью игнорирует umask. Добавьте переменную рядом с PUID и PGID:
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Эти два значения должны находиться в файле .env рядом с файлом Compose, чтобы весь стек считывал одно определение:
PUID=1000
PGID=13000Compose автоматически считывает этот файл для подстановки в стиле ${PUID} — это тот же механизм, который вы используете для учетных данных. Привычки хранить значения вне docker-compose.yml, в файле .env применимы и здесь, с той разницей, что эти два числа не являются секретными.
Проверьте всё от начала до конца, вместо того чтобы доверять конфигурации. Создайте новый файл от имени пользователя приложения, применив настроенную umask, и прочитайте его с хоста. Используйте тестовое имя файла, которого еще не существует:
docker exec --user abc sonarr sh -c 'umask "$UMASK" && touch /data/downloads/permtest'
ls -ln /srv/media/downloads/permtestКорректный результат показывает ваш PUID в качестве владельца, 13000 в качестве группы и -rw-rw-r-- в качестве прав доступа. Если группа отображается как 1000, выполните ls -ldn /srv/media/downloads: либо бит setgid (s в drwxrwsr-x) отсутствует в этом каталоге, либо сам каталог всё еще принадлежит группе 1000, а не media. Команды chown и chmod 2775 выше исправляют обе проблемы. Если права доступа отображаются как -rw-r--r--, проверьте значение UMASK контейнера и ACL по умолчанию для каталога; пересоздайте контейнер, если его окружение устарело. Этот тест проверяет создание файла от имени abc с настроенной маской; режим создания самого приложения и эффективная 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 не предоставляет эквивалента и оставляет выбор пользователя контейнера за ключом user: в Docker, поэтому выбор между PhotoPrism и Immich также определяет, какой из этих механизмов вы будете поддерживать для самой большой библиотеки на сервере. Многие официальные upstream-образы, включая распространенные образы баз данных и веб-серверов, поставляются с фиксированным встроенным пользователем и ожидают, что вы будете использовать user: или оставите всё как есть. Небольшие развертывания с одним приложением поднимают тот же вопрос, поэтому при запуске самостоятельно хостируемого трекера тренировок openGym стоит проверить, от имени какого пользователя на самом деле работает контейнер, прежде чем указывать на него bind mount, так как директория, содержащая базу данных, наследует этот идентификатор вне зависимости от того, задали вы PUID или нет. Реле удаленного доступа попадает в ту же категорию, поэтому, когда вы запускаете собственный релей-сервер RustDesk, пара ключей Ed25519, которую hbbs записывает при первом запуске, оказывается в вашем bind mount с владельцем, соответствующим пользователю, под которым работает образ. Вы можете исправить это позже с помощью chown на хосте или выбрать идентификатор заранее с помощью ключа user:, так как этот образ не задает своего пользователя, при условии, что смонтированная директория доступна для записи выбранному вами ID. То же самое относится к инфраструктуре, которую вы добавляете позже, поэтому установка Authentik перед вашими приложениями для единого входа означает запуск официальных образов authentik и Postgres, которые вообще не считывают PUID, а права доступа к их томам определяются пользователями, от имени которых работают процессы, и настройками, выполняемыми их точками входа (entrypoints). Authentik удалил зависимость от Redis в релизе 2025.10.
Поэтому проверяйте README каждого образа, прежде чем копировать блок переменных окружения между проектами. Docker передает любую заданную вами переменную окружения в любой контейнер, независимо от того, считывает ли ее что-либо внутри, и PUID, который никем не используется, не вызывает ни ошибки, ни предупреждения, ни какого-либо эффекта. Контейнер запускается от имени пользователя, выбранного настройкой user: в Docker или в Dockerfile, а его точка входа может затем понизить привилегии перед запуском приложения. Изучите документацию до того, как добавите что-то новое на сервер, включая самостоятельно хостируемый стек сканирования безопасности open-kritt, где Compose-файл подскажет, поддерживают ли образы PUID или права доступа к монтируемым директориям фиксируются самими образами.
FAQ
Почему мои файлы Docker принадлежат пользователю 911:911?
911 — это UID и GID пользователя abc, встроенного в образы linuxserver.io. Это стандартные значения по умолчанию, но файлы могут сохранять их после того, как вы измените PUID и PGID контейнера. ls -l отображает «сырые» числовые идентификаторы, когда в вашей системе нет учетной записи с такими ID, поэтому отобразить имя невозможно. Установите PUID и PGID в значения, полученные через id, пересоздайте контейнер с помощью docker compose up -d, а затем исправьте владельца существующих файлов командой sudo chown -R 1000:1000 для нужной директории.
Работают ли PUID и PGID в любом образе Docker?
Нет. Это не функция Docker, и Docker их не считывает. Они работают только в тех образах, чей entrypoint считывает эти переменные и вызывает usermod и groupmod перед запуском приложения. К ним относятся образы семейства linuxserver.io и некоторые другие проекты, использующие этот подход. Другие проекты применяют иные переменные, например, USERMAP_UID и USERMAP_GID в paperless-ngx. Если образ не поддерживает ни один из вариантов, переменные будут приняты, но проигнорированы без предупреждения.
Что использовать: PUID и PGID или ключ user: в Docker Compose?
Используйте PUID и PGID, если образ их поддерживает, так как entrypoint успевает отработать от имени root, чтобы исправить /config и корректно запустить службы. Используйте user:, если в документации образа указана поддержка выбранного пользователя; отсутствие поддержки PUID не означает, что entrypoint будет работать без прав root. В образах linuxserver установка user: делает PUID и PGID бесполезными, отключает Docker Mods и пользовательские скрипты, а также перекладывает ответственность за права доступа на все смонтированные тома на вас.
У Sonarr правильный PUID, но он всё равно не может перемещать файлы. В чём проблема?
Проверьте три вещи по порядку. Во-первых, сам монтируемый том с медиафайлами: скрипт настройки пользователя в базовом образе выполняет chown для /app, /config и /defaults, поэтому /data или /downloads сохраняют те права, которые у них есть на хосте. Во-вторых, общая группа: предоставьте контейнерам, имеющим доступ к дереву медиафайлов, одинаковый PGID и права на запись для группы. В-третьих, umask: эффективная маска 022 создает файлы с правами 0644, когда приложение запрашивает 0666 и отсутствуют ACL по умолчанию, поэтому другие контейнеры в группе могут читать их, но не изменять. Если оставить UMASK не заданным, это не гарантирует определенную маску или режим доступа к файлам. Установите UMASK=002 и примените setgid-бит к директориям с помощью chmod 2775, чтобы новые файлы наследовали группу. Приложение, запрашивающее 0600, всё равно создает приватные файлы, а ACL по умолчанию меняет способ назначения прав при создании.