Docker: права UID/GID — фікс томів, що припиняє «Permission denied»

Було корисно?

Ви вчинили правильно: змонтували том, щоб ваші дані пережили перезапуски контейнера.
Потім ваше застосування стартує і падає з Permission denied, ніби воно щойно познайомилося з Linux.

Це не «баг Docker». Це Linux, який робить саме те, про що ви попросили — просто не те, що ви мали на увазі.
Контейнери не мають «користувачів» у звичному сенсі. У них є числа. І ваш том теж належить числам.
Коли ці числа не співпадають — отримуєте класичну виробничу відповідь: «ні».

Справжня проблема: користувачі — це числа, томи — це файлові системи

Docker не придумав права доступу. Він їх успадкував. Процес у контейнері виконується з UID та однією або кількома GID,
так само як будь-який інший процес у Linux. Том (bind mount або іменований volume) підкріплений реальною файловою системою з реальним
власником інодів (UID/GID) і бітами режиму (rwx). Docker просто з’єднує обидва боки.

Коли образ каже «запустити як appuser», це насправді означає під час виконання «запустити як UID 1001 (або 999, або 70, або який обрав автор образу)».
Тим часом ваша директорія на хості може належати UID 1000. Або root. Або LDAP-UID, якого в контейнері немає.
Linux не дивиться на імена. Він порівнює цілі числа. Якщо вони не співпадають і немає дозволу через групу/ACL — доступ відхиляється.

Іменований том проти bind mount: пастка прав виглядає по-різному

Два поширені способи монтування — два трохи різних режими збоїв:

  • Bind mount (-v /host/path:/container/path): контейнер бачить хостову директорію без змін, включно з власністю та ACL.
    Це добре для налагодження; жорстко для уникнення ефекту «працює на моїй машині».
  • Іменований том (-v myvol:/container/path): Docker створює і керує директорією під своїм коренем даних.
    Власність все одно базується на UID/GID, але тепер директорію створюють за замовчуванням Docker/демон і інколи «допомагають» entrypoint образу.

Неприємна правда: chmod 777 — це не виправлення

Якщо ви колись «вирішували» це за допомогою chmod -R 777, ви не виправили права. Ви оголосили банкрутство.
Зазвичай це працює, але потім повертається у вигляді інциденту безпеки, висновку аудиту або дивної історії з корупцією даних.

Правильний підхід нудний: зробіть так, щоб runtime UID/GID контейнера відповідали власності тому (або навпаки), явно і відтворювано.

Цитата, яку варто тримати під рукою:
«Hope is not a strategy.» — Gene Kranz

Налагодження прав — це місце, де надія помирає.

Швидкий план діагностики

Коли ви на виклику, вам не потрібна лекція. Потрібен швидкий шлях до кореня проблеми.
Ось порядок дій, який зазвичай швидко закінчує страждання.

Перше: підтвердьте, під яким UID/GID насправді працює процес

  • Перевірте користувача виконання в контейнері (id всередині контейнера, або подивіться User в конфігурації).
  • Якщо це root і все одно не працює — підозрюйте SELinux/AppArmor, NFS root-squash або монтування в режимі тільки для читання.

Друге: перевірте тип монтування і власника/режим на диску

  • Це bind mount чи іменований том?
  • На хості подивіться власність (stat) і будь-які ACL (getfacl).

Третє: перевірте файлову систему та безпекові шари

  • SELinux увімкнено? Немає міток :Z/:z на bind mount?
  • NFS/CIFS з root-squash або змапленими ідентичностями?
  • Rootless Docker або user namespaces зрушують ідентичності UID?

Потім: оберіть одну стратегію виправлення і зробіть її детермінованою

  • Кращий дефолт: запускайте контейнер з хостовим UID/GID і попередньо створюйте директорії.
  • Альтернатива: один раз зробіть chown тому (акуратно) через ініціалізаційний крок, а не при кожному старті.
  • Уникайте: постійних рекурсивних chown в entrypoint для великих даних.

Жарт №1: Рекурсивний chown — найближча річ у Linux до додатка для медитації. Він змушує вас сидіти й думати про свої вибори.

