Zum Inhalt

Podman Quadlets & Systemd: Deklarative Container-Orchestrierung

Erstellt: 10.11.2026 Aktualisiert: 10.11.2026

Management Summary: Klassische Container-Bereitstellungen stützen sich häufig auf Shell-Skripte oder docker-compose-Dateien, die außerhalb des Linux-Betriebssystem-Lifecycles operieren. Dies führt bei Server-Neustarts, Prozessabstürzen und Update-Zyklen regelmäßig zu unkontrollierten Zuständen und fehlenden Abhängigkeitsketten. Podman Quadlets revolutionieren die Container-Orchestrierung auf Einzel-Hosts: Anstelle fehleranfälliger Skripte übersetzt ein nativer systemd-Generator deklarative .container-, .volume-, .network- und .pod-Dateien beim Systemstart oder Daemon-Reload dynamisch in vollwertige systemd-Services. Dies ermöglicht automatisierte Abhängigkeitsauflösung, systemweites Ressourcen-Supervising, standardisiertes Journald-Logging und Zero-Touch-Auto-Updates mit automatischem Health-Check-Rollback. Dieser Leitfaden behandelt die Generator-Architektur, Rootless-Setups, Multi-Tier-Stacks als docker-compose-Ersatz, Auto-Updates und operative Runbooks.

  • Die Quadlet-Architektur


    Wie der systemd-Generator deklarative .container-Dateien parst und zur Bootzeit in transiente Units kompiliert.

  • Dateitypen & Speicherorte


    Unterschied zwischen Root-Units (/etc/containers/systemd/) und unprivilegierten Rootless-Units (~/.config/containers/systemd/).

  • Multi-Container Stacks


    Komplexe Stacks deklarativ mit .network- und .volume-Units verbinden – der moderne Ersatz für docker-compose.yml.

  • Podman Auto-Update & Rollback


    Zero-Touch-Updates über Registry-Labels, systemd-Timer und automatischer Rollback bei fehlgeschlagenen Healthchecks.

  • Secrets & Berechtigungen


    Sichere Injektion sensibler Daten via Podman Secrets, User Namespaces (subuid) und SELinux-Mount-Flags (:Z,U).

  • Betrieb & Troubleshooting


    Generator-Dry-Run-Diagnostik, Inspektion erzeugter Units in /run/ und Fehlersuche bei fehlschlagenden Diensten.


1. Das Architektur-Paradigma: Warum Compose im OS-Betrieb versagt

In der traditionellen Docker- und Podman-Welt galt docker-compose (oder podman-compose) lange Zeit als Standard zur Definition von Multi-Container-Anwendungen. In produktiven Serverumgebungen weist dieses Modell jedoch fundamentale architektonische Mängel auf:

  1. Kein natives Betriebssystem-Supervising: Compose-Dateien sind reine Client-Werkzeuge. Wenn das System neu startet, muss ein separater Wrapper oder ein intransparentes Restart-Flag (restart: always) greifen. Stürzt der Container-Daemon oder der Daemon-Prozess ab, bleiben Container oft verwaist zurück.
  2. Fehlende Integration in Linux-Standards: cgroups v2-Ressourcenlimits, Audit-Protokolle, Abhängigkeiten zu Bare-Metal-Diensten (z. B. erst starten, wenn das Dateisystem /mnt/data gemountet oder das Netzwerk online ist) lassen sich in Compose nicht nativ mit dem Host abstimmen.
  3. Die Sackgasse podman generate systemd: Ältere Podman-Versionen boten den Befehl podman generate systemd. Dieser generierte jedoch statische, hunderte Zeilen lange Service-Dateien, in denen CLI-Parameter und Container-IDs fest verdrahtet waren. Jedes Update des Containers erforderte das manuelle Löschen und Neuerzeugen der Unit-Datei. Upstream hat diese Methode offiziell als deprecated eingestuft.

Die Lösung: Der native Quadlet-Generator

