Zum Inhalt springen
DasPeTe Tutorials

Rootless Docker: Docker ohne Root unter Linux

Installiere Docker auf einem Linux-Host so, dass der Daemon und jeder Container als dein eigener Benutzer laufen statt als root, und lerne die paar Dinge kennen, die anders funktionieren.

Quest-Ziele

  • Wissen, warum ein Docker-Daemon, der als root läuft, ein Risiko ist
  • Docker aus den offiziellen Paketen auf Ubuntu oder Debian installieren
  • Den Rootless-Modus einrichten und mit dem Rechner starten
  • Prüfen, dass deine Container wirklich ohne root laufen
  • Mit Ports unter 1024 und Dateien in Bind-Mounts umgehen

Auf fast jedem Linux-Server wird Docker gleich installiert: als Hintergrundprogramm, der Docker-Daemon, das als root läuft, der Benutzer, der alles darf. Jeder Container, den er startet, läuft ebenfalls als root, und wer mit dem Daemon sprechen darf, kann diese Macht nutzen. Das ist bequem, heißt aber, dass ein einziger Fehler, in einem Container oder in deiner eigenen Einrichtung, jemandem den ganzen Rechner in die Hand geben kann.

Der Rootless-Modus lässt den Daemon und jeden Container als deinen eigenen, ganz normalen Benutzer laufen. Was du über Docker weißt, funktioniert weiter, von docker run bis docker compose, aber ein Container, der ausbricht, findet nur die Rechte vor, die du ohnehin hast. In diesem Tutorial richtest du ihn auf Ubuntu oder Debian ein, prüfst, dass er wirklich ohne root läuft, und lernst die paar Dinge kennen, die anders funktionieren.

Warum root ein Problem ist

Bei einer normalen Installation schickt der Befehl docker jeden Auftrag über einen Socket an den Daemon, eine besondere Datei unter /var/run/docker.sock. Wer darauf schreiben darf, root oder jedes Mitglied der Gruppe docker, kann einen Container starten, der die ganze Festplatte des Hosts sieht. Eine Zeile wie docker run -it -v /:/host alpine chroot /host öffnet eine Root-Shell auf dem Host, ohne nach einem Passwort zu fragen. Ein Programm in einem Container, das eine Lücke in Docker oder im Kernel findet, landet auf dieselbe Art als root auf dem Host.

So funktioniert der Rootless-Modus

Linux kann einer Gruppe von Prozessen eine eigene Liste von Benutzern geben, einen User-Namespace. Darin darf ein Prozess root sein, während der Kernel ihn außerhalb wie einen gewöhnlichen Benutzer behandelt. Rootless Docker startet seinen Daemon und alle Container in so einem Namespace, und dieser Namespace gehört dir. Root in einem Container ist auf dem Host du. Die anderen Benutzer, die ein Container verwenden kann, etwa der Benutzer 999 einer Datenbank, werden zu Nummern aus einem Bereich, den das System für dich freihält:

text
im Container            auf dem Host
root (0)           ->   robin (1000), also du
Benutzer 1         ->   100000
Benutzer 999       ->   100998
Benutzer 65536     ->   165535

Diese Ersatznummern heißen untergeordnete IDs (subordinate IDs). Zwei kleine Helfer, newuidmap und newgidmap, sind die einzigen Teile mit besonderen Rechten: Sie prüfen, ob ein Bereich wirklich dir gehört, und geben ihn an den Namespace weiter.

Der Benutzer in diesem Tutorial heißt robin. Wo du diesen Namen siehst, steht bei dir dein eigener.

Was du brauchst

  • Einen 64-Bit-Linux-Host mit Ubuntu 22.04, 24.04 oder 26.04 oder Debian 12 oder 13. Das kann ein gemieteter Server sein oder dein eigener Computer.

  • Ein normales Benutzerkonto, das sudo verwenden darf. Mach jeden Schritt als der Benutzer, der Docker verwenden wird, nicht als root.

  • Eine echte Anmeldung als dieser Benutzer: direkt am Rechner oder über ssh.

Deinen ID-Bereich prüfen

Unter Ubuntu und Debian bekommt jeder Benutzer, der mit adduser angelegt wurde, 65.536 untergeordnete IDs. Prüf, ob deine da sind:

~
$ id -u1000$ grep "^$(whoami):" /etc/subuid /etc/subgid/etc/subuid:robin:100000:65536/etc/subgid:robin:100000:65536

Jede Zeile bedeutet Benutzer:erste ID:Anzahl. Die aus /etc/subuid gilt für Benutzer-IDs, die aus /etc/subgid für Gruppen-IDs.