Фікс томів, який справді працює

Виправлення, яке переживає перебудови, зміну вузлів і людську креативність, просте:
вирівняйте UID/GID між процесом контейнера і власністю тому.
Робіть це явно, а не «якось те, що зробив автор образу».

Найнадійніший патерн для Docker Compose

Коли ви контролюєте хостову директорію і використовуєте bind mounts, вкажіть user в Compose як UID/GID хостового користувача,
і зробіть хостову директорію власністю цього UID/GID.

cr0x@server:~$ id
uid=1000(cr0x) gid=1000(cr0x) groups=1000(cr0x),27(sudo),998(docker)

Рішення: якщо ваш сервіс має записувати в bind-mounted директорію, що належить користувачу розгортання (UID 1000),
запускайте контейнер як 1000:1000. Ви не «робите його менш безпечним». Ви робите його передбачуваним.

Приклад фрагмента Compose (концептуально; реалізуйте у вашому стеку):

  • Хост: /srv/myapp/data належить 1000:1000, режим 0750 або жорсткіше.
  • Контейнер: процес виконується як 1000:1000.

Якщо потрібно використовувати іменований том

Іменовані томи підходять. Вони також досить непрозорі, щоб команди почали гадати.
Розумний підхід такий:

  1. Створіть том.
  2. Ініціалізуйте власність один раз — в контрольованому, аудитуємому кроці.
  3. Запускайте реальний додаток як непривілейований UID, що відповідає цій власності.

Антипатерн — дозволяти головному контейнеру запускатися як root лише щоб «полагодити права» на старті.
Саме так сервіс стає root назавжди, бо хтось боїться торкнутися його.

А як щодо «просто chown»?

Chown може бути коректним. Він також може бути катастрофою.
На великих томах рекурсивний chown — повне сканування файлової системи; на мережевому сховищі — повільна DDoS-подія.
Хитрість — робити це один раз і тільки якщо ви впевнені, що цілите в потрібний шлях.

Патерни, що працюють (і чому)

Патерн A: запускати контейнер як хостовий UID/GID (кращий дефолт для bind mounts)

Це підхід «задовольніть Linux». Файлова система вже має власника. Підгоніть під нього процес.
Це уникає chown-шторму і добре працює, коли у вашого процесу вже є стабільна сервісна облікова запис.

Потрібно, щоб застосунок міг працювати не під root. Більшість сучасних образів так і роблять.
Якщо образ наполягає на root без причин — вважайте це запахом проблем.

Патерн B: ініціалізувати власність тому один раз (кращий дефолт для іменованих томів)

Ви створюєте невеликий одноразовий контейнер, чия єдина робота — створити директорії і встановити власність/ACL,
потім запускаєте додаток як звичайний непривілейований користувач.

Правильно зроблено — детерміновано. Неправильно — це пастка з додатковими кроками. Різниця в тому, що ви цілите точні шляхи,
уникаєте рекурсивного chown, якщо це не потрібно, і логируєте свої дії.

Патерн C: ACL замість власності (добре, коли кілька UID потребують запису)

Іноді у вас є сайдкари або декілька контейнерів, що пишуть в один і той же монтуваний шлях (агент резервного копіювання, відправник логів, застосунок).
Власність не може задовольнити всіх, якщо не змусити всіх використовувати один UID (а це ускладнює).
ACL дозволяють надати права запису додатковим UID/GID без зроблення директорії доступною для всіх.

Патерн D: групова власність + setgid-директорії (класика Unix, досі працює)

Якщо кілька процесів повинні створювати файли в одній директорії, зробіть її групозаписуваною і встановіть біт setgid на директорії.
Нові файли успадковують групу директорії. Це одна з тих давніх фіч Unix, яка тихо вирішує реальні проблеми.

Патерн E: Rootless Docker та user namespaces (безпечніше, але змінюють арифметику)

Rootless Docker і userns-remap — відмінні рішення безпеки. Вони також переписують мапінг UID/GID.
Ваш контейнерний UID 0 може мапитися на хостовий UID 100000+.
Це означає, що хостова директорія, яка належить UID 0, більше не буде записуваною «root» в контейнері — бо це не справжній root на хості.

