Was Docker unter der Haube wirklich macht
Docker-Container sind keine Magie und auch keine leichtgewichtigen virtuellen Maschinen, sondern gewöhnliche Linux-Prozesse, denen der Kernel über Namespaces eine eingeschränkte Sicht auf Prozesse, Netzwerk und Dateisystem gibt, während cgroups ihren Ressourcenverbrauch begrenzen. Wer diese beiden Mechanismen versteht, kann jeden laufenden Container direkt vom Host aus inspizieren und einordnen.
Inhaltsverzeichnis
- 1. Was Namespaces und cgroups wirklich sind
- 2. Die Namespace-Typen im Überblick
- 3. PID- und Netzwerk-Namespace im Detail
- 4. Mount- und User-Namespace: Dateisystem und Rechte
- 5. cgroups: Ressourcenbegrenzung statt Isolation
- 6. cgroup v2: Hierarchie und Controller in der Praxis
- 7. Laufenden Container-Prozess vom Host aus inspizieren
- 8. Eigene Sandboxes mit unshare und nsenter bauen
- 9. Sicherheitsgrenzen und Namespaces/cgroups im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Namespaces und cgroups wirklich sind
Wer zum ersten Mal einen Container startet, hat oft den Eindruck, eine Art Mini-virtuelle-Maschine zu bekommen: eigenes Dateisystem, eigene IP-Adresse, eigene Prozessliste. Tatsächlich ist ein Container aber nichts anderes als ein ganz normaler Linux-Prozess, den der Host-Kernel mit einer eingeschränkten Sicht auf die Welt startet. Ein Blick mit ps aux auf dem Host zeigt jeden Container-Prozess ganz gewöhnlich in der Prozessliste, mit einer eigenen PID, die außerhalb des Containers völlig normal aussieht. Es gibt keinen Hypervisor, keinen zweiten Kernel und keine emulierte Hardware, nur cleveres Kernel-Bookkeeping.
Zwei getrennte Kernel-Mechanismen ermöglichen diese Illusion. Namespaces steuern, was ein Prozess überhaupt sehen kann: welche anderen Prozesse, welche Netzwerkschnittstellen, welches Dateisystem. cgroups (Control Groups) steuern dagegen, wie viel ein Prozess von den tatsächlich geteilten Ressourcen wie CPU, Arbeitsspeicher und Ein-/Ausgabe verbrauchen darf. Docker, Podman, containerd und Kubernetes bauen alle auf denselben zwei Bausteinen auf, nur mit unterschiedlicher Orchestrierung darüber. Wer unshare und die cgroup-Dateisysteme direkt bedienen kann, versteht damit auch, was jedes High-Level-Tool im Hintergrund tut, und kann Container-Probleme debuggen, ohne sich auf die Abstraktion verlassen zu müssen.
2. Die Namespace-Typen im Überblick
Der Linux-Kernel kennt mittlerweile acht Namespace-Typen, die jeweils eine andere Ressourcenklasse isolieren. Der PID-Namespace gibt einem Prozess eine eigene Prozessnummerierung, sodass der erste Prozess darin die PID 1 erhält, unabhängig von seiner tatsächlichen PID auf dem Host. Der Network-Namespace isoliert Netzwerkschnittstellen, Routing-Tabellen und Firewall-Regeln. Der Mount-Namespace gibt eine eigene Sicht auf den Verzeichnisbaum, der UTS-Namespace isoliert Hostname und Domainname, und der IPC-Namespace trennt System-V-IPC-Objekte und POSIX-Message-Queues.
Zwei weitere Typen runden das Bild ab: Der User-Namespace erlaubt es, dass ein Prozess innerhalb des Containers als root läuft, auf dem Host aber als unprivilegierter Benutzer gemappt ist, was ein zentraler Sicherheitsgewinn ist. Der cgroup-Namespace schließlich verbirgt die tatsächliche cgroup-Hierarchie des Hosts vor dem Container. Das Werkzeug lsns listet alle aktiven Namespaces auf dem System auf, inklusive Typ, Anzahl der zugehörigen Prozesse und Owner. Diese Übersicht ist der erste Schritt, um zu verstehen, welche Isolation ein laufender Container tatsächlich nutzt.
#!/usr/bin/env bash
# List all active namespaces on the host, grouped by type
lsns -t pid,net,mnt,uts,ipc,user
# Create a fully isolated set of namespaces for a new bash shell
unshare --pid --net --mount --uts --ipc --fork --mount-proc bash
# Inside the new shell: only the shell itself and its children are visible
ps aux
# PID TTY TIME CMD
# 1 pts/2 00:00:00 bash
# 12 pts/2 00:00:00 ps
3. PID- und Netzwerk-Namespace im Detail
Der PID-Namespace ist der augenfälligste: Innerhalb eines frisch gestarteten Containers zeigt ps aux meist nur eine Handvoll Prozesse, angeführt von PID 1. Auf dem Host dagegen trägt derselbe Prozess eine völlig andere, meist deutlich höhere PID. PID-Namespaces sind zudem verschachtelbar: Ein Prozess im äußeren Namespace sieht alle Prozesse der inneren Namespaces, aber nicht umgekehrt, ein innerer Prozess kann niemals einen äußeren Prozess sehen oder signalisieren. Genau diese Einbahnstraßen-Sichtbarkeit ist der Kern der Isolation.
Der Netzwerk-Namespace geht noch weiter: Ein neuer Network-Namespace startet komplett ohne Netzwerkschnittstellen außer einem eigenen, isolierten Loopback-Interface. Docker verbindet einen Container über ein virtuelles Ethernet-Paar (veth) mit einer Bridge auf dem Host, sodass der Container-Namespace eine Seite des Paares als eth0 sieht, während die andere Seite auf der Host-Bridge landet. Mit ip netns exec oder direkt über nsenter --net lässt sich jeder Network-Namespace einzeln betreten und untersuchen, komplett unabhängig davon, ob Docker überhaupt beteiligt war.
4. Mount- und User-Namespace: Dateisystem und Rechte
Der Mount-Namespace ist die Grundlage für das isolierte Root-Dateisystem eines Containers. Innerhalb eines neuen Mount-Namespace kann ein Prozess mit pivot_root oder chroot in ein komplett anderes Wurzelverzeichnis wechseln, ohne dass sich das auf den Host oder andere Namespaces auswirkt. Wichtig ist dabei die Mount-Propagation: Standardmäßig sind neue Mounts private, das heißt Änderungen im Container-Namespace bleiben unsichtbar für den Host und umgekehrt. Wird ein Mount als shared markiert, propagieren Änderungen in beide Richtungen, was für Bind-Mounts zwischen Host und Container gezielt genutzt wird.
Der User-Namespace verschiebt zusätzlich die Bedeutung von Benutzer- und Gruppen-IDs. Ein Prozess kann innerhalb seines Namespace mit UID 0 als root laufen und trotzdem auf dem Host als UID 100000 oder eine andere unprivilegierte ID gemappt sein. Dieses Mapping wird über /proc/PID/uid_map und /proc/PID/gid_map konfiguriert. Ohne User-Namespace-Remapping ist root im Container faktisch root auf dem Host, sobald ein Kernel-Bug oder eine Fehlkonfiguration die Isolation durchbricht, weshalb rootless Podman und Docker mit userns-remap dieses Feature aktiv nutzen, um genau dieses Risiko zu reduzieren.
5. cgroups: Ressourcenbegrenzung statt Isolation
Während Namespaces festlegen, was ein Prozess sehen darf, legen cgroups fest, wie viel er von den geteilten Systemressourcen verbrauchen darf. Ohne cgroups könnte ein einzelner Container durch einen Memory-Leak den kompletten Host in die Knie zwingen, weil er theoretisch den gesamten verfügbaren Arbeitsspeicher belegen könnte. cgroups verhindern das durch harte oder weiche Grenzen pro Controller: cpu für Rechenzeit, memory für Arbeitsspeicher, io für Block-Device-Durchsatz und pids für die maximale Anzahl an Prozessen, ein wirksamer Schutz gegen Fork-Bomben innerhalb eines Containers.
cgroups sind hierarchisch organisiert und werden über ein virtuelles Dateisystem unter /sys/fs/cgroup verwaltet, nicht über eine klassische Systemaufruf-API. Das bedeutet: Limits setzen heißt im Kern, Textwerte in bestimmte Dateien zu schreiben. Ein Docker-Container mit --memory=512m und --cpus=1.5 erzeugt im Hintergrund genau eine solche cgroup mit entsprechend gesetzten Dateien, die der Kernel-Scheduler und der Memory-Controller bei jeder Ressourcenzuteilung respektieren.
6. cgroup v2: Hierarchie und Controller in der Praxis
cgroup v2 hat die ältere, oft inkonsistente v1-Hierarchie mit mehreren getrennten Controller-Bäumen durch eine einzige, vereinheitlichte Hierarchie ersetzt. Alle modernen Distributionen mit systemd als Init-System nutzen standardmäßig cgroup v2, erkennbar an einem einzigen Mountpoint unter /sys/fs/cgroup statt der früheren Aufteilung nach Controllern. Jede cgroup ist als Verzeichnis abgebildet, jede Ressourcengrenze als einzelne Datei darin, etwa cpu.max für die CPU-Quote oder memory.max für die harte Speichergrenze.
Systemd integriert cgroup v2 tief in seine Unit-Dateien: Ein Service mit MemoryMax=512M und CPUQuota=150% in seiner Unit-Datei erzeugt automatisch die passende cgroup-Struktur unter /sys/fs/cgroup/system.slice/. Für Container-Runtimes bedeutet das, dass sowohl Docker als auch systemd auf dieselbe cgroup-v2-Hierarchie zugreifen, was Konflikte vermeidet und konsistentes Monitoring über Tools wie systemd-cgtop ermöglicht, unabhängig davon, ob ein Prozess über Docker oder direkt über systemd gestartet wurde.
# /etc/systemd/system/webapp.service
[Unit]
Description=Webapp with cgroup v2 resource limits
After=network.target
[Service]
ExecStart=/usr/bin/webapp --port 8080
# CPU quota: 150% of one core, written to cpu.max
CPUQuota=150%
# Hard memory ceiling, written to memory.max
MemoryMax=512M
# Soft memory floor, written to memory.low
MemoryLow=128M
# Fork bomb protection, written to pids.max
TasksMax=200
Restart=on-failure
[Install]
WantedBy=multi-user.target
7. Laufenden Container-Prozess vom Host aus inspizieren
Der praktische Nutzen dieses Wissens zeigt sich beim Debuggen: Statt sich ausschließlich auf docker exec zu verlassen, lässt sich jeder Container-Prozess direkt vom Host aus untersuchen. Der erste Schritt ist, die PID des Hauptprozesses zu ermitteln, entweder über docker inspect --format '{{.State.Pid}}' <container> oder direkt über ps aux | grep containerd-shim. Mit dieser PID lassen sich anschließend die Namespace-Zugehörigkeiten direkt unter /proc/PID/ns/ ablesen: Jede Datei dort ist ein symbolischer Link auf einen Namespace, dessen Inode-Nummer eindeutig identifiziert, welche Prozesse denselben Namespace teilen.
Zwei Prozesse mit identischer Inode-Nummer in /proc/PID/ns/net teilen sich exakt denselben Network-Namespace, unabhängig davon, ob sie über Docker gestartet wurden oder nicht. Das Kommando nsenter nutzt diese Information, um in genau die Namespaces eines laufenden Prozesses zu wechseln, ganz ohne Docker-Client. Auch docker inspect selbst liefert im JSON die tatsächlich gesetzten cgroup-Limits im HostConfig-Block, sodass sich Speicher- und CPU-Grenzen eines Containers auch ohne docker stats nachvollziehen lassen.
{
"State": {
"Pid": 48213,
"Status": "running"
},
"HostConfig": {
"Memory": 536870912,
"MemorySwap": 536870912,
"NanoCpus": 1500000000,
"PidsLimit": 200,
"CgroupParent": ""
},
"NetworkSettings": {
"SandboxKey": "/var/run/docker/netns/a1b2c3d4e5f6"
}
}
#!/usr/bin/env bash
set -euo pipefail
# Find the main process PID of a running container
CONTAINER="webshop-app"
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
echo "Host PID: $PID"
# Every namespace the process belongs to is a symlink here
ls -la /proc/"$PID"/ns/
# net:[4026532451] mnt:[4026532448] pid:[4026532452] ...
# Enter the container's network and mount namespace directly from the host
sudo nsenter --target "$PID" --net --mount --pid bash
# Confirm: two processes sharing a namespace show the same inode number
readlink /proc/"$PID"/ns/net
readlink /proc/self/ns/net
8. Eigene Sandboxes mit unshare und nsenter bauen
Das Kommando unshare erstellt neue Namespaces für einen Prozess, ganz ohne Container-Runtime. Der Aufruf unshare --pid --net --mount --uts --ipc --fork --mount-proc bash startet eine neue Bash-Shell, die in praktisch derselben Isolation läuft wie ein frisch gestarteter Docker-Container, nur ohne Overlay-Filesystem und Image-Layer. Das Flag --mount-proc ist entscheidend: Es sorgt dafür, dass /proc im neuen Mount-Namespace neu gemountet wird, sonst würde ps aux in der neuen Shell weiterhin die Prozesse des Hosts anzeigen, weil /proc selbst ein gemountetes Dateisystem ist, das ohne Neumount weiterhin auf den alten Namespace zeigt.
Das Gegenstück zu unshare ist nsenter: Statt neue Namespaces zu erzeugen, betritt es die Namespaces eines bereits laufenden Prozesses. nsenter --target <pid> --pid --net --mount bash öffnet eine Shell mit exakt derselben Sicht wie der Zielprozess, was in der Praxis ein exzellentes Debugging-Werkzeug für Container ohne funktionierende Shell im Image ist, etwa bei minimalen Distroless-Images, in denen docker exec mangels Shell scheitert. Root-Rechte auf dem Host sind für beide Kommandos in der Regel erforderlich, da das Betreten fremder Namespaces ein privilegierter Vorgang ist.
9. Sicherheitsgrenzen und Namespaces/cgroups im Vergleich
Namespaces und cgroups sind mächtige Isolationswerkzeuge, aber sie sind keine Sicherheitsgrenze auf dem Niveau einer virtuellen Maschine. Alle Container auf einem Host teilen sich denselben Kernel: Eine Kernel-Schwachstelle, ein Bug im Namespace-Code oder eine falsch konfigurierte Capability kann theoretisch aus der Isolation ausbrechen. Werkzeuge wie seccomp-Profile, AppArmor oder SELinux ergänzen Namespaces und cgroups deshalb um zusätzliche Schichten, die einzelne Systemaufrufe blockieren oder Dateizugriffe weiter einschränken, selbst wenn ein Prozess formal im richtigen Namespace läuft.
In der Praxis führt das häufig zu falschen Annahmen darüber, was Container tatsächlich leisten. Die folgende Tabelle stellt verbreitete Fehleinschätzungen der korrekten Einordnung gegenüber, jeweils mit Blick auf die konkrete Konsequenz für den Betrieb.
| Bereich | Verbreitete Fehleinschätzung | Korrekte Einordnung | Warum das wichtig ist |
|---|---|---|---|
| Prozessisolation | chroot allein reicht | PID- und Mount-Namespace kombinieren | chroot lässt sich per Directory-Traversal leicht umgehen |
| Ressourcenbegrenzung | ulimit pro Prozess genügt | cgroup v2 mit cpu.max und memory.max | ulimit wirkt nur pro Prozess, nicht pro Gruppe |
| Sicherheitsniveau | Container = VM-Level Isolation | Namespaces teilen sich den Host-Kernel | Ein Kernel-Exploit kann theoretisch alle Container treffen |
| Netzwerktrennung | iptables-Regeln reichen aus | Eigener net-Namespace mit veth und Bridge | Ohne eigenen Namespace teilen sich Prozesse den Stack |
| Container-Debugging | docker exec ist der einzige Zugang | nsenter direkt über /proc/PID/ns | Funktioniert auch bei minimalen Images ohne Shell |
Wer besonders schutzbedürftige Workloads betreibt, sollte über reine Namespace-Isolation hinausdenken. Laufzeiten wie gVisor oder Kata Containers fügen eine zusätzliche Abstraktionsschicht ein, entweder durch einen Nutzerraum-Kernel oder eine leichtgewichtige VM pro Container, und schließen damit genau die Lücke, die reine Namespace- und cgroup-Isolation offen lässt.
Mironsoft
Kernel-nahe Linux-Administration, Container-Hardening und Deployment-Infrastruktur
Container-Infrastruktur, die ihr wirklich versteht?
Wir analysieren eure Container- und Deployment-Umgebung, prüfen Namespace- und cgroup-Konfiguration auf Sicherheitslücken und bauen belastbare, nachvollziehbare Infrastruktur statt Blackbox-Deployments.
Namespace- & cgroup-Audit
Isolationsgrenzen und Ressourcenlimits eurer Container prüfen
Container-Hardening
seccomp, User-Namespace-Remapping und Read-Only-Filesysteme einrichten
CI/CD & Deployment
Robuste Deployment-Pipelines mit reproduzierbaren Ressourcenlimits
10. Zusammenfassung
Die Linux Namespaces und cgroups lösen zwei getrennte Probleme: Namespaces bestimmen, was ein Prozess sehen darf, cgroups bestimmen, wie viel er von den geteilten Ressourcen verbrauchen darf. Zusammen bilden sie das Fundament, auf dem Docker, Podman, containerd und Kubernetes aufsetzen, ohne dass dafür ein Hypervisor oder ein zweiter Kernel nötig wäre. PID-, Netzwerk- und Mount-Namespace isolieren die drei Ressourcenklassen, die Nutzer am häufigsten mit Containern verbinden, während der User-Namespace zusätzlich Root-Rechte innerhalb des Containers von echten Root-Rechten auf dem Host entkoppelt.
Der praktische Gewinn dieses Wissens liegt im Debugging: Mit /proc/PID/ns/, lsns und nsenter lässt sich jeder laufende Container-Prozess direkt vom Host aus untersuchen, unabhängig davon, ob eine Shell im Image vorhanden ist oder der Docker-Client gerade funktioniert. Wichtig bleibt: Namespaces und cgroups sind keine Sicherheitsgrenze auf VM-Niveau, weil alle Container denselben Kernel teilen. seccomp, AppArmor, User-Namespace-Remapping und im Extremfall Laufzeiten wie gVisor oder Kata Containers ergänzen die Isolation dort, wo es wirklich darauf ankommt.
Linux Namespaces und cgroups - Das Wichtigste auf einen Blick
Namespaces = Sichtbarkeit
PID, Net, Mount, UTS, IPC, User und cgroup-Namespace bestimmen, was ein Prozess überhaupt sehen kann.
cgroups = Ressourcengrenzen
cpu.max, memory.max, pids.max begrenzen, wie viel ein Prozess von den geteilten Ressourcen nutzt.
Direkt inspizierbar
/proc/PID/ns/, nsenter und lsns funktionieren auf jedem Linux-Host, unabhängig von Docker.
Keine VM-Isolation
Alle Container teilen den Host-Kernel. seccomp, AppArmor und User-Namespace-Remapping als zusätzliche Schicht nutzen.