Błąd permission denied przy danych montowanych do kontenera zwykle wygląda banalnie, ale jego przyczyna może leżeć w kilku miejscach jednocześnie. Proces działa pod określonym UID i GID, katalog na hoście ma innego właściciela, po drodze występuje ACL albo SELinux, a dodatkowo obraz może zmieniać użytkownika podczas startu. Skuteczna naprawa nie polega na ustawieniu chmod 777, lecz na ustaleniu, kto próbuje wykonać operację, na jakiej ścieżce i jakie uprawnienia rzeczywiście obowiązują. [2]
Cel jest prosty: aplikacja self-hostowana ma móc zapisywać wyłącznie tam, gdzie powinna, bez uruchamiania całego kontenera jako root i bez przypadkowego otwierania dostępu do prywatnych danych na hoście. Kluczowe są identyfikatory numeryczne UID oraz GID, a nie same nazwy użytkowników widoczne w systemie.
Najpierw ustal źródło błędu: host, kontener czy system plików
Objawy, które wyglądają tak samo, ale mają inne przyczyny
Komunikat permission denied nie oznacza automatycznie, że Docker jest uszkodzony albo że katalog ma „za małe” prawa. Ten sam błąd pojawi się wtedy, gdy proces w kontenerze ma niewłaściwy UID, katalog jest własnością innego użytkownika, brakuje bitu wykonywania na katalogu nadrzędnym, zasady SELinux blokują dostęp lub udział NFS mapuje użytkownika w nieoczekiwany sposób.
Typowy objaw to aplikacja, która poprawnie odczytuje konfigurację, ale nie może utworzyć pliku bazy danych, pliku blokady, cache’u albo katalogu z załącznikami. Odczyt pliku wymaga innych warunków niż utworzenie nowego pliku w katalogu. Do tworzenia danych potrzebne są prawa zapisu i wykonywania do samego katalogu, a nie tylko prawo odczytu istniejącego pliku. [3]
Inny częsty przypadek występuje po aktualizacji obrazu. Starsza wersja aplikacji mogła działać jako root i utworzyć pliki z UID 0. Nowa wersja działa już jako użytkownik nieuprzywilejowany, na przykład z UID 1000, 1001 albo innym zdefiniowanym przez autora obrazu. Dane istnieją, są widoczne w kontenerze, ale nie można ich modyfikować.
Co wiemy z samego komunikatu aplikacji? Zwykle tylko to, że określona operacja na ścieżce została odrzucona. Czego nie wiemy? Nie znamy UID procesu, rzeczywistego miejsca montowania, właściciela katalogu na hoście ani dodatkowych mechanizmów bezpieczeństwa. Te informacje trzeba sprawdzić przed zmianą konfiguracji.
Wolumen zarządzany przez Docker a bind mount
W diagnostyce trzeba od razu rozróżnić dwa mechanizmy. Wolumen zarządzany przez Docker jest tworzony i przechowywany przez silnik Docker. Zwykle znajduje się w katalogu danych Dockera i jest deklarowany w Compose nazwą wolumenu. Bind mount wskazuje konkretną ścieżkę hosta, na przykład /srv/nextcloud/data:/var/www/html/data. To bind mounty najczęściej powodują problemy związane z UID i GID w self-hostingu.
| Rodzaj montowania | Przykład w Compose | Najczęstsze źródło problemu |
|---|---|---|
| Wolumen nazwany | app_data:/var/lib/app | Pliki utworzone przez root, zmiana użytkownika obrazu, ręczna ingerencja w dane Dockera |
| Bind mount | /srv/app/data:/var/lib/app | Niedopasowany właściciel katalogu hosta, brak praw grupy, SELinux, ACL |
| Tmpfs | type: tmpfs | Ograniczenia aplikacji, parametry montowania, zbyt mały rozmiar pamięci |
| Udział sieciowy | NFS/CIFS zamontowany na hoście | Mapowanie UID, root squash, ACL serwera plików, opcje montowania |
Przy bind mouncie Docker nie „przejmuje” praw dostępu do plików. Kontener otrzymuje widok na katalog hosta, ale jądro systemu nadal sprawdza właściciela, grupę, tryb dostępu, ACL i polityki bezpieczeństwa. Docker nie zamienia automatycznie użytkownika app z kontenera na użytkownika o tej samej nazwie na hoście. Liczą się wartości numeryczne. [1]
Mapa zależności podczas dostępu do pliku
Najprostszy model wygląda następująco: proces aplikacji próbuje otworzyć plik lub utworzyć katalog, jądro sprawdza UID i GID tego procesu, następnie porównuje je z właścicielem i grupą obiektu na systemie plików. Potem analizowane są klasyczne prawa Unix, ewentualne ACL, kontekst SELinux oraz zasady systemu plików sieciowego. Dopiero po przejściu wszystkich tych warstw operacja może się udać.
- Proces w kontenerze działa z konkretnym UID i jednym lub wieloma GID.
- Docker przekazuje ten UID do jądra hosta, chyba że używane są mechanizmy mapowania użytkowników.
- Host sprawdza prawa katalogu oraz wszystkich katalogów prowadzących do niego.
- ACL może przyznać lub odebrać dostęp niezależnie od prostego wyniku
chmod. - SELinux może zablokować dostęp mimo pozornie poprawnego właściciela i trybu pliku.
- NFS, CIFS i podobne systemy mogą inaczej interpretować UID, GID lub prawa roota.
W praktyce nie należy zaczynać od szerokiego chmod -R 777. Taka komenda maskuje część problemów, tworzy ryzyko wycieku danych, a w środowisku z SELinux lub udziałem sieciowym często nie rozwiązuje przyczyny. Co gorsza, po kolejnym restarcie albo aktualizacji problem może wrócić w mniej oczywistej formie.
Krok 1. Zidentyfikuj proces, użytkownika i ścieżkę zapisu
Ustal, pod jakim UID i GID działa aplikacja
Najpierw sprawdź stan usługi. W katalogu z plikiem Compose przydatne będzie polecenie:
docker compose psNastępnie uruchom powłokę lub pojedyncze polecenie w działającym kontenerze. W większości obrazów wystarczy:
docker compose exec nazwa_uslugi id
docker compose exec nazwa_uslugi whoami
docker compose exec nazwa_uslugi sh -c 'ps -eo user:20,uid,gid,pid,comm'Jeżeli kontener nie ma powłoki sh, można wykonać samo id albo skorzystać z narzędzi dostępnych w obrazie. Minimalne obrazy produkcyjne często nie zawierają bash, ps ani nawet whoami. Brak tych programów nie jest błędem obrazu; oznacza jedynie, że diagnostykę trzeba wykonać inną metodą.
Wynik podobny do poniższego ma kluczowe znaczenie:
uid=1000(app) gid=1000(app) groups=1000(app),44(video)Nazwa app jest informacją pomocniczą. Najważniejsze są liczby: UID 1000 i GID 1000. Na hoście UID 1000 może należeć do użytkownika o zupełnie innej nazwie. Z perspektywy uprawnień do plików nie ma to znaczenia, ponieważ system operacyjny ocenia identyfikatory liczbowe.
Sprawdź konfigurację obrazu, Compose i procesu startowego
Użytkownik procesu może być ustawiony na kilku poziomach. Obraz może zawierać dyrektywę USER w Dockerfile. Plik compose.yaml może mieć pole user:. Entry point może uruchomić część przygotowań jako root, a później przełączyć proces aplikacji na konto nieuprzywilejowane. Niektóre obrazy czytają zmienne typu PUID i PGID, tworząc albo modyfikując użytkownika podczas startu. [4]
Do sprawdzenia konfiguracji kontenera przydaje się:
docker inspect nazwa_kontenera --format '{{.Config.User}}'
docker inspect nazwa_kontenera --format '{{json .Mounts}}'Puste pole w wyniku .Config.User nie musi oznaczać, że aplikacja stale działa jako root. Obraz może mieć własny użytkownik zdefiniowany wewnętrznie albo skrypt startowy może wykonać przełączenie. Dlatego wynik docker inspect należy porównać z wynikiem id wykonanym w działającym kontenerze.
Jeżeli aplikacja uruchamia procesy potomne, sprawdź ich rzeczywiste UID. Serwer WWW, proces kolejki, harmonogram zadań i narzędzie migracyjne mogą działać pod różnymi kontami. Błąd przy aktualizacji schematu bazy plikowej może dotyczyć innego procesu niż błąd przy normalnej pracy aplikacji.
Ustal, gdzie naprawdę są zamontowane dane
Nie zakładaj, że ścieżka wpisana w Compose jest jedynym miejscem zapisu. Aplikacja może korzystać z katalogu danych, katalogu cache, lokalizacji plików tymczasowych, katalogu logów albo ścieżki wskazanej w konfiguracji wewnętrznej. Błąd w /tmp będzie wymagał innej analizy niż błąd w bind mouncie /config.
Lista montowań kontenera pokazuje, czy problematyczna ścieżka rzeczywiście odpowiada katalogowi hosta:
docker inspect nazwa_kontenera --format '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}'W kontenerze można dodatkowo sprawdzić punkt montowania:
docker compose exec nazwa_uslugi sh -c 'mount | grep -F "/var/lib/aplikacji"'
docker compose exec nazwa_uslugi sh -c 'ls -ld /var/lib/aplikacji'Jeżeli problem dotyczy konkretnego pliku, odnotuj pełną ścieżkę z logu. Następnie ustal jej odpowiednik na hoście. Gdy Compose zawiera wpis /srv/mojserwer/app-data:/var/lib/app, plik /var/lib/app/uploads/a.txt odpowiada plikowi /srv/mojserwer/app-data/uploads/a.txt na hoście.
Kryterium zakończenia pierwszego kroku
Do zmiany właściciela lub konfiguracji można przejść dopiero wtedy, gdy znane są trzy elementy: UID i GID procesu wykonującego operację, dokładna ścieżka w kontenerze oraz odpowiadająca jej ścieżka na hoście. Bez tego każda korekta jest zgadywaniem.
Jeżeli aplikacja nie startuje wystarczająco długo, aby uruchomić docker compose exec, sprawdź logi i definicję obrazu. Można też jednorazowo uruchomić obraz z innym entrypointem diagnostycznym, ale taka operacja wymaga ostrożności, ponieważ nie powinna modyfikować danych produkcyjnych.
Krok 2. Odczytaj właściciela i prawa po stronie hosta bez zgadywania
Jak interpretować wynik ls -ln i stat
Na hoście używaj widoku numerycznego. Nazwy użytkowników mogą wprowadzać w błąd, zwłaszcza gdy host i kontener mają różne pliki /etc/passwd. Polecenie:
ls -ldn /srv/mojserwer/app-data
ls -lan /srv/mojserwer/app-data
stat -c '%n | uid=%u gid=%g | mode=%a (%A)' /srv/mojserwer/app-dataPrzykładowy wynik:
drwxr-x--- 8 1001 1001 4096 Mar 10 12:00 /srv/mojserwer/app-dataOznacza to, że właścicielem katalogu jest UID 1001, grupą jest GID 1001, właściciel ma prawa rwx, grupa r-x, a pozostali nie mają dostępu. Proces z UID 1000 i GID 1000 nie będzie mógł tworzyć danych w takim katalogu, nawet jeśli jego nazwa użytkownika w kontenerze brzmi podobnie do nazwy właściciela na hoście.
Warto rozumieć trzy klasy klasycznych uprawnień Unix:
- właściciel — prawa dotyczą UID zgodnego z właścicielem pliku lub katalogu;
- grupa — prawa dotyczą procesu należącego do GID właściciela obiektu lub do odpowiedniej grupy dodatkowej;
- pozostali — prawa używane, gdy proces nie pasuje ani do właściciela, ani do grupy.
Przy katalogach bit x oznacza możliwość przejścia przez katalog i odwołania się do obiektów wewnątrz niego. Bit r pozwala odczytać listę nazw, a bit w umożliwia tworzenie, usuwanie i zmianę nazw wpisów, o ile obecny jest także bit x. Katalog z prawami rw-, ale bez x, nadal może blokować działania aplikacji w sposób, który na pierwszy rzut oka wydaje się nielogiczny.
Sprawdź prawa katalogów nadrzędnych
Poprawne prawa do katalogu docelowego nie wystarczą, jeśli proces nie może przejść przez jeden z katalogów nadrzędnych. Problem często występuje, gdy dane są trzymane w katalogu domowym użytkownika, a katalog /home/uzytkownik jest dostępny tylko dla właściciela.
Do analizy całej ścieżki użyj:
namei -l /srv/mojserwer/app-data/uploads
namei -om /srv/mojserwer/app-data/uploadsJeżeli narzędzie namei nie jest dostępne, sprawdź każdy poziom osobno:
ls -ldn /srv
ls -ldn /srv/mojserwer
ls -ldn /srv/mojserwer/app-data
ls -ldn /srv/mojserwer/app-data/uploadsW środowisku self-hostingu bezpieczniejszym wzorcem jest przechowywanie danych usług w wydzielonym drzewie, na przykład /srv/nazwa-uslugi albo /opt/stacks/nazwa-uslugi/data, zamiast montowania fragmentów katalogu domowego. Zakres uprawnień jest wtedy łatwiejszy do zrozumienia, a kopie zapasowe nie mieszają danych aplikacji z prywatnymi plikami użytkownika hosta.
Znajdź pliki utworzone przez root lub inny proces
Nawet gdy katalog główny ma poprawnego właściciela, pojedyncze pliki albo podkatalogi mogą mieć inne UID i GID. Dzieje się tak po ręcznym kopiowaniu danych poleceniem wykonanym przez root, po migracji z innego serwera, po błędzie skryptu aktualizacyjnego albo po wcześniejszym uruchomieniu kontenera z domyślnym użytkownikiem root.
find /srv/mojserwer/app-data -xdev -printf '%u:%g %m %pn' | less
find /srv/mojserwer/app-data -xdev -uid 0 -ls
find /srv/mojserwer/app-data -xdev -not -user 1000 -lsPrzed użyciem masowego chown -R sprawdź, czy katalog nie zawiera plików, których właściciel powinien pozostać inny. Dotyczy to zwłaszcza sekretów, certyfikatów, plików generowanych przez reverse proxy, katalogów kopii zapasowych oraz danych współdzielonych z inną usługą. Zmiana całego drzewa jest uzasadniona tylko wtedy, gdy wszystkie jego dane faktycznie należą do jednej aplikacji i jednego modelu uprawnień.
Scenariusz z niewłaściwym GID
Załóżmy, że katalog ma właściciela 1001:2000 i tryb 2770. Aplikacja w kontenerze działa jako UID 1001, ale z GID 1001. Jeżeli UID procesu jest zgodny z właścicielem, zapis będzie możliwy. Jeśli jednak proces działa jako UID 1002, a jego grupa główna to 1001, uprawnienia grupowe nie zadziałają, ponieważ katalog należy do GID 2000.
To pokazuje, dlaczego samo stwierdzenie „kontener działa jako użytkownik 1000” nie wystarcza. W modelu opartym na współdzielonej grupie trzeba sprawdzić także grupy dodatkowe procesu: [5]
docker compose exec nazwa_uslugi id -aWynik powinien zawierać GID grupy, która ma prawa do katalogu. Jeśli nie zawiera, potrzebna jest zmiana konfiguracji użytkownika, dodanie grupy dodatkowej albo zastosowanie innego modelu dostępu.
Krok 3. Dobierz właściwy model naprawy UID i GID
Model A — uruchamianie kontenera z UID i GID użytkownika hosta
Najprostszy model dla wielu aplikacji to uruchamianie procesu z UID i GID istniejącego użytkownika hosta. W Compose można użyć pola user::
services:
aplikacja:
image: przyklad/aplikacja:stable
user: "1000:1000"
volumes:
- /srv/aplikacja/data:/var/lib/aplikacjaW konfiguracjach przenośnych identyfikatory można przekazać przez plik .env:
PUID=1000
PGID=1000
services:
aplikacja:
image: przyklad/aplikacja:stable
user: "${PUID}:${PGID}"
volumes:
- /srv/aplikacja/data:/var/lib/aplikacjaNa hoście wartości można odczytać tak:
id -u nazwa_uzytkownika
id -g nazwa_uzytkownikaModel A dobrze działa z obrazami, które od początku są przygotowane do pracy jako dowolny użytkownik nieuprzywilejowany. Takie aplikacje nie próbują instalować pakietów przy starcie, nie muszą zmieniać właściciela katalogów systemowych i nie wymagają nasłuchiwania na porcie uprzywilejowanym. Jest to często rozsądne rozwiązanie dla narzędzi webowych, serwerów plików, usług indeksujących lub aplikacji korzystających z wyłącznego katalogu danych.
Ostrzeżenie: nie każdy obraz obsługuje wymuszenie user:. Entry point może wymagać roota do wygenerowania konfiguracji, utworzenia katalogów runtime albo obniżenia uprawnień dopiero po wykonaniu zadań startowych. Jeśli po dodaniu user: usługa przestaje uruchamiać się poprawnie, nie należy obchodzić problemu przez ustawienie privileged: true. Najpierw sprawdź dokumentację obrazu i logi startowe.
Model B — dopasowanie właściciela katalogu do użytkownika aplikacji
Drugi model polega na pozostawieniu domyślnego użytkownika obrazu i zmianie właściciela danych na UID oraz GID używane przez aplikację. Jeżeli proces ma UID 1001 i GID 1001, a katalog jest przeznaczony wyłącznie dla tej usługi, zmiana może wyglądać tak:

sudo chown -R 1001:1001 /srv/aplikacja/data
sudo find /srv/aplikacja/data -type d -exec chmod 750 {} ;
sudo find /srv/aplikacja/data -type f -exec chmod 640 {} ;Dobór konkretnych trybów zależy od aplikacji. Katalogi wymagają zwykle prawa x, pliki danych nie zawsze muszą być wykonywalne ani zapisywalne dla grupy. Nie kopiuj mechanicznie powyższych wartości do każdego projektu. Jeśli aplikacja tworzy pliki wykonywalne, skrypty lub współdzielone artefakty, wymagania będą inne.
Model B jest szczególnie przydatny wtedy, gdy autor obrazu przewidział stałego użytkownika, na przykład UID 911, 568, 999 albo inny. Wymuszanie UID hosta mogłoby zepsuć wewnętrzne założenia obrazu, podczas gdy dopasowanie właściciela katalogu pozostawia mechanizm startowy bez zmian.
Warto przeczytać również powiązany artykuł: Automatyzacja wydań VS Code Extension CI.
Model C — wspólna grupa i bit setgid dla danych używanych przez więcej niż jeden proces
Jeżeli z tego samego katalogu korzysta kilka kontenerów albo aplikacja i zadanie pomocnicze uruchamiane na hoście, praktyczniejszy może być model grupowy. Zamiast przypisywać cały katalog jednemu UID, tworzysz lub wybierasz wspólny GID, przyznajesz tej grupie prawa zapisu i dodajesz ją do procesów, które mają współdzielić dane.
Przykładowo katalog danych ma należeć do grupy o GID 2000:
sudo chown -R 1001:2000 /srv/aplikacja/data
sudo find /srv/aplikacja/data -type d -exec chmod 2770 {} ;
sudo find /srv/aplikacja/data -type f -exec chmod 660 {} ;Cyfra 2 w trybie 2770 ustawia bit setgid dla katalogu. Dzięki temu nowe pliki i podkatalogi tworzone w tym miejscu dziedziczą grupę katalogu, czyli w tym przykładzie GID 2000. Ogranicza to sytuację, w której pierwszy kontener zapisuje plik dla swojej prywatnej grupy, a drugi proces traci do niego dostęp.
W Compose dodatkową grupę można przekazać przez group_add:
services:
aplikacja:
image: przyklad/aplikacja:stable
user: "1001:1001"
group_add:
- "2000"
volumes:
- /srv/aplikacja/data:/var/lib/aplikacjaPo uruchomieniu sprawdź, czy grupa rzeczywiście jest widoczna dla procesu:
docker compose exec aplikacja id -aWynik powinien zawierać 2000 wśród grup dodatkowych. Sam wpis group_add nie naprawi problemu, jeśli katalog nadal nie ma grupowego prawa zapisu albo po drodze brakuje prawa wykonywania.
Ostrzeżenie: model grupowy wymaga kontroli nad maską tworzenia plików, czyli umask. Jeżeli aplikacja tworzy pliki z trybem 600, drugi proces należący nawet do prawidłowej grupy nie uzyska do nich dostępu. W takiej sytuacji sprawdź dokumentację aplikacji, jej zmienne środowiskowe lub sposób uruchamiania procesu. Nie próbuj kompensować niewłaściwego umask okresowym chmod -R, ponieważ problem będzie stale wracał.
Model D — osobny etap inicjalizacji danych
Niektóre obrazy wymagają uruchomienia głównego procesu zgodnie z własną logiką startową, ale katalog danych musi zostać przygotowany wcześniej. W takim przypadku można zastosować jednorazowy kontener inicjalizacyjny albo ręcznie wykonać przygotowanie danych przed pierwszym startem usługi.
Wariant ręczny jest najprostszy, gdy katalog należy wyłącznie do jednej aplikacji:
sudo install -d -o 1001 -g 1001 -m 750 /srv/aplikacja/data
sudo install -d -o 1001 -g 1001 -m 750 /srv/aplikacja/data/cache
sudo install -d -o 1001 -g 1001 -m 750 /srv/aplikacja/data/uploadsPolecenie install -d tworzy katalogi z określonym właścicielem i trybem, bez konieczności późniejszego szerokiego rekursywnego poprawiania całego drzewa.
Jeżeli potrzebny jest kontener pomocniczy, ogranicz jego zadanie do konkretnych ścieżek. Przykład dla nowego katalogu danych:
services:
init-data:
image: alpine:3.20
user: "0:0"
volumes:
- /srv/aplikacja/data:/data
command:
- /bin/sh
- -ec
- |
mkdir -p /data/cache /data/uploads
chown 1001:1001 /data /data/cache /data/uploads
chmod 750 /data /data/cache /data/uploads
restart: "no"Uruchom go jednorazowo przed startem właściwej usługi:
docker compose run --rm init-dataTen model jest użyteczny przy wdrożeniu od zera, migracji do nowego dysku albo tworzeniu ściśle określonych katalogów roboczych. Jest mniej odpowiedni jako automatyczna akcja wykonywana przy każdym starcie stosu. Kontener inicjalizacyjny, który za każdym razem wykonuje rekursywny chown, może opóźniać start, zmieniać prawa plików utworzonych przez inne usługi i maskować realne błędy konfiguracji.
Kiedy wybrać który model
- Wybierz model A, gdy obraz poprawnie działa z dowolnym użytkownikiem nieuprzywilejowanym, a katalog danych ma być kontrolowany przez UID użytkownika hosta.
- Wybierz model B, gdy obraz oczekuje własnego, stałego UID/GID i katalog jest przeznaczony tylko dla tej aplikacji.
- Wybierz model C, gdy dane muszą być zapisywane przez więcej niż jeden świadomie dopuszczony proces, na przykład aplikację i kontener wykonujący importy lub kopie logiczne.
- Wybierz model D, gdy trzeba przygotować konkretne katalogi przed pierwszym startem, ale główna usługa nie powinna stale działać jako root ani wykonywać korekt właściciela przy każdym uruchomieniu.
Jeżeli katalog zawiera dane kilku niezależnych usług, najbezpieczniejszym wyborem zwykle nie jest żaden wspólny model uprawnień, lecz rozdzielenie danych na osobne katalogi i osobne mounty. Na przykład lepiej użyć /srv/stack/app-data oraz /srv/stack/proxy-certs niż montować całe /srv/stack do obu kontenerów.
Kryterium zakończenia kroku 3
Przed edycją Compose powinieneś mieć jedną, konkretną decyzję: który UID/GID zapisuje dane, czy potrzebuje grup dodatkowych oraz które katalogi mają zostać przygotowane. Jeśli odpowiedzią jest „spróbuję kilku chown i zobaczę”, model nie został jeszcze dobrany.
Krok 4. Wprowadź zmianę w Docker Compose i przygotuj dane przed startem
Zatrzymaj usługę przed zmianą właściciela aktywnych danych
Przed zmianą właściciela katalogów, grup lub praw zatrzymaj procesy zapisujące dane. Dotyczy to szczególnie baz danych, kolejek, indeksów wyszukiwania oraz usług przechowujących pliki przesyłane przez użytkowników.
docker compose stop aplikacja
docker compose psJeżeli usługa ma zależności, zatrzymaj także procesy, które mogą równolegle modyfikować ten sam mount. Dla przykładu osobny worker, harmonogram zadań i główny kontener webowy mogą używać jednego katalogu uploadów.
Nie zmieniaj właściciela plików bazy danych w trakcie jej pracy. Nawet jeśli sama zmiana metadanych systemu plików zwykle nie uszkadza zawartości pliku, równoczesne działania administracyjne zwiększają ryzyko pomyłki, częściowej migracji lub nieodwracalnego bałaganu w danych.
Edytuj Compose tylko w jednym miejscu odpowiedzialnym za tożsamość procesu
W zależności od wybranego modelu ustaw user:, zmienne wymagane przez obraz albo grupy dodatkowe. Nie mieszaj wszystkich metod naraz bez uzasadnienia. Przykład usługi działającej jako użytkownik hosta:
services:
aplikacja:
image: przyklad/aplikacja:stable
user: "${PUID}:${PGID}"
environment:
TZ: Europe/Warsaw
volumes:
- /srv/aplikacja/data:/var/lib/aplikacja
restart: unless-stoppedPlik .env przechowuje wartości używane przez Compose:
PUID=1000
PGID=1000Po zmianie sprawdź wynikową konfigurację, zanim uruchomisz kontenery:
docker compose configTo polecenie rozwija zmienne środowiskowe i pozwala szybko zauważyć literówkę, pustą wartość albo nieoczekiwany format. Szczególną uwagę zwróć na to, czy user: ma postać liczbową, na przykład 1000:1000, a nie dosłowny tekst z nierozwiniętym znacznikiem zmiennej.
Przygotuj katalogi dokładnie w zakresie mountu
Jeżeli usługa montuje:
- /srv/aplikacja/data:/var/lib/aplikacjato zmieniaj właściciela i prawa w /srv/aplikacja/data, a nie w całym /srv/aplikacja, chyba że inne katalogi również należą wyłącznie do tej samej usługi. Najpierw sprawdź zawartość i właścicieli:
sudo ls -lan /srv/aplikacja
sudo find /srv/aplikacja/data -maxdepth 2 -printf '%u:%g %m %pn' | lessDla modelu B, w którym aplikacja działa jako 1001:1001, przygotowanie może wyglądać następująco:
sudo chown 1001:1001 /srv/aplikacja/data
sudo chmod 750 /srv/aplikacja/data
sudo chown 1001:1001 /srv/aplikacja/data/cache
sudo chmod 750 /srv/aplikacja/data/cacheJeżeli katalog zawiera istniejące dane, poprawiaj tylko elementy, które rzeczywiście muszą należeć do aplikacji. Dobrym podejściem jest najpierw wyszukanie odstępstw, a potem zmiana wybranych plików:
sudo find /srv/aplikacja/data -xdev ( -uid 0 -o -gid 0 ) -ls
sudo find /srv/aplikacja/data/cache -xdev -user root -exec chown 1001:1001 {} +Drugi przykład celowo obejmuje tylko katalog cache. Pliki tymczasowe i cache są zwykle bezpieczniejszym miejscem do korekty niż całe drzewo obejmujące konfigurację, certyfikaty, archiwa lub dane innych komponentów.
Uwzględnij SELinux, gdy system go egzekwuje
Na hostach z aktywnym SELinux same klasyczne prawa Unix mogą być poprawne, a zapis nadal będzie blokowany przez politykę bezpieczeństwa. Dla bind mountów Docker Compose może wymagać oznaczenia wolumenu sufiksem :Z albo :z.
services:
aplikacja:
image: przyklad/aplikacja:stable
volumes:
- /srv/aplikacja/data:/var/lib/aplikacja:Z:Z jest przeznaczone dla katalogu używanego przez jeden kontener, a :z dla danych współdzielonych przez wiele kontenerów. Nie dodawaj tych opcji automatycznie na każdym hoście; mają znaczenie przede wszystkim w środowiskach z SELinux, takich jak Fedora, RHEL, Rocky Linux czy AlmaLinux.
Jeżeli po poprawieniu UID, GID i trybów problem pozostaje, sprawdź komunikaty AVC:
getenforce
sudo ausearch -m AVC -ts recent | tail -n 30Wynik Enforcing oznacza, że polityka SELinux jest aktywnie egzekwowana. Nie wyłączaj SELinux globalnie tylko po to, aby potwierdzić diagnozę w produkcji. Najpierw popraw kontekst mountu albo politykę wymaganą dla danej usługi.
Nie zapomnij o udziałach NFS i CIFS
Gdy ścieżka hosta znajduje się na NFS lub CIFS/SMB, lokalne chown może nie działać zgodnie z oczekiwaniem albo serwer udziału może mapować użytkowników do innego UID. W takiej sytuacji poprawka w Compose nie rozwiąże problemu, jeśli serwer plików odrzuca zapis.
Przed startem sprawdź typ systemu plików:
findmnt -T /srv/aplikacja/data -o TARGET,SOURCE,FSTYPE,OPTIONSDla NFS zwróć uwagę między innymi na mapowanie identyfikatorów po stronie serwera oraz opcję root_squash. Dla CIFS sprawdź opcje montowania, takie jak uid=, gid=, file_mode= i dir_mode=. W tych przypadkach właściwe UID/GID trzeba często ustawić w definicji montowania udziału na hoście, a nie wyłącznie na katalogu widocznym dla Dockera.
Lista kontrolna przed uruchomieniem usługi
- Usługa i wszystkie procesy zapisujące do tego samego katalogu są zatrzymane.
- W Compose ustawiono tylko model zgodny z dokumentacją obrazu:
user:, zmienne PUID/PGID,group_addalbo brak wymuszenia użytkownika. docker compose configpokazuje oczekiwane wartości po rozwinięciu zmiennych.- Katalog hosta istnieje i ma właściciela, grupę oraz tryb zgodne z wybranym modelem.
- Katalogi robocze, cache i uploady mają prawa wykonywania dla procesu, który ma do nich wejść.
- Nie zmieniono praw katalogów należących do innej usługi.
- W środowisku z SELinux ustawiono właściwy kontekst mountu.
- W przypadku NFS lub CIFS zweryfikowano zasady dostępu po stronie udziału sieciowego.
Krok 5. Zweryfikuj naprawę w kontenerze, nie tylko w konfiguracji
Uruchom usługę i sprawdź faktyczną tożsamość procesu
Po przygotowaniu danych uruchom usługę:
docker compose up -d aplikacja
docker compose ps
docker compose logs --tail=100 aplikacjaNastępnie sprawdź użytkownika oraz grupy wewnątrz działającego kontenera:
docker compose exec aplikacja id -aJeżeli obraz zawiera powłokę, warto także zobaczyć proces główny:
docker compose exec aplikacja ps -o pid,user,group,args -p 1W usługach, które po starcie uruchamiają worker pod innym użytkownikiem niż proces PID 1, sprawdź również konkretne procesy aplikacji:
docker compose exec aplikacja ps -eo pid,user,group,argsNie wystarczy potwierdzić, że Compose zawiera poprawny wpis user:. Liczy się użytkownik procesu, który rzeczywiście próbuje utworzyć plik, katalog, socket lub plik blokady.
Wykonaj kontrolowany test odczytu, zapisu i usunięcia
Najprostszy test powinien odbyć się dokładnie w katalogu, którego używa aplikacja. Dla mountu widocznego w kontenerze jako /var/lib/aplikacja:
docker compose exec aplikacja sh -ec '
test -r /var/lib/aplikacja
test -w /var/lib/aplikacja
test -x /var/lib/aplikacja
touch /var/lib/aplikacja/.permission-test
printf "okn" > /var/lib/aplikacja/.permission-test
cat /var/lib/aplikacja/.permission-test
rm /var/lib/aplikacja/.permission-test
'Test obejmuje nie tylko utworzenie pliku, ale też zapis, odczyt i usunięcie. Usuwanie jest istotne, ponieważ zależy od prawa zapisu do katalogu nadrzędnego, a nie od praw samego pliku.
Jeśli obraz nie zawiera sh, nie instaluj narzędzi w działającym kontenerze produkcyjnym wyłącznie dla diagnostyki. Użyj funkcji aplikacji, jej wbudowanego polecenia administracyjnego albo uruchom jednorazowy kontener diagnostyczny z tym samym mountem i tym samym UID/GID.
Przykład kontenera testowego dla modelu 1001:1001:
docker run --rm
--user 1001:1001
-v /srv/aplikacja/data:/data
alpine:3.20
sh -ec 'touch /data/.permission-test && rm /data/.permission-test'Ten test potwierdza dostęp do bind mountu, ale nie zastępuje testu w samej aplikacji. Aplikacja może używać dodatkowych katalogów, własnego umask, ACL albo procesów pomocniczych z inną tożsamością.
Potwierdź wynik po stronie hosta
Po udanym teście sprawdź, jak właściciel i prawa nowo utworzonego pliku wyglądają na hoście. Zamiast od razu usuwać plik testowy, można na chwilę pozostawić go do kontroli:
docker compose exec aplikacja sh -c 'touch /var/lib/aplikacja/.permission-owner-test'
ls -ln /srv/aplikacja/data/.permission-owner-test
stat -c '%n | uid=%u gid=%g | mode=%a (%A)' /srv/aplikacja/data/.permission-owner-test
rm /srv/aplikacja/data/.permission-owner-testW modelu grupowym sprawdź dodatkowo, czy nowy plik otrzymał oczekiwany GID. Jeżeli katalog ma ustawiony bit setgid, nowo utworzone elementy powinny dziedziczyć jego grupę.
stat -c '%n | uid=%u gid=%g | mode=%a' /srv/aplikacja/data/.permission-owner-testJeżeli UID jest poprawny, ale GID niezgodny z modelem współdzielonej grupy, sprawdź setgid katalogu oraz umask aplikacji.
Sprawdź działanie funkcji, która wcześniej zgłaszała błąd
Test touch potwierdza podstawowy dostęp do katalogu, ale końcowym kryterium jest operacja wykonywana przez aplikację. W zależności od usługi może to być:
- wysłanie pliku przez interfejs webowy;
- utworzenie miniatury, cache lub indeksu;
- zapis konfiguracji z panelu administracyjnego;
- wykonanie importu albo synchronizacji;
- uruchomienie zadania cyklicznego, które tworzy plik blokady;
- zapis kopii lub eksportu do dedykowanego katalogu.
Po wykonaniu tej operacji sprawdź logi:
docker compose logs --since=10m aplikacja
docker compose logs --since=10m workerJeżeli błąd dotyczył konkretnego podkatalogu, na przykład uploads, cache lub tmp, zweryfikuj go osobno. Poprawny dostęp do katalogu głównego nie gwarantuje praw do podkatalogu utworzonego wcześniej przez root albo przez inny kontener.
Gdy test nadal kończy się „permission denied”
- Porównaj ponownie UID/GID procesu z właścicielem problematycznego katalogu, a nie tylko katalogu głównego mountu.
- Sprawdź pełną ścieżkę przez
namei -l, włącznie z katalogami nadrzędnymi. - Zweryfikuj grupy dodatkowe przez
id -a. - Sprawdź ACL poleceniem
getfacl -p /srv/aplikacja/data, jeśli system korzysta z ACL. - Na hoście z SELinux przejrzyj najnowsze komunikaty AVC.
- Przy NFS lub CIFS potwierdź, że serwer udziału akceptuje zapis dla używanego UID/GID.
- Sprawdź, czy aplikacja nie zapisuje do innej ścieżki niż zakładano, na przykład do katalogu tymczasowego poza mountem.
Praktyczna checklista po naprawie
- Kontener uruchamia się bez błędów związanych z właścicielem lub dostępem do danych.
- Proces wykonujący zapis ma oczekiwany UID, GID i grupy dodatkowe.
- Kontrolowany test tworzenia, odczytu, zapisu i usunięcia pliku przeszedł w rzeczywistym katalogu mountu.
- Nowe pliki na hoście mają właściciela i grupę zgodne z wybranym modelem.
- Funkcja aplikacji, która wcześniej zawodziła, działa poprawnie.
- Logi aplikacji, workera i hosta nie zawierają nowych komunikatów
permission denied. - Nie rozszerzono uprawnień na niepowiązane katalogi ani nie zmieniono właściciela danych innych usług.
Najczęściej zadawane pytania (FAQ)
Czy permission denied zawsze oznacza błędnego właściciela katalogu?
Nie. Przyczyną może być także brak praw do katalogu nadrzędnego, ACL, SELinux, mapowanie użytkowników na NFS lub zmiana użytkownika przez obraz podczas startu.
Dlaczego nazwa użytkownika w kontenerze nie wystarcza do diagnozy?
Uprawnienia systemowe opierają się na numerycznych identyfikatorach UID i GID. Ta sama nazwa użytkownika może oznaczać inny identyfikator na hoście i w kontenerze.
Jak sprawdzić, pod jakim użytkownikiem działa aplikacja?
W działającym kontenerze można użyć poleceń id, whoami lub ps, na przykład przez docker compose exec nazwa_uslugi id.
Czym różni się wolumen nazwany od bind mountu?
Wolumen nazwany jest zarządzany przez Docker, natomiast bind mount wskazuje konkretną ścieżkę na hoście. Problemy z niedopasowaniem UID i GID najczęściej dotyczą bind mountów.
Czy chmod -R 777 jest właściwym rozwiązaniem?
Nie. Takie ustawienie może nie rozwiązać problemów z SELinux lub udziałami sieciowymi, a dodatkowo niepotrzebnie otwiera dostęp do danych. Najpierw należy ustalić UID, GID, ścieżkę i rzeczywiste reguły dostępu.
Źródła
- [1] Docker Docs – Bind mounts | Docker Docs ↑
- [2] Docker Docs – Define services in Docker Compose | Docker Docs ↑
- [3] Docker Docs – Isolate containers with a user namespace | Docker Docs ↑
- [4] LinuxServer.io na GitHubie – Difference between PUID PGID and the docker-compose user and groupadd ↑
- [5] LinuxServer.io na GitHubie – docker-sonarr README ↑