Mit Quadlet (eingeführt in Podman 4.4 und seither kontinuierlich ausgebaut) wurde das Problem an der Wurzel gelöst. Quadlet ist kein neuer Hintergrunddienst, sondern ein systemd-Generator (podman-system-generator), der direkt in den Boot- und Reload-Zyklus von systemd eingeklinkt ist:

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    Quadlet_File["Deklarative Quadlet-Datei<br/>(~/.config/containers/systemd/web.container)"] ==> Systemd_Reload["systemctl --user daemon-reload<br/>(oder System-Boot)"]
    Systemd_Reload --> Generator["podman-system-generator<br/>(Parst OCI-Spezifikation)"]
    Generator --> Transiente_Unit["Transiente systemd-Unit<br/>(/run/user/1000/systemd/generator/web.service)"]
    Transiente_Unit --> Systemd_Engine["systemd Service-Manager<br/>(Abhängigkeiten, cgroups, Journal)"]
    Systemd_Engine ==> Conmon["conmon Runtime-Monitor<br/>(Unprivilegierter Child-Prozess)"]
    Conmon --> Container["OCI Rootless-Container<br/>(Isolierter App-Prozess)"]

Der entscheidende Vorteil: Du schreibst wenige, hochgradig lesbare Zeilen Konfiguration. Systemd erzeugt daraus on-the-fly den perfekten Produktions-Service mit allen notwendigen Start-, Stopp-, Healthcheck- und Überwachungsmechanismen.


2. Dateitypen, Speicherorte & Rootless-Voraussetzungen

Quadlet unterstützt unterschiedliche Dateitypen, die jeweils einen bestimmten Aspekt der Container-Infrastruktur abbilden.

Die 6 Quadlet-Dateitypen

Dateiendung Zweck Generierte systemd-Unit
.container Definiert einen einzelnen OCI-Container <name>.service
.volume Erstellt und verwaltet ein persistentes Podman-Named-Volume <name>-volume.service
.network Definiert ein isoliertes Bridge-Netzwerk <name>-network.service
.pod Gruppiert mehrere Container in einem gemeinsamen Netzwerk-/IPC-Namespace <name>-pod.service
.kube Führt Kubernetes-kompatible Pod-YAML-Manifeste via podman kube play aus <name>.service
.image Verwaltet das automatische Vorab-Herunterladen von OCI-Images <name>-image.service

Speicherorte: Systemweit vs. Rootless

Quadlets können auf zwei Ebenen betrieben werden:

  1. Systemweit (Root):
  2. Pfad: /etc/containers/systemd/
  3. Ausführung als root im Kontext des System-Managers (systemctl).
  4. Geeignet für Infrastruktur-Dienste, die privilegierte Host-Ressourcen benötigen.
  5. Rootless (Unprivilegiert – Empfohlener Enterprise-Standard):
  6. Pfad: ~/.config/containers/systemd/ (alternativ /etc/containers/systemd/users/ für flottenweite User-Profile).
  7. Ausführung vollständig im Userspace des jeweiligen Dienst-Accounts via systemctl --user.
  8. Sicherheitsgewinn: Selbst bei einem vollständigen Ausbruch aus dem Container besitzt der Prozess keinerlei Root-Rechte auf dem Host-System!

Unabdingbare Voraussetzung: User-Lingering aktivieren

Standardmäßig beendet Linux alle Hintergrundprozesse eines unprivilegierten Benutzers, sobald sich dieser per SSH abmeldet. Für produktive Server-Dienste muss das sogenannte Lingering für den entsprechenden Dienst-Account aktiviert werden:

# Aktiviere persistente systemd-Instanz für den Service-Benutzer (z. B. "deploy")
sudo loginctl enable-linger deploy

# Status überprüfen (muss "Linger=yes" ausgeben)
loginctl show-user deploy --property=Linger

3. Anatomie einer .container-Datei im Detail

Eine Quadlet-Datei kombiniert vertraute systemd-Sektionen ([Unit], [Service], [Install]) mit der Quadlet-spezifischen Sektion [Container].

Schlüsselfertiges Beispiel: Gehärteter Nginx-Microservice

Erstelle das Verzeichnis und die Datei ~/.config/containers/systemd/webserver.container:

# /home/deploy/.config/containers/systemd/webserver.container
# ==============================================================================
# Enterprise Rootless Nginx Container Unit via Podman Quadlet
# ==============================================================================

[Unit]
Description=Produktions-Webserver (Rootless OCI Nginx)
Documentation=https://nejat-hakan.de/Linux/linux_containers__systemd_quadlets/
After=network-online.target
Wants=network-online.target