Це не баг. Це сенс.

Жарт №2: «Просто запустіть як root» — еквівалент «просто перезавантажте прод» для контейнерів. Працює, але видає вас.

Практичні завдання: команди, виводи, рішення

Нижче реальні завдання, які ви можете виконати під час інциденту або для профілактичного укріплення.
Кожне включає: команду, що означає її вивід та яке рішення прийняти.

Завдання 1: визначити effective UID/GID контейнера

cr0x@server:~$ docker exec -it myapp sh -lc 'id && umask'
uid=1001(app) gid=1001(app) groups=1001(app)
0022

Значення: процес виконується як UID/GID 1001 і створює файли з umask 0022 за замовчуванням.
Рішення: змонтована директорія має бути записуваною для UID 1001 (власник або ACL) або для групи, до якої належить процес.

Завдання 2: перевірити, який користувач Docker вважає за свій

cr0x@server:~$ docker inspect -f '{{.Config.User}}' myapp
1001:1001

Значення: контейнер налаштований на запуск як числовий користувач/група.
Рішення: співпадайте власність хостової директорії з 1001:1001 або змініть runtime-користувача, щоб відповідати директорії.

Завдання 3: підтвердити джерело монтування і його тип

cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}' myapp
bind /srv/myapp/data -> /var/lib/myapp
volume myapp-cache -> /cache

Значення: /var/lib/myapp — bind mount; /cache — іменований том.
Рішення: налагоджуйте права bind mount на хостовому шляху; налагоджуйте іменований том через місце розташування Docker volume.

Завдання 4: перевірити власність і режим хостової директорії

cr0x@server:~$ stat -c 'path=%n owner=%u:%g mode=%a type=%F' /srv/myapp/data
path=/srv/myapp/data owner=0:0 mode=755 type=directory

Значення: належить root, не записувана для інших (755 означає, що запис може виконувати лише власник).
Рішення: або chown на 1001:1001, або запуск контейнера як root (не рекомендовано), або використовувати ACL/груповий запис.

Завдання 5: симулювати доступ за допомогою тимчасового контейнера з певним UID

cr0x@server:~$ docker run --rm -u 1001:1001 -v /srv/myapp/data:/mnt alpine sh -lc 'touch /mnt/test && ls -ln /mnt/test'
touch: /mnt/test: Permission denied

Значення: UID 1001 не може записати в bind mount; відмова відтворювана поза вашим додатком.
Рішення: виправте права файлової системи спочатку; не витрачайте час на «налагодження додатку».

Завдання 6: виправити власність bind mount (цільово, а не рекурсивно без потреби)

cr0x@server:~$ sudo chown 1001:1001 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=755

Значення: власник директорії тепер UID/GID 1001. Режим 755 означає, що власник може записувати.
Рішення: повторно протестуйте запис. Якщо додатку потрібна групова взаємодія — відрегулюйте режим/ACL відповідно.

Завдання 7: використовувати setgid + груповий запис для спільних директорій

cr0x@server:~$ sudo chgrp 1001 /srv/myapp/data
cr0x@server:~$ sudo chmod 2775 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=2775

Значення: біт setgid встановлено (2xxx). Нові файли успадковують групу 1001; група має запис.
Рішення: використовуйте це, коли кілька процесів ділять GID і ви хочете передбачувану групову власність.

Завдання 8: додати ACL для другого UID-писача без послаблення режиму

cr0x@server:~$ sudo setfacl -m u:1002:rwx /srv/myapp/data
cr0x@server:~$ getfacl -p /srv/myapp/data | sed -n '1,12p'
# file: /srv/myapp/data
# owner: 1001
# group: 1001
user::rwx
user:1002:rwx
group::rwx
mask::rwx
other::r-x

Значення: UID 1002 явно має rwx на директорії; маска це дозволяє.
Рішення: обирайте ACL, коли у вас є кілька UID у контейнерах і ви не хочете, щоб «всі були 1000».

Завдання 9: знайти іменований том на хості (для налагодження та ініціалізації)

cr0x@server:~$ docker volume inspect myapp-cache -f '{{.Mountpoint}}'
/var/lib/docker/volumes/myapp-cache/_data