Dockers Pakete installieren

Docker hat eine eigene Paketquelle. Sie ist neuer als das Paket docker.io von Ubuntu und Debian und enthält die Teile, die der Rootless-Modus braucht. Sag apt zuerst, wo sie liegt, und gib ihm Dockers Signaturschlüssel, damit es prüfen kann, dass die Pakete wirklich von Docker kommen. Die Zeile . /etc/os-release liest Name und Version deines Systems in Variablen wie $ID, die ubuntu oder debian enthält, damit dieselben Befehle auf beiden funktionieren:

bash
# Dockers Signaturschlüssel, damit apt prüfen kann, woher die Pakete kommen
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/$ID/gpg" \
  -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Dockers Paketquelle, für deine Distribution und ihre Version
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/$ID
Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME}
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update

Dann installierst du Docker zusammen mit dem, was der Rootless-Modus braucht:

bash
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin \
  docker-compose-plugin docker-ce-rootless-extras uidmap dbus-user-session
  • docker-ce ist die Docker Engine mit ihrem Daemon, und docker-ce-cli der Befehl docker.

  • containerd.io führt die Container für den Daemon aus.

  • docker-buildx-plugin und docker-compose-plugin bringen docker build und docker compose mit.

  • docker-ce-rootless-extras bringt RootlessKit, das den Namespace baut, und das Einrichtungswerkzeug, das du gleich startest.

  • uidmap enthält newuidmap und newgidmap.

  • dbus-user-session lässt systemd auch Programme deines Benutzers verwalten, nicht nur die des ganzen Systems.

Den Root-Daemon abschalten

Die Installation von docker-ce hat auch den üblichen Daemon gestartet, der als root läuft. Den willst du nicht, also halt ihn an und sorg dafür, dass er beim Booten nicht startet. disable nimmt ihn aus dem Systemstart, und --now hält ihn sofort an. Das Einrichtungswerkzeug weigert sich, solange der Socket des Root-Daemons da ist, deshalb löscht ihn die zweite Zeile:

~
$ sudo systemctl disable --now docker.service docker.socket$ sudo rm -f /var/run/docker.sock

Den Rootless-Modus einrichten

Melde dich zuerst ab und wieder an, oder schließ deine ssh-Verbindung und öffne eine neue, damit deine Sitzung die eben installierten Pakete kennt. Starte dann das Einrichtungswerkzeug als dein normaler Benutzer, ohne sudo. Sein Befehl check sagt dir, ob noch etwas fehlt, und install richtet alles ein:

~
$ dockerd-rootless-setuptool.sh check[INFO] Requirements are satisfied$ dockerd-rootless-setuptool.sh install[INFO] Creating /home/robin/.config/systemd/user/docker.service[INFO] starting systemd service docker.service+ systemctl --user start docker.service+ sleep 3+ systemctl --user --no-pager --full status docker.service● docker.service - Docker Application Container Engine (Rootless)     Loaded: loaded (/home/robin/.config/systemd/user/docker.service; disabled; preset: enabled)     Active: active (running) since Tue 2026-11-03 10:12:41 CET; 3s ago# weitere Zeilen des Status und was docker version ausgibt+ systemctl --user enable docker.serviceCreated symlink '/home/robin/.config/systemd/user/default.target.wants/docker.service' → '/home/robin/.config/systemd/user/docker.service'.[INFO] Installed docker.service successfully.[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger robin`[INFO] Creating CLI context "rootless"[INFO] Using CLI context "rootless"[INFO] Make sure the following environment variable(s) are set (or add them to ~/.bashrc):export PATH=/usr/bin:$PATH[INFO] Some applications may require the following environment variable too:export DOCKER_HOST=unix:///run/user/1000/docker.sock

Das Werkzeug hat drei Dinge erledigt:

  • Es hat einen Benutzerdienst geschrieben, ~/.config/systemd/user/docker.service, und ihn gestartet. systemd führt den Daemon jetzt als dein Benutzer aus und startet ihn neu, falls er abstürzt.

  • Es hat einen Kontext namens rootless angelegt und auf ihn umgeschaltet. Ein Kontext sagt dem Befehl docker, mit welchem Daemon er sprechen soll, hier mit dem Socket in /run/user/1000 statt mit /var/run/docker.sock.

  • Es hat zwei Zeilen für deine Shell ausgegeben. Die mit PATH kannst du auslassen, weil Dockers Pakete docker schon nach /usr/bin legen.

Docker mit dem Rechner starten