[Container]
# OCI-Image-Quelle
Image=docker.io/library/nginx:alpine

# Eindeutiger Container-Name für Logging und CLI-Inspektion
ContainerName=production-webserver

# Port-Mapping: Host-Port 8080 auf unprivilegierten Container-Port 80
PublishPort=127.0.0.1:8080:80

# Persistente Volumes: :Z setzt den SELinux-Label, :U korrigiert Datei-Besitzrechte
Volume=%h/web_content:/usr/share/nginx/html:ro,Z
Volume=%h/web_logs:/var/log/nginx:Z,U

# Umgebungsvariablen
Environment=TZ=Europe/Berlin

# Automatische Updates aktivieren (prüft Registry regelmäßig)
AutoUpdate=registry

# Härtungsoptionen: Keine neuen Privilegien, Lese-Zugriff auf Root-Dateisystem
SecurityLabelDisable=false
NoNewPrivileges=true
ReadOnly=true
DropCapability=ALL
AddCapability=CHOWN SETGID SETUID NET_BIND_SERVICE

[Service]
# Automatische Wiederherstellung bei Absturz
Restart=always
TimeoutStartSec=300

[Install]
# Automatischer Start beim Booten der User-Session
WantedBy=default.target

Die wichtigsten [Container]-Direktiven im Überblick

  • Image=: Das vollständige Image samt Registry-Angabe (vermeide unqualifizierte Namen wie nginx, nutze immer docker.io/library/nginx:alpine oder quay.io/...).
  • PublishPort=: Syntax [Host-IP:]Host-Port:Container-Port. Bei Rootless-Containern sollten Ports immer an 127.0.0.1 gebunden werden, um den Datenverkehr über einen Host-Reverse-Proxy (Nginx, Caddy) zu terminieren.
  • Volume=: Unterstützt Platzhalter wie %h (Home-Verzeichnis des aktuellen Benutzers). Die Flags :Z (SELinux-Container-Kontext) und :U (automatisches Chownen auf die Namespace-SubUID) sind für den fehlerfreien Betrieb elementar.
  • AutoUpdate=registry: Signalisiert dem Auto-Update-Timer, dieses Image regelmäßig gegen die Remote-Registry zu prüfen.
  • NoNewPrivileges=true & DropCapability=ALL: Härtet den Container auf Kernel-Ebene gegen Privilege Escalation ab.

4. Multi-Container Stacks & Netzwerke: Vollwertiger docker-compose-Ersatz

In der Praxis bestehen Web-Anwendungen selten aus einem einzelnen Container. Ein typischer Stack benötigt eine Web-Applikation, eine relationale Datenbank und ein isoliertes internes Netzwerk.

Quadlet realisiert dies durch das Zusammenspiel von .network-, .volume- und mehreren .container-Dateien.

1. Das isolierte Netzwerk: database.network

Erstelle ~/.config/containers/systemd/app-net.network:

# /home/deploy/.config/containers/systemd/app-net.network
# ==============================================================================
# Isoliertes Bridge-Netzwerk für Anwendungs-Stack
# ==============================================================================

[Network]
# Interner DNS-Name des Netzwerks
NetworkName=production-app-net

# Subnetz definieren (verhindert Überschneidungen mit Firmen-VPNs)
Subnet=10.89.10.0/24
Gateway=10.89.10.1

# Internes Netzwerk: Verhindert direkten ausgehenden Internetzugriff der Datenbank
Internal=false

2. Das persistente Datenbank-Volume: db-data.volume

Erstelle ~/.config/containers/systemd/db-data.volume:

# /home/deploy/.config/containers/systemd/db-data.volume
# ==============================================================================
# Dediziertes persistentes Volume für PostgreSQL
# ==============================================================================

[Volume]
VolumeName=production-postgres-data

3. Der Datenbank-Container: postgres.container

Erstelle ~/.config/containers/systemd/postgres.container:

# /home/deploy/.config/containers/systemd/postgres.container
# ==============================================================================
# PostgreSQL 16 Datenbank-Unit
# ==============================================================================

[Unit]
Description=PostgreSQL Datenbank-Dienst
After=app-net-network.service
Requires=app-net-network.service