Значення: іменований том живе під коренем даних Docker.
Рішення: використовуйте цей шлях для перевірки на хості (stat, getfacl) або для обережної одноразової ініціалізації прав.

Завдання 10: перевірити власність іменованого тому і підтвердити невідповідність UID контейнера

cr0x@server:~$ sudo stat -c 'owner=%u:%g mode=%a' /var/lib/docker/volumes/myapp-cache/_data
owner=0:0 mode=755

Значення: директорія тому належить root; контейнерний користувач 1001 не може записувати.
Рішення: ініціалізуйте власність один раз (цільовий chown лише для потрібного шляху), потім запускайте додаток непривілейовано.

Завдання 11: виконати одноразову ініціалізацію для безпечного chown тому (обмежена область)

cr0x@server:~$ docker run --rm -v myapp-cache:/cache alpine sh -lc 'addgroup -g 1001 app && adduser -D -u 1001 -G app app; mkdir -p /cache/app; chown -R 1001:1001 /cache/app; ls -ldn /cache/app'
drwxr-xr-x    2 1001     1001          4096 Feb  4 12:00 /cache/app

Значення: створено піддиректорію /cache/app, що належить 1001:1001, без рекурсивного chown всього кореня тому.
Рішення: монтуйте і використовуйте цю піддиректорію з додатком, щоб уникнути несподіваних конфліктів власності.

Завдання 12: перевірити всередині контейнера додатку, що запис тепер працює

cr0x@server:~$ docker exec -it myapp sh -lc 'touch /cache/app/ok && ls -ln /cache/app/ok'
-rw-r--r--    1 1001     1001            0 Feb  4 12:01 /cache/app/ok

Значення: файл створено з правильною числовою власністю.
Рішення: ви завершили — якщо тільки не впливають SELinux/AppArmor або семантика NFS.

Завдання 13: перевірити чи монтування встановлено як тільки для читання (часто зустрічається)

cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{if .RW}}{{else}}{{println "RO:" .Destination}}{{end}}{{end}}' myapp
RO: /var/lib/myapp

Значення: монтування встановлено як read-only на рівні Docker.
Рішення: спочатку виправте прапори Compose/run; права не матимуть значення, якщо монтування RO.

Завдання 14: перевірка SELinux (bind mounts, які мають працювати, але не працюють)

cr0x@server:~$ getenforce
Enforcing

Значення: SELinux працює в режимі Enforcing.
Рішення: якщо bind mounts відмовляють при правильних UID/GID/режимах, проставте мітки для bind mount (наприклад, :Z/:z) і повторно тестуйте.

Завдання 15: виявити поведінку NFS root-squash (root не може виправити те, що root не може володіти)

cr0x@server:~$ mount | grep ' /srv/myapp '
nfs01:/exports/myapp on /srv/myapp type nfs4 (rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.10,local_lock=none,addr=10.0.0.20)

Значення: підкладка — NFSv4. Root-squash конфігурований на стороні сервера, тут його не видно.
Рішення: якщо chown повертає «Operation not permitted», припиніть боротися з контейнером і виправте мапінг ідентичностей/опції експорту.

Завдання 16: перевірити ремапінг неймспейсів користувачів (UID не будуть такими, як ви думаєте)

cr0x@server:~$ docker info | sed -n '1,120p' | grep -E 'rootless|userns'
rootless: false
userns: host

Значення: remap userns відсутній; контейнерні UID мапляться напряму на хостові UID.
Рішення: вирівнювання UID/GID просте. Якщо userns увімкнено — врахуйте діапазони remap в плані.

Завдання 17: підтвердити, що ядро бачить монтування там, де ви його очікуєте

cr0x@server:~$ docker exec -it myapp sh -lc 'grep -E " /var/lib/myapp | /cache " /proc/mounts'
/dev/sda1 /var/lib/myapp ext4 rw,relatime 0 0
/dev/sda1 /cache ext4 rw,relatime 0 0

Значення: монтування присутні і rw на рівні ядра.
Рішення: якщо записи все ще відмовляють — це права, SELinux/AppArmor, незмінні атрибути або семантика мережевої файлової системи.

