Gemeinsame Basis: worin sie sich nicht unterscheiden
Vergleiche starten oft beim Marketing. Nützlicher ist die Feststellung, wie viel identisch ist – der Rest des Artikels handelt nur noch von den Abweichungen.
Identisch bei beiden
- Isolation – Namespaces, cgroups, seccomp, Capabilities und LSM wie SELinux oder AppArmor. Beide „erfinden" keine Isolation, sie konfigurieren den Kernel
- Images – dasselbe OCI-Format, dieselben Registries, dieselben Layer
- Dateiformat – Containerfile und Dockerfile sind dasselbe, nur anders benannt
- Runtime – am Ende startet ein OCI-Runtime-Binary den Prozess:
runcbei Docker,crunbei Podman (beide sind austauschbar) - Kommandos –
run,ps,logs,exec,build,pushheißen und wirken gleich
Der eine Unterschied, aus dem alle anderen folgen
Docker ist eine Client-Server-Anwendung: Die CLI schickt Befehle an einen dauerhaft laufenden Dienst, der alle Container besitzt. Podman startet Container als gewöhnliche Kindprozesse des aufrufenden Benutzers – es gibt keinen zentralen Dienst, der etwas besitzt.
Daraus folgen die Themen der nächsten Abschnitte: Wem gehören die Prozesse (1), mit welchen Rechten laufen sie (2), wer richtet das Netzwerk ein (3), wem gehören die Dateien im Volume (4) und wer startet alles nach einem Reboot (6).
| Frage | Docker | Podman |
|---|---|---|
| Wer besitzt die Container? | dockerd | dein Benutzer |
| Rechte im Normalfall | root (rootless möglich) | dein Benutzer (rootful möglich) |
| Zustand nach Reboot | Daemon startet Container | systemd startet Container |
| API | immer da (Socket) | optional (podman system service) |
| Gruppierung | Compose-Projekte | Pods und Compose |
Prozessmodell: Daemon gegen Wächterprozess
Der sichtbarste Unterschied steht in der Prozessliste. Er erklärt, warum ein Docker-Neustart alle Container betrifft und ein Podman-Problem meist nur einen.
Wer hängt an wem
Bei Docker geht jeder Befehl an dockerd; darunter verwaltet containerd je Container einen Shim-Prozess. Bei Podman baut der aufrufende Prozess den Container auf, übergibt ihn an conmon und beendet sich – zurück bleibt ein Wächter je Container, dessen Elternprozess systemd ist.
Praktisch heißt das: ps -ef zeigt bei Podman keine gemeinsame Wurzel. Genau deshalb kann Podman Container nicht „von sich aus" nach einem Reboot starten – dazu später mehr in Abschnitt 6.
Was ein Ausfall bedeutet
- Docker – stirbt der Daemon, sind alle Verwaltungsbefehle tot. Ob die Container weiterlaufen, hängt an
live-restore - Podman – stirbt ein conmon, betrifft das genau einen Container. Es gibt keinen Prozess, dessen Ausfall alle betrifft
Was ein Update bedeutet
- Docker – ein Engine-Update ist ein Neustart des Daemons und damit ein Eingriff in alle Container gleichzeitig
- Podman – ein Update tauscht ein Binary aus; laufende Container merken davon nichts, weil sie nicht an ihm hängen
"live-restore": true in der daemon.json – und diese Option ist nicht der Standard und mit Swarm unvereinbar. Ohne sie beendet ein Daemon-Neustart alle Container. Umgekehrt gilt mit live-restore: Die Container laufen zwar, aber docker ps, Logs und Health-Status sind währenddessen nicht verfügbar. Beides sollte man wissen, bevor man den Punkt in einer Architekturdiskussion anführt.Prozessbaum selbst ansehen
Der Unterschied lässt sich in zwei Befehlen belegen – nützlich, wenn in einer Diskussion behauptet wird, Podman habe „auch einen Daemon".
# Docker: alles hängt unter dockerd bzw. containerd
ps -ef --forest | grep -A 5 dockerd
# Podman: je Container ein conmon, Elternprozess systemd
ps -ef --forest | grep conmon
systemctl --user status
Sichtbar wird außerdem, dass podman run im Vordergrund kein Client ist, der wartet, sondern selbst der Prozess, der den Container aufbaut.
Rechte: was root im Container auf dem Host bedeutet
In beiden Fällen ist der Prozess im Container standardmäßig root. Der Unterschied ist, worauf diese Null auf dem Host abgebildet wird.
Der Eskalationspfad im Vergleich
Ein Ausbruch aus dem Container ist kein theoretischer Fall – er ist der Grund, warum Rootless existiert. Entscheidend ist, welche Rechte der ausgebrochene Prozess auf dem Host hat.
Rootless heißt nicht, dass im Container kein root existiert, sondern dass dieses root über User-Namespaces auf einen unprivilegierten Bereich abgebildet wird – festgelegt in /etc/subuid und /etc/subgid.
Was rootless kostet
| Einschränkung | Grund | Ausweg |
|---|---|---|
| Ports unter 1024 | privilegierte Ports | net.ipv4.ip_unprivileged_port_start senken oder auf höheren Port veröffentlichen |
| Datei-Eigentümer im Volume | UID-Abbildung | :U, --userns=keep-id |
| Manche Netzwerkmodi | keine Rechte für Bridges am Host | rootful betreiben oder Portweiterleitung nutzen |
| Overlay-Dateisystem | kernelabhängig | native Overlay-Unterstützung oder fuse-overlayfs |
/var/run/docker.sock ist gleichbedeutend mit root auf dem Host – auch als „nur lesend" eingebundenes Volume, denn über die API lässt sich ein privilegierter Container mit gemountetem Wurzelverzeichnis starten. In CI-Runnern, Monitoring-Agenten und Deploy-Tools wird dieser Mount routinemäßig als harmlose Bequemlichkeit behandelt. Wer Podman rootless nutzt, hat dieses Problem nicht, weil es keinen privilegierten Socket gibt.Rootless prüfen und einrichten
Vor der Fehlersuche lohnt die Feststellung, in welchem Modus man überhaupt arbeitet – der Unterschied ist an der Ausgabe sofort erkennbar.
podman info --format '{{.Host.Security.Rootless}}'
id -u
grep "^$USER:" /etc/subuid /etc/subgid
Fehlen die subuid-Bereiche, startet rootless gar nicht erst sauber. Nachtragen und danach die Zuordnung neu aufbauen lassen.
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 <user>
podman system migrate
Bei Docker ist rootless kein Schalter, sondern eine eigene Installation je Benutzer.
dockerd-rootless-setuptool.sh install
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
Netzwerk: zwei Stacks mit denselben Begriffen
Die Kommandos sehen gleich aus, die Umsetzung darunter ist verschieden. Wichtig wird das bei DNS zwischen Containern, bei veröffentlichten Ports und bei Firewall-Regeln.
Wer macht was
| Aufgabe | Docker | Podman |
|---|---|---|
| Bridge und Regeln | eingebaut, nftables oder iptables | netavark, seit Podman 6 nftables |
| DNS zwischen Containern | eingebauter Resolver | aardvark-dns |
| Rootless-Netz | gvisor-tap-vsock (Docker 29) | pasta (Podman 5/6) |
| Standardnetz | bridge | podman |
Gemeinsam ist beiden eine Falle, die nichts mit dem Werkzeug zu tun hat: Im vorgegebenen Standardnetz gibt es keine Namensauflösung zwischen Containern. Die gibt es erst in einem selbst angelegten Netz.
podman network create appnet
podman run -d --network appnet --name db postgres:16
podman run --rm --network appnet alpine ping -c1 db
--network=pasta mit passenden Optionen, sonst bleibt rootful.Portveröffentlichung und Firewall prüfen
Wenn ein veröffentlichter Port nicht antwortet, ist die erste Frage, ob überhaupt jemand lauscht – und auf welcher Adresse.
podman port <container>
ss -tlnp | grep <port>
Bei Docker kommt die Besonderheit dazu, dass veröffentlichte Ports Firewall-Regeln erzeugen, die an einer eventuell konfigurierten Host-Firewall vorbeigehen können. Die tatsächlich aktiven Regeln stehen im Firewall-Backend, nicht in der Werkzeugausgabe.
# je nach Backend
sudo nft list ruleset | grep -i docker
sudo iptables -t nat -L DOCKER -n
Docker 29 kann nftables als Backend nutzen; Podman 6 hat den iptables-Weg entfernt. Auf gemischten Hosts lohnt der Blick, welches Backend gerade aktiv ist, bevor Regeln von Hand ergänzt werden.
Volumes: wem die Dateien gehören
Der häufigste Stolperstein beim Umstieg. Ein Bind-Mount, der unter Docker funktioniert, liefert unter Podman rootless „Permission denied" – ohne dass an den Rechten etwas falsch wäre.
Warum die Rechte anders wirken
Rootless bildet die UIDs im Container auf einen subuid-Bereich ab. Wer im Container als root schreibt, erzeugt auf dem Host Dateien, die einer UID wie 100000 gehören – nicht dir. Umgekehrt gehören deine Dateien im Container einer UID, die dort niemand kennt.
| Situation | Mittel | Wirkung |
|---|---|---|
| Container soll in dein Verzeichnis schreiben | --userns=keep-id | deine UID bleibt im Container dieselbe |
| Bestehende Daten passend machen | :U am Mount | Podman ändert den Eigentümer rekursiv |
| SELinux-System (RHEL, Fedora) | :Z oder :z | setzt den Kontext, sonst Zugriff verweigert |
# eigener User im Container, SELinux-Kontext privat setzen
podman run --rm -v ./data:/data:Z,U --userns=keep-id alpine touch /data/probe
chmod 777. Das behebt nichts, weil die Ursache nicht die Zugriffsmaske ist, sondern die UID-Abbildung oder der SELinux-Kontext – dafür bleibt danach ein weltweit beschreibbares Verzeichnis zurück. Erst ls -n auf dem Host und id im Container vergleichen, dann :U, keep-id oder :Z einsetzen.Wo die Daten liegen
Rootless speichert alles im Benutzerprofil, nicht unter /var/lib. Das ist der Grund, warum Images nach einem Wechsel zwischen sudo podman und podman scheinbar verschwinden – es sind zwei getrennte Bestände.
podman info --format '{{.Store.GraphRoot}}'
# rootless: ~/.local/share/containers/storage
# rootful: /var/lib/containers/storage
docker info --format '{{.DockerRootDir}}'
Praktische Folge: Ein Image, das mit sudo gezogen wurde, steht dem unprivilegierten Aufruf nicht zur Verfügung, und umgekehrt. Bei Platzproblemen lohnt der Blick in beide Verzeichnisse.
Images: bauen, benennen, beziehen
Gleiches Format, andere Werkzeuge darunter – und eine Namensauflösung, die sich anders verhält, als man es von Docker gewohnt ist.
Docker
- BuildKit – paralleler Bau, Cache-Mounts, Secrets im Build
- Image-Store – ab Docker 29 bei Neuinstallationen containerd statt overlay2
- Namen – unqualifizierte Namen gehen immer zu Docker Hub
Podman
- Buildah –
podman buildist Buildah unter anderem Namen; Buildah kann zusätzlich ohne Containerfile bauen - Skopeo – Images zwischen Registries kopieren und prüfen, ohne sie zu ziehen
- Namen –
registries.confentscheidet, wohin ein unqualifizierter Name zeigt
podman run nginx ist nicht dasselbe wie docker run nginx. Podman löst kurze Namen über die Suchliste in /etc/containers/registries.conf auf – je nach Distribution und Konfiguration wird interaktiv nachgefragt, eine andere Registry bevorzugt oder der Aufruf schlägt fehl. In Skripten, Dockerfiles und CI-Pipelines gehören Namen deshalb vollständig qualifiziert: docker.io/library/nginx:1.27. Sonst zieht dieselbe Zeile auf zwei Hosts zwei verschiedene Images.Build-Unterschiede, die im Alltag auffallen
Beide bauen aus derselben Datei, aber die BuildKit-Erweiterungen sind Docker-spezifisch. Ein Containerfile mit Cache-Mounts läuft unter Podman nicht zwingend gleich.
# BuildKit-Syntax – unter Podman nicht in jedem Fall unterstützt
RUN --mount=type=cache,target=/root/.cache/go-build go build ./...
Wer beide Wege bedienen muss, hält den Bau bei den Grundfunktionen und legt Abhängigkeiten in eigene Schichten, statt sich auf Cache-Mounts zu verlassen.
Nützlich und ohne Docker-Gegenstück ist das Prüfen und Kopieren von Images ohne lokalen Pull:
skopeo inspect docker://docker.io/library/nginx:1.27
skopeo copy docker://docker.io/library/nginx:1.27 docker://registry.intern/nginx:1.27
policy.json festgelegt statt pro Aufruf.Betrieb: Autostart, Compose, Pods
Hier wird das Prozessmodell zur Alltagsfrage: Ohne Daemon gibt es niemanden, der nach einem Reboot etwas startet. Diese Rolle übernimmt systemd.
Wer startet den Container nach dem Reboot
| Docker | Podman | |
|---|---|---|
| Zuständig | der Daemon | systemd |
| Konfiguration | --restart=always | Quadlet-Unit oder podman-restart.service |
| Ablage | im Daemon-Zustand | Datei unter ~/.config/containers/systemd/ |
| Für Benutzerdienste | – | loginctl enable-linger <user> |
Quadlet ist der empfohlene Weg: Eine kurze Unit-Datei beschreibt den Container, systemd erzeugt daraus beim Neuladen einen Dienst. Das frühere Erzeugen von Unit-Dateien aus laufenden Containern gilt als überholt.
# ~/.config/containers/systemd/web.container
[Container]
Image=docker.io/library/nginx:1.27
PublishPort=8080:80
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user start web
loginctl enable-linger $USER
podman run --restart=always wird ohne Fehlermeldung angenommen und wirkt im laufenden Betrieb – nach einem Reboot ist der Container trotzdem weg, weil kein Daemon existiert, der ihn wieder startet. Es braucht eine Quadlet-Unit oder den aktivierten podman-restart.service, und für Benutzerdienste zusätzlich enable-linger, sonst beendet systemd beim Abmelden alles. Das ist der häufigste Umstiegsfehler überhaupt: In der Testphase läuft alles, der erste Neustart des Servers räumt den Dienst ab.Compose
Compose-Dateien laufen zum großen Teil unverändert. Zwei Wege: podman-compose als eigenständige Umsetzung oder das originale Compose gegen den Podman-Socket.
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
docker compose up -d
Pods und Kubernetes
Podman kennt Pods als echtes Objekt – mehrere Container teilen sich einen Netzwerk-Namespace, genau wie in Kubernetes. Daraus lässt sich YAML erzeugen und wieder abspielen.
podman pod create --name app -p 8080:80
podman kube generate app > app.yaml
podman kube play app.yaml
Tools, die einen Docker-Socket erwarten
alias docker=podman deckt die Kommandozeile ab, aber nicht Werkzeuge, die direkt mit der API sprechen – etwa Testcontainers, Buildpacks oder manche CI-Runner. Für sie braucht es den kompatiblen Socket.
systemctl --user enable --now podman.socket
podman info --format '{{.Host.RemoteSocket.Path}}'
Danach erwarten die meisten Werkzeuge nur noch die passende Umgebungsvariable. Testcontainers braucht zusätzlich den Hinweis, dass der Socket nicht privilegiert ist.
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
export TESTCONTAINERS_RYUK_DISABLED=true
Umstieg und Entscheidung, der Reihe nach
Ein Wechsel scheitert selten an den Kommandos und fast immer an Autostart, Volumes und Registry-Namen. In dieser Reihenfolge lässt sich das abarbeiten.
Die vier Phasen auf einen Blick
- Phase A · Betriebsart festlegenRootless oder rootful, und ob eine Docker-kompatible API gebraucht wird.
- Phase B · Images und NamenKurze Namen qualifizieren, Registries konfigurieren.
- Phase C · Daten und RechteVolumes, UID-Abbildung, SELinux-Kontexte klären.
- Phase D · Start und ÜberwachungQuadlet-Units schreiben, Reboot testen, Monitoring anpassen.
Phase A · Betriebsart
Schritt 1 · Rootless oder rootful entscheiden
Rootless ist der Normalfall und der eigentliche Gewinn. Rootful bleibt sinnvoll, wenn privilegierte Ports ohne Umweg, echte Client-Adressen oder besondere Netzwerkmodi gebraucht werden. Die Entscheidung fällt zuerst, weil sie Speicherort, Netzwerk und Rechte bestimmt.
podman info --format '{{.Host.Security.Rootless}}'
Phase B · Images
Schritt 2 · Namen qualifizieren
Jeder unqualifizierte Name in Compose-Dateien, Containerfiles und Skripten wird zur Fehlerquelle, sobald die Suchliste anders aussieht. Einmal durchsuchen und vollständige Namen eintragen.
grep -rn "image:" --include="*.yml" --include="*.yaml" . | grep -v "/"
podman info --format '{{.Registries}}'
Phase C · Daten
Schritt 3 · Volumes und Rechte klären
Vor der Umstellung feststellen, welcher Benutzer im Container schreibt und wem die Dateien auf dem Host gehören müssen. Danach entscheidet sich, ob keep-id, :U oder ein angepasster Benutzer im Image der richtige Weg ist.
podman run --rm -v ./data:/data:Z alpine sh -c 'id; ls -ln /data'
Bestehende Docker-Volumes liegen unter /var/lib/docker/volumes; sie werden nicht automatisch übernommen. Der einfachste Weg ist ein Archiv statt eines Kopierens auf Dateiebene, damit Rechte und Symlinks erhalten bleiben.
Phase D · Start und Überwachung
Schritt 4 · Quadlet schreiben und den Reboot testen
Der Test ist nicht „der Container läuft", sondern „der Container läuft nach einem Neustart des Hosts wieder". Ohne diesen Test bleibt der häufigste Umstiegsfehler unentdeckt.
systemctl --user daemon-reload
systemctl --user enable --now web
sudo reboot
# nach dem Neustart
systemctl --user status web
podman ps
Schritt 5 · Überwachung anpassen
Monitoring, das den Docker-Socket abfragt, sieht unter Podman nichts. Entweder den kompatiblen Socket bereitstellen oder auf systemd-Zustände umstellen – letzteres ist im rootless-Betrieb der geradere Weg.
systemctl --user list-units 'podman*' --no-pager
podman healthcheck run <container>
Wann welches Werkzeug
| Lage | Empfehlung |
|---|---|
| Einzelne Dienste auf einem Server | Podman rootless mit Quadlet |
| RHEL, Fedora, OpenShift-Umfeld | Podman – gleiche Bibliotheken wie CRI-O, im System enthalten |
| Team arbeitet mit Compose und Desktop-Werkzeugen | Docker, oder Podman mit aktiviertem Socket |
| Swarm im Einsatz | Docker – Podman hat keine Entsprechung |
| CI-Runner ohne privilegierten Socket | Podman rootless oder Buildah |
| Lernen für Kubernetes | Podman – Pods und YAML-Erzeugung entsprechen den Cluster-Begriffen |