[Container]
Image=docker.io/library/postgres:16-alpine
ContainerName=app-database

# Verknüpfung mit dem deklarativen Quadlet-Netzwerk
Network=app-net.network

# Verknüpfung mit dem deklarativen Quadlet-Volume
Volume=db-data.volume:/var/lib/postgresql/data:Z

# Sichere Umgebungskonfiguration
Environment=POSTGRES_DB=app_db
Environment=POSTGRES_USER=app_user
EnvironmentFile=%h/.config/containers/postgres.env

# Kein Port-Mapping nötig: Zugriff erfolgt rein intern über DNS "app-database"

[Service]
Restart=always
TimeoutStartSec=180

[Install]
WantedBy=default.target

4. Der Web-Anwendungs-Container: app.container

Erstelle ~/.config/containers/systemd/app.container:

# /home/deploy/.config/containers/systemd/app.container
# ==============================================================================
# Web-Applikation mit nativer Abhängigkeit zur Datenbank
# ==============================================================================

[Unit]
Description=Hauptanwendung (Node.js / Python Backend)
# Garantiert, dass die Datenbank vor der App startet
After=postgres.service
Requires=postgres.service

[Container]
Image=quay.io/mein-unternehmen/web-app:latest
ContainerName=app-backend

# Befindet sich im selben Netzwerk wie die Datenbank
Network=app-net.network

# Verbindung zur DB über internen Hostnamen "app-database:5432"
Environment=DB_HOST=app-database
Environment=DB_PORT=5432
Environment=DB_NAME=app_db

# Exponiere App nur lokal für den Host-Proxy
PublishPort=127.0.0.1:3000:3000

AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target

Wie systemd die Abhängigkeiten steuert

Im Gegensatz zu docker-compose überwacht systemd den realen Prozesszustand: * Requires=postgres.service: Stoppt oder stürzt postgres.service ab, wird app.service kontrolliert mitgeführt bzw. neu gestartet. * After=postgres.service: Gewährleistet die exakte Einhaltung der Start-Reihenfolge beim Booten.


5. Zero-Touch Auto-Updates & Health-Check-Rollback

Eines der mächtigsten Enterprise-Features von Podman Quadlet ist das native Auto-Update-System.

Wie Podman Auto-Updates funktionieren

  1. In der .container-Datei wird AutoUpdate=registry hinterlegt.
  2. Beim Start prüft Podman das Image-Label io.containers.autoupdate=registry oder die Direktive in der Quadlet-Datei.
  3. Der vom System mitgelieferte systemd-Timer podman-auto-update.timer wird aktiviert.
  4. Einmal täglich (standardmäßig um Mitternacht) kontaktiert der Timer die Registry, vergleicht den SHA256-Digest des lokalen Images mit dem Remote-Tag und lädt bei Diskrepanzen das neue Image herunter.
  5. Der Dienst wird neu gestartet.
%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    Timer["podman-auto-update.timer<br/>(Täglicher systemd-Trigger)"] ==> AutoUpdate_Service["podman-auto-update.service<br/>(Prüft Registry auf neue Digests)"]
    AutoUpdate_Service --> Pull{"Neues Image verfügbar?"}
    Pull -->|"Nein"| Done["Keine Aktion erforderlich"]
    Pull -->|"Ja"| Download["Pull neues OCI-Image<br/>(podman pull)"]
    Download --> Restart["systemctl restart <unit><br/>(Neuer Container startet)"]
    Restart --> Healthcheck{"Healthcheck erfolgreich?<br/>(HealthCmd ausgeführt)"}
    Healthcheck -->|"Erfolgreich"| Stable["Update abgeschlossen<br/>(Neuer Stand aktiv)"]
    Healthcheck -->|"Fehlschlag"| Rollback["Automatischer Rollback!<br/>(Vorheriges Image wiederhergestellt)"]

Timer aktivieren

# Aktiviere den automatischen Update-Timer für den aktuellen Benutzer
systemctl --user enable --now podman-auto-update.timer

# Nächste Ausführungszeiten überprüfen
systemctl --user list-timers podman-auto-update.timer

Healthchecks für sichere Rollbacks definieren

Ohne Healthcheck weiß Podman nicht, ob der neue Container fehlerfrei funktioniert. Schlägt der Start fehl oder hängt der Webserver, verhindert ein Healthcheck Produktionsausfälle:

[Container]
Image=docker.io/library/nginx:alpine
AutoUpdate=registry

# Healthcheck: Prüfe alle 30s, ob Nginx auf HTTP 200 antwortet
HealthCmd=curl -f http://localhost:80/ || exit 1
HealthInterval=30s
HealthRetries=3
HealthTimeout=5s
HealthStartupInterval=5s

Wenn das neue Image nach dem Update dreimal in Folge den Healthcheck verfehlt, bricht Podman ab und rollt die Service-Unit automatisch auf die vorherige funktionierende Image-Version zurück!


6. Enterprise-Betrieb, Troubleshooting & Best Practices

1. Quadlet-Dateien aktivieren und kompilieren

Jede Änderung an einer .container-, .network- oder .volume-Datei erfordert ein Neuladen von systemd:

# 1. systemd veranlassen, die Generatoren neu auszuführen
systemctl --user daemon-reload

# 2. Generierten Service starten und für Boot aktivieren
systemctl --user enable --now webserver.service

# 3. Status kontrollieren
systemctl --user status webserver.service

2. Der Generator-Dry-Run: Fehleranalyse vor dem Start

Wenn eine Unit nach einem daemon-reload nicht auftaucht, liegt meist ein Syntaxfehler in der Quadlet-Datei vor. Der Generator bietet einen nützlichen Dry-Run-Modus:

# Führe den Generator manuell im Terminal aus, um Parsing-Fehler zu sehen
/usr/lib/systemd/system-generators/podman-system-generator --dryrun --user

Gibt der Generator Warnungen wie Key 'Port' is unknown aus, muss die Direktive korrigiert werden (z. B. PublishPort statt Port).

3. Inspektion der tatsächlich generierten Unit

Um zu verstehen, welche nativen systemd-Parameter Quadlet im Hintergrund generiert hat:

# Zeige die vom Generator dynamisch erzeugte Service-Datei an
cat /run/user/$(id -u)/systemd/generator/webserver.service

Hier wird sichtbar, wie Quadlet Parameter wie ExecStart=/usr/bin/podman run ..., ExecStop=/usr/bin/podman stop ... und SyslogIdentifier=webserver vollautomatisch injiziert hat.

4. Revisionssicheres Logging mit journalctl

Da jeder Quadlet-Container ein nativer systemd-Service ist, landen alle Container-Logs (stdout und stderr) direkt im systemd-Journal:

# Live-Logs des Containers mit Zeitstempeln verfolgen
journalctl --user -u webserver.service -f

# Logs seit dem letzten Boot-Vorgang anzeigen
journalctl --user -u webserver.service -b

# Logs mit Fehlerschweregrad "Error" oder höher filtern
journalctl --user -u webserver.service -p err

5. Troubleshooting-Matrix

Problem Ursache Lösung
Service stoppt bei SSH-Logout User-Lingering ist nicht aktiv sudo loginctl enable-linger $USER ausführen.
Permission denied auf Volume-Dateien Fehlendes SELinux-Label oder falsche SubUID-Zuordnung In der .container-Datei beim Volume :Z,U anhängen: Volume=/host/dir:/cont/dir:Z,U.
Service startet nicht (Unit not found) Syntaxfehler in der .container-Datei oder fehlendes daemon-reload 1. systemctl --user daemon-reload
2. Generator-Dryrun ausführen und Output prüfen.
Container kann Port < 1024 nicht binden Kernel-Sicherheitsgrenze für unprivilegierte Benutzer Entweder High-Port (> 1024) mappen oder Kernel-Parameter setzen: sudo sysctl net.ipv4.ip_unprivileged_port_start=80.
Auto-Update schlägt fehl Fehlendes Image-Tag oder Registry-Authentifizierung fehlt podman login <registry> für den Dienstbenutzer ausführen und sicherstellen, dass nicht das Tag :latest auf immutable Digests gepinnt ist.

Mit Podman Quadlets verschmilzt die Containerisierung nahtlos mit den Kernstärken von Enterprise Linux: Stabilität, deterministische Startreihenfolgen, feingranulare Ressourcenkontrolle und autonome Wiederherstellung ohne fremde Daemon-Schichten.