Завдання 18: перевірити незмінний атрибут (рідко, але трапляється)

cr0x@server:~$ sudo lsattr -d /srv/myapp/data
-------------------P-- /srv/myapp/data

Значення: немає встановленого незмінного (i) атрибуту.
Рішення: якщо ви бачите ----i--------, приберіть його за допомогою chattr -i (обережно) перед тим як бігти за іншими примарами.

Типові помилки: симптом → причина → виправлення

1) Симптом: «Permission denied» при старті, але лише на одному хості

Причина: bind mount вказує на директорію з іншою власністю/ACL на тому хості (часто створену вручну або іншим автоматичним прогоном).

Виправлення: стандартизуйте створення директорій під час провізії. Застосовуйте власність/режим через конфігураційний менеджмент. Запускайте контейнери з явним числовим UID/GID.

2) Симптом: контейнер запускається як root, але все одно не може записати

Причина: невідповідна SELinux-мітка, монтування тільки для читання або NFS root-squash.

Виправлення: перевірте прапор RW; перевірте getenforce; якщо NFS — виправте мапінг ідентичностей експорту; якщо SELinux — проставте мітки монтування.

3) Симптом: «Operation not permitted» при chown змонтованої директорії

Причина: файлова система не підтримує chown так, як ви очікуєте (звично для NFS/CIFS), або у вас нема привілеїв змінювати власність там.

Виправлення: вирівняйте ідентичності на рівні сховища (UID-мепінг), або використайте підтримувані ACL, або пишіть у директорію, яка вже має правильного власника.

4) Симптом: працювало після видалення тому, але потім зламалось знову

Причина: ви «полагодили» скиданням стану. Наступний запуск створює директорії з root-власністю або іншим UID.

Виправлення: додайте детермінований крок ініціалізації (створити піддиректорію + chown один раз) і припиніть покладатися на імпліцитні дефолти.

5) Симптом: файли, створені додатком, на хості належать root

Причина: процес контейнера працює як root, або entrypoint образу некоректно перемикає користувачів, або ви використали user: "0" як швидке рішення.

Виправлення: запускайте як непривілейований числовий UID; переконайтеся, що entrypoint не ескалює привілеї; перевірте через id всередині контейнера.

6) Симптом: сайдкар/backup не може читати файли, створені додатком

Причина: занадто строгий umask, відсутня стратегія спільної групи або немає ACL для другого UID.

Виправлення: встановіть групову власність + setgid на директоріях; або додайте ACL; або вирівняйте обидва контейнери на спільний GID.

7) Симптом: після увімкнення rootless Docker все дає «Permission denied»

Причина: шляхи на хості належать реальному root, але «root» rootless контейнера мапиться на непривілейований хостовий UID.

Виправлення: chown хостові директорії на rootless-користувача (або відрегулюйте subuid/subgid мапінги), або уникайте bind mounts, які потребують власності хост-root.

8) Симптом: величезна затримка старту контейнера

Причина: entrypoint виконує рекурсивний chown на великому монтуваному наборі даних.

Виправлення: видаліть рекурсивний chown з гарячого шляху; зробіть одноразову ініціалізацію; цілете піддиректорію; або використайте provisioning на рівні файлової системи.

9) Симптом: права виглядають коректно, але записи відмовляють лише в проді

Причина: у проді увімкнено SELinux в режимі Enforcing або використовується інше сховище (NFS/CephFS) з іншою семантикою.

Виправлення: тестуйте з продоподібними налаштуваннями безпеки; явно обробляйте SELinux-мітки та мапінг ідентичностей мережевого сховища.

Три міні-історії з практики

Міні-історія №1: Інцидент через неправильне припущення

Команда розгорнула контейнеризований раннер задач, який записував артефакти у bind-mounted директорію під /srv/builds.
У staging все було гаразд. В проді артефакти почали падати з Permission denied випадково.
On-call пройшовся по звичайних підозрах: збій сховища, заповнений диск, пошкоджений образ.

