Что такое 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. Таким образом, PUID=1000 никогда не передается в Docker. Переменная переназначает пользователя внутри контейнера до запуска приложения, что означает, что все файлы, созданные этим приложением, будут записаны на ваш диск с владельцем 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, но не стоит полагаться на это по умолчанию. На переустановленном сервере или при создании дополнительной учетной записи идентификатор может быть 1001 или выше, и неверное значение здесь станет причиной ошибки. Если ваши сервисы работают от имени выделенной сервисной учетной записи, а не вашего основного пользователя, выполните 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, который пересоздает контейнер.
Почему невозможно удалить файл, созданный контейнером
Ядро сравнивает идентификаторы, а не имена. Ваш 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. Идентификатор действителен, и доступ к файлам работает нормально. Не работает только поиск имени пользователя.
Rootless Docker
Rootless Docker запускает сам демон от имени вашего непривилегированного пользователя, поэтому на хосте ничто не работает от имени реального root. Это полностью меняет арифметику владения. UID 0 в контейнере отображается на UID пользователя хоста, который запустил rootless Docker, а UID контейнера n для любого n от 1 и выше отображается на subuid + (n - 1), где subuid — это база диапазона, выделенного вам в /etc/subuid и /etc/subgid. Docker ожидает там наличие как минимум 65,536 подчинённых идентификаторов.
Перечитайте это правило отображения, так как оно инвертирует привычные рекомендации. В rootless Docker контейнер, записывающий файлы от имени root, создаёт файлы, принадлежащие вам. Контейнер, записывающий файлы от имени UID 1000, создаёт файлы, принадлежащие подчинённому идентификатору в районе 100999, к которым ваша оболочка не имеет доступа. Поэтому значение PUID, верное для обычного демона, здесь будет ошибочным. Эти два механизма решают одну и ту же задачу на разных уровнях, и их совместное использование без проверки приводит к тому, что для удаления директории пользователям приходится использовать sudo. Если вы переходите на rootless, проверьте владельца одного записанного файла на своём сервере, прежде чем переносить в него библиотеку данных.
Для большинства self-hosted стеков на одном VPS выбор PUID и PGID при rootful daemon является практичным решением, поскольку именно для такой схемы собраны и документированы образы. Используйте user:, если в README образа указано, что он протестирован для этой схемы, либо если вы запускаете официальный upstream image, который вообще не поддерживает PUID. Рабочее пространство для документов, например self-hosted instance AFFiNE на одном VPS, относится ко второму случаю: ни один из его контейнеров не читает PUID, а права на каталог базы данных и загруженные файлы определяются runtime, а не параметрами блока environment. То же относится к self-hosted support desk Chatwoot, где Rails container и его Sidekiq worker записывают данные в один каталог uploads и ни один из них не читает PUID. Поэтому этот каталог должен принадлежать тому пользователю, под которым образ уже запускается. Для более нового стека ничего не меняется. Поэтому выделение каждому участнику команды собственного изолированного OneCLI agent оставляет каталоги рабочих пространств и каталог данных Postgres принадлежащими пользователю, под которым уже запускается каждый образ. В результате это проблема user: и chown, а не PUID. Если разместить единый self-hosted API перед Codex, Claude Code и Hermes, будет использоваться та же схема: образ запускается под собственным встроенным пользователем, а bind mount с базой данных и сохранёнными ключами получает права этого пользователя.
Сценарий медиа-стека: общая группа для нескольких контейнеров
Медиа-стек 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. Каждый контейнер в стеке сохраняет свой PUID, но использует один и тот же PGID. Это касается и контейнеров, расположенных далее по цепочке, которые только читают готовую библиотеку, например, самого Jellyfin и фронтендов, которые к нему подключают, таких как Halcyon, представляющий библиотеку в виде интерактивного видеосалона 90-х.
Затем установите UMASK=002 для каждого контейнера linuxserver в стеке. Это шаг, который часто пропускают. По умолчанию в этих образах используется 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Эти два значения следует поместить в файл .env рядом с файлом Compose, чтобы весь стек считывал единое определение:
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 также определяет, какой из этих механизмов вы будете поддерживать для самой большой библиотеки на сервере. Многие официальные upstream-образы, включая стандартные образы баз данных и веб-серверов, поставляются с фиксированным встроенным пользователем и ожидают, что вы будете использовать user: или оставите всё как есть. Небольшие развертывания с одним приложением поднимают тот же вопрос, поэтому при запуске self-hosted трекера тренировок openGym стоит проверить, от имени какого пользователя на самом деле работает контейнер, прежде чем указывать на него bind mount, так как директория, содержащая базу данных, унаследует права этого пользователя независимо от того, задали вы PUID или нет. Реле удаленного доступа попадает в ту же категорию, поэтому, когда вы запускаете собственный реле-сервер RustDesk, пара ключей Ed25519, которую hbbs записывает при первом запуске, оказывается в вашем bind mount с владельцем, соответствующим пользователю, под которым работает образ, и единственным доступным вам исправлением остается chown на стороне хоста. То же самое относится к инфраструктуре, которую вы добавляете позже: установка Authentik перед вашими приложениями для единого входа означает запуск официальных образов сервера, Postgres и Redis, которые вообще не считывают PUID, а права доступа к их томам определяются средой выполнения, а не entrypoint, который можно настроить.
Поэтому проверяйте README каждого образа, прежде чем копировать блок переменных окружения между проектами. Docker передает любую заданную вами переменную окружения в любой контейнер, независимо от того, считывает ли её что-либо внутри, и PUID, которую никто не использует, не вызывает ни ошибки, ни предупреждения, ни какого-либо эффекта. Контейнер работает от имени того пользователя, который был указан в конце его Dockerfile, и вы узнаете об этом по владельцу файлов, которые он создает. Прочитайте документацию перед добавлением чего-либо нового на сервер, включая self-hosted стек сканирования безопасности open-kritt, где Compose-файл подскажет вам, поддерживают ли образы PUID или права доступа к монтируемым директориям фиксируются самими образами.
FAQ
Почему владельцем моих Docker-файлов является 911:911?
911 — это UID и GID пользователя abc, встроенного в образы linuxserver.io. Если вы видите эти значения, значит, контейнер был запущен без указания PUID и PGID, поэтому его скрипт инициализации оставил настройки по умолчанию. 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 и некоторые другие проекты, перенявшие этот подход. Другие проекты используют иные переменные, например USERMAP_UID и USERMAP_GID в paperless-ngx. Если образ не поддерживает ни один из вариантов, переменные будут приняты, но проигнорированы без предупреждения.
Что использовать: PUID и PGID или ключ user: в Docker Compose?
Используйте PUID и PGID, если образ их поддерживает, так как entrypoint запускается от имени root на время, достаточное для исправления /config и корректного запуска сервисов. Используйте user:, если в образе нет поддержки PUID или если в README указано, что он протестирован для работы без root-прав. В образах linuxserver установка user: делает PUID и PGID бесполезными, отключает Docker Mods и пользовательские скрипты, а также возлагает на вас полную ответственность за права доступа ко всем примонтированным томам.
У Sonarr верный PUID, но он всё равно не может перемещать файлы. В чём проблема?
Проверьте три вещи по порядку. Во-первых, сам монтируемый том с медиафайлами: скрипт инициализации меняет владельца только для /app, /config и /defaults, поэтому /data или /downloads сохраняют те права, которые были заданы на хосте. Во-вторых, общая группа: если клиент загрузки и Sonarr работают под разными GID, они не смогут изменять файлы друг друга, поэтому назначьте всем контейнерам в стеке одинаковый PGID. В-третьих, umask: значение по умолчанию UMASK=022 создает файлы с правами 0644 без бита записи для группы, что полностью нивелирует смысл общей группы. Установите UMASK=002 и примените setgid-бит к директориям с помощью chmod 2775, чтобы новые файлы наследовали группу.