PUID і PGID у Docker Compose: як виправити UID 911
PUID і PGID не є налаштуваннями Docker. Пояснюємо convention linuxserver.io, чому bind mount отримує власника 911:911 і як це виправити.
Що насправді означають PUID і PGID
PUID і PGID — це дві змінні середовища, які під час запуску зчитують певні образи контейнерів. Сам Docker їх не перевіряє. Це загальноприйнята домовленість, яку використовують образи linuxserver.io та деякі інші образи. Якщо образ не запрограмований на їх зчитування, він мовчки їх ігнорує.
В образі linuxserver.io є користувач abc. Його створено під час збирання образу з UID (ідентифікатором користувача) 911 і GID (ідентифікатором групи) 911. Контейнер запускається від імені root, виконує init-скрипти, і один із них спочатку змінює ідентифікатори цього користувача:
groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abcПрапорець -o дає змогу використати ідентифікатор, який уже використовується в іншому місці. Після цього init знижує привілеї та запускає застосунок від імені 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 виводить числовий ідентифікатор замість імені, якщо на хості немає облікового запису, що відповідає цьому ідентифікатору. На сервері немає 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 25Скрипт ініціалізації linuxserver виводить результат у startup log двома рядками:
User UID: 911
User GID: 911Якщо після встановлення PUID=1000 у файлі Compose ці рядки містять 911, змінна не потрапила до контейнера. Зазвичай причина полягає в тому, що ви відредагували docker-compose.yml, а потім виконали docker compose restart, яка повторно використовує наявний контейнер із початковим оточенням. Для застосування змін оточення потрібна docker compose up -d, яка повторно створює контейнер.
Чому не можна видалити файл, створений контейнером
Ядро порівнює числові ідентифікатори, а не імена. Ваша оболонка працює з UID 1000. Файл належить UID 911. Каталог, у якому він міститься, має права drwxr-xr-x і також належить UID 911, тому група та інші користувачі мають права на читання й виконання, але не на запис. Для видалення файлу потрібні права на запис до його каталогу, а не до самого файлу. Тому ви отримуєте таку помилку, навіть якщо сам файл виглядає нешкідливим:
rm: cannot remove '/srv/appdata/sonarr/config.xml': Permission deniedКонтейнер, який записує дані, стикається з тією самою проблемою з іншого боку. Якщо каталог на хості належить вашому користувачу й має режим 755, а застосунок працює від UID 911, його перша спроба запису завершується помилкою Permission denied, і застосунок повідомляє про неї у власному форматі. У .NET-застосунку, такому як Sonarr або Radarr, це відображається як UnauthorizedAccessException: Access to the path '/data/downloads' is denied. Рядок прав перед іменем файлу показує, до якого з трьох наборів прав застосовується перевірка. Уміння правильно читати drwxr-xr-x перетворює цю помилку з незрозумілої на очевидну.
Це саме проблема bind mount. Коли Docker створює порожній іменований том і монтує його поверх шляху, який уже існує в image, він копіює в том вміст цього шляху, зокрема власника та біти прав доступу. Тому застосунок отримує каталог, яким уже володіє. 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 не виправляють
Ось момент, через який виникають проблеми навіть у тих, хто все налаштував правильно. Під час запуску init-скрипт linuxserver змінює власника рівно трьох шляхів: /app, /config і /defaults. Монтування медіаданих до цього списку не входять. Шляхи /data, /downloads і /tv передаються застосунку без змін. Тому, якщо на стороні хоста ці монтування належать користувачу, якому користувач контейнера не може записувати, контейнер запускається без помилок, виводить правильний UID у банері, а потім завершується з помилкою під час першого імпорту.
Це правильна поведінка. Рекурсивний chown для медіатеки обсягом дванадцять терабайтів під час кожного запуску контейнера був би катастрофою. Водночас це означає, що за каталоги з медіаданими відповідаєте ви. Саме в цих монтуваннях найчастіше виникають проблеми з правами доступу.
Три способи керувати користувачем і коли кожен із них застосовний
Змінні середовища PUID і PGID
Цей спосіб працює лише з образами, у яких entrypoint читає ці змінні. Він популярний, тому що контейнер спочатку запускається від імені root, виконує власне налаштування, виправляє /config і лише після цього знижує привілеї. Docker Mods і власні init-скрипти продовжують працювати. Недолік у тому, що ви покладаєтеся на домовленість, а не на функцію платформи, і назви змінних не є стандартними для всіх проєктів.
Ключ user: у Compose
Це справжня функція 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. Ідентифікатор дійсний, а доступ до файлів працює нормально. Не працює лише пошук імені.
Rootless Docker
Rootless Docker запускає сам daemon від імені вашого непривілейованого користувача, тому на хості ніщо не працює від імені справжнього root. Це повністю змінює відповідність власників. UID 0 контейнера відповідає UID користувача хоста, від імені якого працює rootless Docker, а UID контейнера n для будь-якого n зі значенням 1 або більше відповідає subuid + (n - 1), де subuid — початок діапазону, виділеного для вас у /etc/subuid і /etc/subgid. Docker очікує, що там буде щонайменше 65,536 додаткових ID.
Прочитайте цю відповідність ще раз, оскільки вона змінює стандартну рекомендацію на протилежну. У rootless Docker контейнер, який записує файли від імені root, створює файли, власником яких єте ви. Контейнер, який записує файли з UID 1000, створює файли, власниками яких є додаткові ID приблизно в діапазоні 100999, і ваша оболонка не може до них звернутися. Тому значення PUID, правильне для daemon із правами root, у цьому випадку неправильне. Ці два механізми вирішують ту саму проблему на різних рівнях. Якщо поєднати їх без перевірки, можна отримати каталог, для видалення якого знадобиться sudo. Якщо ви переходите на rootless, перевірте власника одного створеного файла на власному сервері, перш ніж переносити до нього бібліотеку.
Для більшості self-hosted стеків на одному VPS практичним вибором є PUID і PGID у daemon із правами root, оскільки саме для цього налаштовані й документовані відповідні образи. Використовуйте user:, коли в README образу зазначено, що його протестовано з цим параметром, або коли ви запускаєте офіційний upstream-образ, який взагалі не підтримує PUID. Робочий простір для документів, наприклад self-hosted екземпляр AFFiNE на одному VPS, належить до останнього випадку, оскільки жоден із його контейнерів не читає PUID, а власники каталогу бази даних і завантажених файлів визначаються середовищем виконання, а не будь-чим у блоці змінних середовища.
Випадок із медіастеком: одна спільна група для контейнерів
Медіастек arr із Sonarr, Radarr і клієнтом завантажень — це практичний приклад, де теорія перестає бути теорією. Клієнт завантажень записує готовий файл у /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 {} +Початковий 2 у 2775 — це біт setgid. Для каталогу це означає, що кожен новий файл і підкаталог успадковує групу media, а не основну групу користувача, який його створив. Тому ця схема працює і для нових завантажень без повторного виконання chown. Вийдіть із системи та увійдіть знову або виконайте newgrp media, перш ніж перевіряти власний доступ. Група, додана за допомогою usermod -aG, не з’являється у вже відкритій сесії shell.
У контейнері groupmod -o -g 13000 abc змінює GID групи abc на 13000. Тому abc записує файли з тим самим GID, що й група media на хості. Кожен контейнер у стеку зберігає власний PUID і використовує один спільний PGID.
Потім встановіть 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 не постачає еквівалентних змінних і передає визначення користувача контейнера ключу user: у Docker. Тому вибір між PhotoPrism та Immich також визначає, який із цих механізмів вам доведеться підтримувати для найбільшої бібліотеки на сервері. Багато офіційних upstream-образів, зокрема поширені образи баз даних і вебсерверів, містять фіксованого вбудованого користувача й очікують, що ви використаєте user: або залишите все без змін. Те саме стосується інфраструктури, яку ви додасте пізніше. Тому встановлення Authentik перед застосунками для єдиного входу означає використання офіційних образів сервера, Postgres і Redis, які взагалі не читають PUID, а права власності на томи визначаються середовищем виконання, а не entrypoint-скриптом, який можна налаштувати.
Тому перед копіюванням блоку змінних середовища між проєктами перевірте README кожного образу. Docker передає будь-яку задану вами змінну середовища в будь-який контейнер незалежно від того, чи читає її щось усередині. Якщо PUID не використовується жодним компонентом, це не спричинить помилки, попередження або будь-якої дії. Контейнер запускається від імені користувача, якого визначено в його власному Dockerfile. Дізнатися це можна за правами власності на файли, які він створює.
FAQ
Чому мої Docker-файли належать 911:911?
911 — це UID і GID користувача abc, вбудованого в образи linuxserver.io. Це означає, що контейнер запущено без заданих PUID і PGID, тому його init-скрипт залишив вбудовані значення за замовчуванням. 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. В образі, який не зчитує жодну з цих змінних, вони приймаються та ігноруються без попередження.
Що використовувати в Docker Compose: PUID і PGID чи ключ user:?
Використовуйте PUID і PGID, якщо образ їх підтримує, оскільки entrypoint ще достатньо довго працює від імені root, щоб виправити /config і правильно запустити власні сервіси. Використовуйте user:, якщо образ не підтримує PUID або в README образу зазначено, що він протестований для роботи без root. В образі linuxserver встановлення user: робить PUID і PGID неактивними, зупиняє виконання Docker Mods і власних сервісів та покладає відповідальність за права доступу до всіх змонтованих томів на вас.
Sonarr має правильний PUID, але все одно не може переміщувати файли. У чому проблема?
Перевірте три речі в такому порядку. По-перше, сам media mount: init змінює власника лише для /app, /config і /defaults, тому /data або /downloads зберігає права власності, задані на хості. По-друге, спільну групу: якщо клієнт завантаження і Sonarr працюють із різними GID, жоден із них не може змінювати файли іншого, тому призначте однаковий PGID усім контейнерам у стеку. По-третє, umask: значення за замовчуванням UMASK=022 в образі створює файли з правами 0644 без біта запису для групи, що повністю унеможливлює використання спільної групи. Встановіть UMASK=002 і задайте біт setgid для каталогів за допомогою chmod 2775, щоб нові файли успадковували групу.