Неправильне припущення було тонким: «хостова директорія завжди створюється нашою автоматизацією, отже належить сервісній обліковці».
Але один продовжуваний хост був відновлений у поспіху під час вікна обслуговування. Людина вручну відтворила /srv/builds.
Воно належало root, режим 755, а контейнер працював як UID 1001.

Картина збоїв виглядала випадковою, бо роботи запускалися по різних хостах. Ті, що потрапляли на «ручний» хост — падали.
Ніхто не помітив відмінності власності, поки хтось не запустив stat по флоту і не виявив аномалію.

Виправлення не було героїчним. Додали просту перевірку перед запуском у провізії, що стверджує власність і режим, і встановили Compose запуск як 1001:1001 явно.
Найголовніше — перестали покладатися на імена в документації — всі рукописи почали використовувати числові UID/GID.

Урок: якщо існування директорії є частиною коректності — її власність також частина коректності. Linux не працює на відчуттях.

Міні-історія №2: Оптимізація, що зіграла назад

Інша організація мала великий сервіс обробки даних. Щоб зменшити операційні витрати, вони додали крок старту, який робив
chown -R app:app /data при кожному старті контейнера. Це тимчасово зникло з тикетів підтримки.
Але це також зробило деплоя «надійними» у сенсі «надійно повільними».

Спочатку це було лише незручністю. Потім набір даних зріс. Rolling update означав кілька контейнерів паралельно, кожен рекурсивно обробляв терабайти.
CPU підскочив, metadata I/O підскочив, і бекенд сховища почав кидати аварійні повідомлення по латентності. Додаток був «здоровий», кластер — ні.

Повернення удару було не лише в продуктивності. Рекурсивний chown торкався міток часу і власності файлів, які використовувалися downstream-системами для виявлення «свіжості».
Купа пайплайнів переобробили дані, бо змінилася власність, що змінило метадані, що спровокувало їхню логіку «нових даних».
Ніхто не був у захваті.

Виправлення полягало у винесенні ініціалізації прав з runtime контейнера:
одноразова провізійна робота створила піддиректорію й виставила власність один раз.
Також вони змінили додаток, щоб він відмовлявся стартувати, якщо не може писати, замість спроб «пофіксити» права. Fail fast, fail honest.

Урок: «самовідновлення прав» часто просто «повільно спалює вашу файлову систему».

Міні-історія №3: Нудна, але правильна практика, що врятувала

Платформена команда стандартизувала політику: кожен stateful контейнер повинен декларувати числовий user в Compose/Kubernetes,
і кожен записуваний монт повинен бути провізований з відповідною власністю перед плануванням навантаження.
Ніяких винятків, ніякого «тимчасового root», ніяких 777. Це дратувало розробників близько тижня.

Через місяці вони мігрували флот сервісів з локальних дисків на NFS, щоб спростити бекапи.
NFS приніс свої звичні нюанси мапінгу ідентичностей. Передбачувано, кілька сервісів почали кидати помилки прав під час першого канарі.
Але оскільки UID/GID були задекларовані і сталі, поверхня для налагодження була малою: або мапінг експорту, або провізування директорії.

On-call не гадали. Перевірили задекларований UID, перевірили власність директорії на NFS-сервері, виправили мапінг і продовжили.
Ніяких перебудов образів. Ніякого екстреного «запуску як root». Ніяких нічних chmod.

Практика була нудною: явні числові ідентичності, попередньо провізовані записувані шляхи та перевірка перед польотом в CI, що контейнер запускається не як root.
Ця нудьга окупилася.

Урок: стандартизація перемагає креативність, особливо щодо прав доступу.

