Docker per użytkownik — jak oddzielić kontenery między wieloma kontami użytkowników?
Z kontenerów opartych na usłudze Docker korzystam na co dzień — w zastosowaniach prywatnych, jak również w ramach mojej pracy zawodowej, którą wykonuję zdalnie na własnym komputerze. Bardzo lubię wyraźny podział między kwestiami osobistymi i zawodowymi, dlatego w swoim systemie operacyjnym posiadam dwie partycje dyskowe i dwa konta użytkowników — oddzielne do pracy i do prywatnego użytku. Niestety architektura Dockera utrudnia użytkownikowi tego typu separację.
Na czym polega problem?
Program Docker, używany natywnie w systemie z rodziny Linux, uruchamia się system-wide, tj. dla całego systemu operacyjnego: pliki kontenerów zapisywane są poza katalogiem domowym, a wywołanie polecenia takiego jak docker ps -a
wyświetla dane na temat wszystkich kontenerów w systemie, a nie tylko tych należących do bieżącego użytkownika.
Jak wspomniałem, mam dwa konta użytkowników, a katalog domowy każdego z nich znajduje się na oddzielnej partycji dyskowej. Zachowanie Dockera psuje higienę separacji między sprawami prywatnymi a służbowymi. Dodatkowo, przechowywanie danych i konfiguracji kontenerów na partycji systemowej sprawia, że przy ewentualnej reinstalacji systemu wszystkie te dane zostałyby utracone, jeśli zapomniałbym o tym szczególe.
Jednym z rozwiązań tego problemu jest rootless Docker — oficjalna inicjatywa umożliwiająca uruchomienie tej usługi per user. Nie podoba mi się ona ze względu na niepełną kompatybilność w porównaniu ze standardowym sposobem uruchamiania oraz nieprzejrzystą formę instalacji. Do kontenerów niewymagających usługi uruchamianej jako root zdecydowanie wolę program Podman firmy Red Hat.
Przygotowałem swoje alternatywne rozwiązanie, które nie posiada właściwie żadnych negatywnych skutków ubocznych (przynajmniej w moich zastosowaniach). Zmodyfikowałem konfigurację jednostek systemd tak, aby w moim systemie pracowały dwie oddzielne kopie usługi Docker — każda dla jednego z kont użytkowników w systemie. Za pomocą dowiązań symbolicznych przeniosłem też dane kontenerów na oddzielne partycje każdego z kont.
Moje rozwiązanie — krok po kroku
Instrukcję przedstawiam na przykładzie Fedory 35 i paczki moby-engine
z repozytoriów systemowych, jednak jej implementacja w innych dystrybucjach Linuksa, jak również w przypadku paczek z upstreamu nie powinna różnić się za bardzo. W razie wątpliwości zachęcam do zostawienia komentarza pod artykułem.
Stworzymy całkowicie nową usługę typu template o nazwie docker@
, którą zastąpimy domyślną usługę docker
. Dzięki temu uruchamianie nowych kopii Dockera dla każdego z użytkowników będzie bardzo proste.
(Uwaga: zastosowanie niewielkich plików „drop-in” do podmiany jedynie wymaganych opcji nie jest możliwe w tym przypadku ze względu na konieczność nadpisania zależności (np. pola After
), co nie jest wspierane w tym mechanizmie.)
Krok 1: przygotowanie jednostki systemd typu template
Skopiuj plik /usr/lib/systemd/system/docker.socket
do /etc/systemd/system/docker@.socket
. W nowo utworzonym pliku wprowadź zmiany według zaznaczenia poniżej.
[Unit] Description=Docker Socket for the API [Socket] ListenStream=/run/docker.sock SocketMode=0660 SocketUser=root SocketGroup=docker
[Unit] Description=Docker Socket (for user %i) [Socket] ListenStream=/run/docker-%i.sock SocketMode=0660 SocketUser=root SocketGroup=docker
Skopiuj plik /usr/lib/systemd/system/docker.service
do /etc/systemd/system/docker@.service
. W nowym pliku wprowadź zmiany, jak zaznaczyłem poniżej.
[Unit] Description=Docker Application Container Engine Documentation=https://docs.docker.com After=docker.socket network-online.target firewalld.service Requires=docker.socket Wants=network-online.target [Service] Type=notify EnvironmentFile=-/etc/sysconfig/docker ExecStart=/usr/bin/dockerd \ --host=fd:// \ --exec-opt native.cgroupdriver=systemd \ $OPTIONS ExecReload=/bin/kill -s HUP $MAINPID ...
[Unit] Description=Docker (for user %i) Documentation=https://docs.docker.com After=docker@%i.socket network-online.target firewalld.service Requires=docker@%i.socket Wants=network-online.target [Service] Type=notify EnvironmentFile=-/etc/sysconfig/docker ExecStart=/usr/bin/dockerd \ --host=fd:// \ --exec-opt native.cgroupdriver=systemd \ --pidfile /var/run/docker-%i.pid \ --data-root /var/lib/docker-%i \ --exec-root /var/run/docker-%i \ $OPTIONS ExecReload=/bin/kill -s HUP $MAINPID ...
Krok 2: przygotowanie folderów dla każdej instancji usługi Docker
Stwórz foldery według schematu /var/lib/docker-NAZWA_UŻYTKOWNIKA
. Zadbaj o uprawnienie właściciela i grupy root:root
na tworzonych folderach. W moim przypadku nazwy użytkowników to tomasz
i work-tomasz
.
sudo mkdir /var/lib/docker-tomasz sudo mkdir /var/lib/docker-work-tomasz
Z użyciem poniższych poleceń stosuję alternatywne rozwiązanie: tworzę dowiązania symboliczne do miejsca w katalogu domowym każdego z kont użytkowników w moim systemie. W ten sposób osiągam ładną separację warstw abstrakcji: jednostka systemd nie musi „wiedzieć” o tym, że tak naprawdę pliki lądują w katalogach domowych użytkowników.
sudo mkdir /home/tomasz/.local/share/docker sudo mkdir /home/work-tomasz/.local/share/docker sudo ln -s /home/tomasz/.local/share/docker /var/lib/docker-tomasz sudo ln -s /home/work-tomasz/.local/share/docker /var/lib/docker-work-tomasz
W przypadku dystrybucji Linuksa takiej jak Fedora należy zadbać także o konfigurację SELinuksa.
sudo semanage fcontext -a -e /var/lib/docker /var/lib/docker-tomasz sudo semanage fcontext -a -e /var/run/docker /var/run/docker-tomasz sudo restorecon -R /var/{lib,run}/docker-tomasz sudo semanage fcontext -a -e /var/lib/docker /var/lib/docker-work-tomasz sudo semanage fcontext -a -e /var/run/docker /var/run/docker-work-tomasz sudo restorecon -R /var/{lib,run}/docker-work-tomasz
W przypadku wyboru podejścia opartego o zapis danych w katalogach domowych podobną zmianę trzeba wprowadzić również dla nich.
sudo semanage fcontext -a -e /var/lib/docker /home/tomasz/.local/share/docker sudo semanage fcontext -a -e /var/lib/docker /home/work-tomasz/.local/share/docker sudo restorecon -R /home/{tomasz,work-tomasz}/.local/share/docker
Krok 3: wyłączenie domyślnych i włączenie własnych jednostek systemd
Wyłącz domyślną usługę Docker, aby mieć pewność, że nie pracuje i nie koliduje ze stworzonymi samodzielnie instancjami.
sudo systemctl disable --now docker.{socket,service} sudo systemctl mask docker.{socket,service}
Następnie włącz nowo utworzone usługi — oczywiście tutaj również wprowadź swoje własne nazwy kont użytkowników. :)
sudo systemctl enable --now docker@tomasz.socket sudo systemctl enable --now docker@work-tomasz.socket
Krok 4: konfiguracja połączenia w środowisku
Ze względu na istnienie dwóch usług Docker, trzeba dokonać sprecyzowania dla aplikacji klienckich (polecenia docker
czy docker-compose
), z którą z nich mają się połączyć.
Dodaj do .bashrc
albo .profile
poniższą definicję zmiennej środowiskowej.
export DOCKER_HOST=unix:///var/run/docker-$USER.sock
Jeżeli korzystasz z poleceń CLI Dockera bez używania sudo
, tj. twoje konto użytkownika jest dodane do grupy docker
, następująca zmiana nie jest wymagana. Natomiast zdecydowanie nie jest to właściwa konfiguracja pod względem bezpieczeństwa (pomimo, że powszechnie stosowana i polecana). Z perspektywy zabezpieczeń systemu operacyjnego używanie Dockera bez sudo
jest otwieraniem poważnej luki bezpieczeństwa.
sudo
domyślnie nie przekazuje wszystkich zmiennych środowiskowych uruchamianemu oprogramowaniu. Stwórz plik w folderze /etc/sudoers.d
o nazwie takiej jak na przykład 90-docker-host-env
i zdefiniuj w nim wyjątek dla zmiennej DOCKER_HOST
.
Defaults env_keep += "DOCKER_HOST"
Dodaj komentarz