Ein Benutzerdienst läuft, solange du angemeldet bist, und auf deinem eigenen Computer reicht das. Auf einem Server soll Docker weiterlaufen, wenn du ssh schließt, und nach einem Neustart wieder anlaufen. Dafür erlaubst du den Diensten deines Benutzers, zu bleiben (linger):

~
$ sudo loginctl enable-linger $(whoami)$ loginctl show-user $(whoami) --property=LingerLinger=yes

Ab jetzt steuerst du Docker mit systemctl --user, ohne sudo:

bash
systemctl --user status docker     # läuft er?
systemctl --user restart docker    # neu starten, etwa nach einer Änderung der Einstellungen
journalctl --user -u docker        # sein Log, wenn etwas schiefgeht

Prüfen, ob es funktioniert

docker info sagt dir, mit welchem Daemon du sprichst und wie er läuft. Es gibt eine Menge aus, deshalb sucht grep die Zeilen heraus, auf die es ankommt:

~
$ docker info | grep -E "Context|rootless|Root Dir" Context:    rootless  rootless Docker Root Dir: /home/robin/.local/share/docker
  • Context: rootless heißt, dass der Befehl docker mit deinem eigenen Daemon spricht.

  • rootless allein ist eine der Sicherheitsoptionen des Daemons: Sie bestätigt, dass er im Rootless-Modus läuft.

  • Docker Root Dir ist der Ort, an dem deine Images, Container und Volumes jetzt liegen: in deinem Home-Ordner.

Dann startest du deinen ersten Container:

~
$ docker run --rm hello-worldUnable to find image 'hello-world:latest' locallylatest: Pulling from library/hello-world4f55086f7dd0: Pull completeDigest: sha256:c567b231f65614688711a75f31518a064258a51d442117a3e771a3a8fdba78efStatus: Downloaded newer image for hello-world:latestHello from Docker!This message shows that your installation appears to be working correctly.

Den Unterschied sehen

Jetzt der Beweis. Im Container bist du immer noch root:

~
$ docker run --rm alpine idUnable to find image 'alpine:latest' locallylatest: Pulling from library/alpineDigest: sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9aStatus: Downloaded newer image for alpine:latestuid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Starte einen Container, der eine Weile läuft, und schau ihn dir vom Host aus an. ps listet laufende Programme mit dem Benutzer, als der sie laufen:

~
$ docker run -d --name sleeper alpine sleep 300cc5ff079109a24e95f7c1704fe7b2804bacd0f27514f9c81b71888e83f79136a$ ps -o user,pid,cmd -C sleepUSER         PID CMDrobin       4821 sleep 300$ docker rm -f sleepersleeper

Root im Container ist auf dem Host robin. Bei einem Root-Daemon stünde in dieser Spalte root. Versuch jetzt, /etc/shadow zu lesen, die Datei mit den Passwort-Hashes aller Benutzer des Rechners, die nur root lesen darf:

~
$ docker run --rm -v /etc/shadow:/shadow:ro alpine cat /shadowcat: can't open '/shadow': Permission denied

Bei einem Root-Daemon gibt das jeden Hash aus. Hier sagt der Kernel nein, weil root im Container für den Host nur du bist.

Was anders funktioniert

Fast alles funktioniert wie vorher. Auf diese Unterschiede stößt du am ehesten.

Ports unter 1024

Unter Linux darf nur root Ports unter 1024 öffnen, etwa 80 für HTTP und 443 für HTTPS, und dein Daemon ist nicht root:

~
$ docker run --rm -p 80:80 nginx:alpineUnable to find image 'nginx:alpine' locallyalpine: Pulling from library/nginxStatus: Downloaded newer image for nginx:alpinedocker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint nice_mestorf (dc9f509282865ba169473877ec68d9e5f87fc77b31fcbe2d23b0ff9f76bdb268): error while calling RootlessKit PortManager.AddPort(): cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or set CAP_NET_BIND_SERVICE on rootlesskit binary, or choose a larger port number (>= 1024): listen tcp4 0.0.0.0:80: bind: permission denied

Der einfache Ausweg ist ein Port ab 1024. Auf einem Server nimmt dann ein Reverse Proxy die Anfragen auf 80 und 443 entgegen und reicht sie weiter:

~
$ docker run -d --name web -p 8080:80 nginx:alpine3a1f7c2e9b8d4f60a5e2c1b7d9f3e8a6c4b2d0f1e9a7c5b3d1f0e8a6c4b2d9f7$ curl -s localhost:8080 | grep title<title>Welcome to nginx!</title>$ docker rm -f webweb