Цікаві факти та історичний контекст

  • UIDи та GIDи давніше за контейнери. Docker не створив цю модель; він використовує класичну Unix-власність і біт режиму.
  • Імена — це декорація. Linux зберігає власність як цілі числа в інодах; імена користувачів резольвляться пізніше через /etc/passwd або NSS.
  • Ранні налаштування контейнерів за замовчуванням використовували root. Це було зручно і привчило покоління запускати сервіси як UID 0.
  • User namespaces змінили визначення «root». З userns або режимом rootless, контейнерний root може мапитися на непривілейований хостовий UID-діапазон.
  • OverlayFS не «виправляє» права. OverlayFS рішає шари; змонтовані томи все одно підкоряються власності та політиці базової файлової системи.
  • NFS root-squash старіше за більшість контейнерних платформ. Це функція безпеки на боці сховища, що робить root у клієнта схожим на «nobody».
  • ACL — не новинка. POSIX ACL існують роками; їх менше використовують, бо біт режиму простіше пояснити.
  • SELinux — окрема вісь від UID/GID. Ви можете мати ідеальну власність і все одно отримати відмову через MAC-політику.
  • Багато офіційних образів стандартизували фіксовані UID. Образи БД часто використовують стабільні числові ID, щоб томи були сумісними при оновленнях — за умови, що ви поважаєте ці ID.

Контрольні списки / покроковий план

Покроковий план: зробіть так, щоб stateful контейнер перестав боротися з томом

  1. Визначте ідентичність виконання.
    Вирішіть числовий UID/GID, під яким має працювати сервіс. Використовуйте виділене, стабільне число (не «те, що обрав базовий образ сьогодні»).
  2. Виберіть стратегію монтування.
    Bind mount для явного контролю хоста; іменований том для портативності; мережеве сховище лише якщо ви розумієте його модель ідентичностей.
  3. Провізуйте записуваний шлях.
    Створіть директорію (або піддиректорію) і виставте власність/режим. Віддавайте перевагу цільовому chown перед рекурсивним.
  4. Зробіть це тестовим.
    Додайте перевірку перед запуском: може контейнер записати тимчасовий файл у монтування при старті? Якщо ні — падайте з чітким логом.
  5. Розділяйте ініціалізацію та runtime.
    Якщо потрібно зробити chown — робіть це як одноразова init job або ручний крок, а не при кожному старті контейнера.
  6. Обробляйте SELinux/AppArmor явно.
    Якщо у вас SELinux enforсing — впишіть мітки монтування в конфігурацію запуску/compose.
  7. Документуйте числові ID.
    Покладіть UID/GID у репозиторій поруч із Compose-манифестами. Імена дрейфують; числа — ні.
  8. Репетируйте шлях відновлення після катастрофи.
    Якщо ви відновлюєте том з бекапу — чи зберігає він власність? Якщо ні — який крок перекладання власності потрібен?

Перевірка перед деплоєм (використайте перед першим запуском у проді)

  • Контейнер запускається не як root (перевірте id всередині контейнера).
  • Пункти монтування записувані цим UID/GID (перевірте тимчасовим touch).
  • Немає рекурсивного chown в entrypoint для великих шляхів даних.
  • На системах з SELinux — має бути стратегія коректного маркування bind mount.
  • Мережеві файлові системи мають план мапінгу ідентичностей (UID-парність або відповідна власність директорії).
  • Процес бекапу/відновлення зберігає або повторно застосовує власність детерміновано.

Інцидентний чек-лист (коли «Permission denied» вас будить)

  • Підтвердьте UID/GID контейнера (docker exec ... id).
  • Підтвердьте тип монтування і RW-статус (docker inspect).
  • Перевірте хостовий шлях: власність/режим/ACL (stat, getfacl).
  • Перевірте SELinux Enforcing (getenforce) і аудиторські логи при потребі.
  • Перевірте NFS/CIFS і симптоми root-squash (mount, поведінка chown).
  • Застосуйте найменше безпечне виправлення: вирівнювання власності, ACL або коректне маркування — не 777.

Поширені запитання

1) Чому користувач контейнера є в образі, але його немає на хості?

Тому що це різні простори імен для імен користувачів. Для перевірок доступу важливі тільки числові ID.
Хосту не потрібно знати ім’я користувача; йому потрібно, щоб UID/GID співпадав із власністю або наданими правами.

2) Якщо я запускаю контейнер з -u 1000:1000, чи потрібен цей користувач всередині образу?

Не обов’язково. Ядеро застосовує числові ID. Деякі додатки очікують запис у passwd для UID (для викликів getpwuid()).
Якщо застосунок скаржиться — додайте запис або використайте образ, що підтримує довільні UID.

3) Чи безпечно змінювати власність директорії Docker volume під /var/lib/docker?

