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:
im Container auf dem Host
root (0) -> robin (1000), also du
Benutzer 1 -> 100000
Benutzer 999 -> 100998
Benutzer 65536 -> 165535Diese 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
sudoverwenden 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:
# 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 updateDann installierst du Docker zusammen mit dem, was der Rootless-Modus braucht:
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin \
docker-compose-plugin docker-ce-rootless-extras uidmap dbus-user-sessiondocker-ceist die Docker Engine mit ihrem Daemon, unddocker-ce-clider Befehldocker.containerd.ioführt die Container für den Daemon aus.docker-buildx-pluginunddocker-compose-pluginbringendocker buildunddocker composemit.docker-ce-rootless-extrasbringt RootlessKit, das den Namespace baut, und das Einrichtungswerkzeug, das du gleich startest.uidmapenthältnewuidmapundnewgidmap.dbus-user-sessionlä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
rootlessangelegt und auf ihn umgeschaltet. Ein Kontext sagt dem Befehldocker, mit welchem Daemon er sprechen soll, hier mit dem Socket in/run/user/1000statt mit/var/run/docker.sock.Es hat zwei Zeilen für deine Shell ausgegeben. Die mit
PATHkannst du auslassen, weil Dockers Paketedockerschon nach/usr/binlegen.
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:
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 schiefgehtPrü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: rootlessheißt, dass der Befehldockermit deinem eigenen Daemon spricht.rootlessallein ist eine der Sicherheitsoptionen des Daemons: Sie bestätigt, dass er im Rootless-Modus läuft.Docker Root Dirist 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.