Muss ein Container selbst auf Port 80 antworten, erlaubst du gewöhnlichen Programmen, Ports ab 80 zu öffnen. Die Datei in /etc/sysctl.d hält die Einstellung über einen Neustart hinweg, und sysctl -p wendet sie sofort an:

~
$ echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-rootless-ports.confnet.ipv4.ip_unprivileged_port_start=80$ sudo sysctl -p /etc/sysctl.d/99-rootless-ports.confnet.ipv4.ip_unprivileged_port_start = 80

Damit darf jedes Programm jedes Benutzers auf dem Rechner diese Ports öffnen, was auf einem Server, der dir allein gehört, kein Problem ist. Starte den Container danach noch einmal.

Dateien in Bind-Mounts

Ein Bind-Mount wie -v ./data:/data zeigt einen Ordner des Hosts im Container. Was root im Container dort schreibt, gehört dir. Was ein Container als anderer Benutzer schreibt, gehört einer deiner Ersatz-IDs, die der Host als bloße Nummer zeigt. Datenbanken machen das: Das offizielle Postgres-Image schreibt seine Dateien zum Beispiel als Benutzer 999. Du siehst es an einem Ordner, der im Container dem Benutzer 999 gehört:

~
$ mkdir data$ docker run --rm -v ./data:/data alpine touch /data/hello.txt$ docker run --rm -v ./data:/data alpine sh -c 'mkdir /data/db && echo hi > /data/db/file && chown -R 999:999 /data/db && chmod 700 /data/db'$ ls -l datatotal 4drwx------ 2 100998 100998 4096 Nov  3 10:05 db-rw-r--r-- 1 robin  robin     0 Nov  3 10:05 hello.txt$ rm -rf datarm: cannot remove 'data/db': Permission denied$ rootlesskit rm -rf data

db gehört 100998, der Nummer des Hosts für den Benutzer 999, und nur dieser Benutzer darf hineinschauen, deshalb kann rm den Ordner nicht leeren. rootlesskit führt einen Befehl in derselben Art Namespace aus wie Docker, in dem du root über alle deine IDs bist, und dort lässt sich der Ordner löschen.

~
$ mkdir files$ docker run --rm --user $(id -u):$(id -g) -v ./files:/data alpine touch /data/notes.txttouch: /data/notes.txt: Permission denied

Lass die Option weg, dann schreibt root im Container deine Dateien als du.

Deine eigenen Images

Der Rootless-Daemon hält seine Images, Container und Volumes in ~/.local/share/docker, sie belegen also Platz in deinem Home-Ordner. Jeder Benutzer, der den Rootless-Modus einrichtet, bekommt seinen eigenen Daemon, und niemand sieht die Container der anderen. Images von einem Root-Daemon, den du vorher hattest, sind nicht da, also lade oder baue sie neu.

Grenzen für CPU und Speicher

--memory und --pids-limit funktionieren wie gewohnt. --cpus braucht zusätzlich, dass systemd deinem Benutzer die CPU überlässt, was aktuelle Systeme tun. Diese Zeile zeigt, was dein Benutzer begrenzen darf:

~
$ cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllerscpu memory pids

Fehlt dort cpu, zeigt Dockers Seite mit Tipps, wie du es ergänzt.

Doch anders entschieden?

Das Einrichtungswerkzeug macht auch rückgängig, was es getan hat: Es hält den Benutzerdienst an, entfernt ihn und löscht den Kontext. Deine Images und Container bleiben, also löschst du sie danach, wieder mit rootlesskit, weil manche Dateien deinen Ersatz-IDs gehören:

~
$ dockerd-rootless-setuptool.sh uninstall$ rootlesskit rm -rf ~/.local/share/docker

Wie es weitergeht

Dein Docker läuft jetzt ohne root, und alles andere bleibt, wie du es kennst: docker compose up, docker build und der Rest. Um damit etwas online zu bringen, betreibt der letzte Teil der Portfolio-Serie, Bring dein Portfolio online, ein CMS und eine Website mit Docker Compose auf einem Server. Er installiert Docker der Kürze halber als root. Mit Rootless Docker legst du das Projekt stattdessen in deinen Home-Ordner statt nach /opt und erlaubst die Ports ab 80 wie oben gezeigt, weil sein Caddy-Container auf 80 und 443 antwortet.

Kommentare

Die Kommentare sind GitHub Discussions: Zum Schreiben mit GitHub anmelden.

Die Kommentare kommen über giscus von GitHub. Beim Laden gehen deine IP-Adresse an giscus.app und GitHub.