Може бути, але робіть це дисципліновано. Краще створити і володіти піддиректорією в обсязі, ніж міняти корінь тому.
Уникайте рекурсивного chown на великих даних. І ніколи не запускайте «догадливі» команди як root у тім дереві під час інциденту.

4) Чому права ламаються після відновлення з бекапу?

Багато інструментів бекапу зберігають вміст, але не числову власність, або відновлюють під користувачем, що виконує відновлення.
Якщо файли після відновлення належать root (або іншому UID), ваш контейнерний користувач втрачає доступ на запис.
Виправляйте, відновлюючи зі збереженням власності або виконавши контрольований крок повторного призначення власності.

5) У цьому контексті в чому різниця між chmod і chown?

chown змінює, кому належать файли (який UID/GID має «права власника»).
chmod змінює, що можуть робити власники/групи/інші. Якщо власник неправильний, chmod часто просто переставляє розчарування.

6) Мій застосунок працює як root в контейнері. Хіба це нормально, бо він «ізольований»?

Ізоляція не абсолютна. Root в контейнері має широкі права в межах цього контейнера контексту, і налаштування можуть бути неправильними.
Запуск не як root зменшує радіус ураження і змушує вас вирішувати права томів правильно, а не заклеювати їх.

7) Чому це працює на macOS/Windows Docker Desktop, але ламається на Linux?

Docker Desktop використовує VM і інший механізм шарингу файлової системи. Семантика власності і прав може там переводитися або спрощуватися.
Linux-хости «реальні» в тому сенсі, що bind mount — це фактична файловий система з реальними UID, ACL і модулями безпеки.

8) Як мені обробляти кілька контейнерів, що пишуть в один том?

Віддавайте перевагу стратегії спільної GID (груповий запис + setgid) або використовуйте ACL, щоб дати запис кільком UID.
Уникайте запускати все як один і той же UID, якщо ви не готові до наслідків для аудиту й налагодження.

9) А якщо я використовую Kubernetes замість Docker Compose?

Принципи ті самі: вирівняйте runtime UID/GID з правами сховища. Kubernetes додає опції securityContext на кшталт
runAsUser і fsGroup. Будьте обережні: fsGroup може викликати рекурсивні зміни прав на деяких типах томів,
що чудово для маленьких томів і болісно для величезних.

10) Чи має значення umask у цій історії?

Так. Навіть якщо власність директорії коректна, строгий umask може зробити файли такими, що інші процеси не зможуть їх читати.
Коли у вас є сайдкари або спільні рідери, вирішіть навмисно, чи потрібен груповий доступ на читання/запис і налаштуйте umask відповідно.

Висновок: наступні кроки, що залишаються

«Permission denied» на Docker-томах рідко таємниче. Майже завжди це невідповідність числових UID/GID, відсутність групових/ACL-прав
або рівень безпеки (SELinux, NFS root-squash), що виконує свою роботу.
Виправлення, яке тримається, — припинити імпровізації і почати декларувати ідентичність та власність як частину контракту розгортання.

Зробіть це далі (у порядку)

  1. Визначте стабільний числовий UID/GID для кожного stateful сервісу і зафіксуйте його в репозиторії.
  2. Провізуйте записувані директорії навмисно (власник/режим, або ACL, або група+setgid), а не випадково.
  3. Запускайте контейнери під цим UID/GID і перевіряйте простим тестом запису під час старту або в CI.
  4. Приберіть рекурсивний chown з entrypoint, якщо тільки ваш шлях даних не крихітний і вас влаштовує повільний деплой.
  5. Врахуйте SELinux і мережеве сховище заздалегідь; вони не стають простішими о 2-й ночі.

Якщо ви ставитеся до томів як до частини застосунку — а не як до після думки — ви перестанете грати у whack-a-mole з правами.
Ваше майбутнє «я» все одно іноді отримає виклик, звісно. Але вже через щось цікавіше.

← Попередня
Realtek Audio не працює? Виправлення, яке краще за перевстановлення драйверів
Наступна →
Windows не завантажується після оновлення: відновлення за 15 хвилин без перевстановлення

Залишити коментар