Veröffentlicht in

podman – pod automatisieren: service, autostart und update

Eine „upgrade“-Funktion wie bei docker bietet podman wie schon beschrieben nicht. Ein pod-update erledigen wir daher weiter unten erst einmal von Hand – mit drei kleinen Kommandos. Das funktioniert wunderbar, will aber jede Woche aufs Neue getippt werden. Zeit also, das zu ändern: in diesem Howto wollen wir einen podman pod automatisieren, ihn als Service beim Booten starten lassen und die Updates gleich mit der Automatik erledigen.

Und weil sich hier ganz gut zeigt, wo sich die beiden Container-Welten unterscheiden, streuen wir an den passenden Stellen immer wieder einen kurzen Blick Richtung docker ein. So viel vorweg: docker verlässt sich auf seinen Daemon im Hintergrund und dessen restart-policies, und für automatische Updates greift man dort meist zu Drittwerkzeugen wie Watchtower. podman geht einen anderen Weg – es kommt ganz ohne Daemon aus und überlässt den kompletten Lebenszyklus dem, was auf einem Linux-System ohnehin schon läuft: systemd. Schauen wir uns das mal an.

Der Ausgangspunkt: das manuelle update

Bevor wir automatisieren, noch einmal kurz der Weg von Hand – der bleibt schließlich die Grundlage. Als erstes sichern wir die aktuelle Konfiguration vom pod, falls noch nicht geschehen.

# podman kube generate mypod > mypod.yaml

docker-Nutzer kennen so eine Beschreibung als docker-compose.yml, die man ohnehin im Projekt liegen hat. Bei podman erzeugen wir sie uns mit kube generate bei Bedarf direkt aus dem laufenden pod – praktisch, wenn der pod damals eher „zu Fuß“ gestartet wurde.

Danach ziehen wir die neuesten Images der container im pod.

# podman pull registry.bla/image1:latest
# podman pull registry.bla/image2:latest

Anschließend den aktuell noch laufenden pod ersetzen.

# podman kube play --replace mypod.yaml

Das war es im Grunde schon. Wo docker mit docker compose pull und docker compose up -d arbeitet, erledigen wir dasselbe mit einem pull und einem kube play --replace. Drei Kommandos, kein Hexenwerk – aber eben Handarbeit, und genau die wollen wir gleich loswerden.

Kleinere Probleme

Bei mir kam im ersten Schritt die folgende Fehlermeldung:

Error: container ....... is associated with pod ....... : use generate on the pod itself

Die Ursache war recht schnell ermittelt: mein pod hatte den gleichen namen wie ein Container im pod. Den Fehler kann man ganz leicht umgehen, indem man die Pod-ID verwendet.

# podman pod ps

POD ID        NAME       STATUS ....
a8d723ab1     myname     Running ....
8da7bdsb3     othername  Running ....

# podman kube generate a8d723ab1 > myname.yaml

Okay, habe ich mir gedacht, ich kann ja auch einfach den pod in der yaml umbenennen.

apiVersion: v1
kind: Pod
metadata:
  annotations:
    ...
  labels:
    app: myname        # ← nur ein Label, NICHT der Pod-Name
  name: myname         # ← DAS ist der Pod-Name  ← hier umbenennen
spec:
  containers:
  - name: myname       # ← Container-Spec-Name 
    image: ...

Ich hab also den Namen einfach umbenannt in myname_pod und dann wie oben beschrieben ein replace gemacht.

podman kube play --replace myname.yaml
Pods stopped:
Pods removed:
...
starting container ...... : a dependency of container .... failed to start: container state improper
starting container ...... : cannot listen on the TCP port: .... address already in use

Was hab ich übersehen? Genau, die yaml hat einen neuen Containernamen, den findet replace natürlich nicht da der pod noch unter altem Namen läuft. Da ich einen laufenden pod nicht einfach umbenennen kann, wird er gelöscht und anschließend mit der yaml neu erstellt.

podman pod stop myname
podman pod rm myname
podman kube play myname.yaml 

Hier ist ein replace nicht notwendig, da wir den Pod vorher manuell gelöscht haben.

podman pod automatisieren mit systemd und Quadlet

Kommen wir zum spannenden Teil. Ein Container soll ja nicht nur laufen, wenn wir zufällig eingeloggt sind – er soll beim Booten starten, nach einem Absturz neu anlaufen und sich ordentlich verwalten lassen. Bei docker übernimmt das der Daemon: dockerd läuft dauerhaft im Hintergrund, und mit einer restart-policy wie --restart=always sorgt er dafür, dass der Container nach einem Reboot wieder hochkommt.

podman hat so einen Daemon bewusst nicht. Ein podman-Container ist einfach ein Kindprozess – und um Prozesse zu starten, zu überwachen und bei Bedarf neu zu starten, gibt es auf jedem modernen Linux längst ein Werkzeug: systemd. Genau da setzen wir an. Statt einen zweiten Verwalter mitlaufen zu lassen, machen wir aus unserem pod einen ganz normalen systemd-Service.

Und genau das ist der eigentliche Grundgedanke hinter podman: rootless. Bei docker läuft der Daemon klassisch als root – jeder Container wird von einem privilegierten Prozess im Hintergrund gestartet, und wer den docker-Socket erreicht, hat praktisch root auf dem Host. podman dreht das um: es gibt keinen zentralen Dienst, die Container laufen als ganz normale Prozesse unter deinem eigenen Benutzer. Ein Ausbruch aus dem Container landet dann nicht bei root, sondern nur bei den Rechten, die dein User ohnehin schon hat. Genau deshalb bleiben wir in diesem Howto durchgehend rootless – das ist bei podman nicht die Ausnahme, sondern der Normalfall.

Das Bindeglied dafür heißt Quadlet. Quadlet ist seit podman 4.4 fester Bestandteil und zB in Debian 13 (Trixie) mit podman 5.x dabei. Die Idee dahinter ist angenehm simpel: wir beschreiben deklarativ, was laufen soll, und ein systemd-Generator baut daraus im Hintergrund die passende Service-Unit. Das früher übliche podman generate systemd hat damit ausgedient – es gilt inzwischen als veraltet.

Die .kube-Unit anlegen

Quadlet kennt verschiedene Unit-Typen – .container für einen einzelnen Container, .network, .volume und für unseren Fall das passende .kube, das eine komplette Kubernetes-yaml übernimmt. Da wir unseren pod ohnehin schon per kube play aus einer yaml starten, ist das die naheliegende Wahl. Rootless landet die Datei unter ~/.config/containers/systemd/, systemweit als root unter /etc/containers/systemd/.

# ~/.config/containers/systemd/mypod.kube
[Unit]
Description=mein pod als service
After=network-online.target

[Kube]
Yaml=/home/user/pods/myname.yaml

[Install]
WantedBy=default.target

Viel ist das nicht, und das ist auch der Punkt. Im Abschnitt [Kube] verweisen wir mit Yaml= auf unsere pod-Beschreibung – den Pfad geben wir dabei absolut an, sonst findet der Generator die Datei später nicht. Der [Unit]-Teil ist klassisches systemd (hier warten wir mit After= brav aufs Netzwerk), und [Install] regelt, wann der Service starten soll. Wer mehr braucht, findet im [Kube]-Block noch Schlüssel wie Network=, PublishPort= oder ConfigMap= – für den Anfang kommen wir aber gut ohne aus.

Service starten und beim Booten aktivieren

Damit systemd unsere neue Datei überhaupt bemerkt, lesen wir die Units einmal neu ein. Der Generator baut daraus dann die eigentliche Service-Unit – und deren Name ergibt sich schlicht aus dem Dateinamen ohne Endung. Aus mypod.kube wird also mypod.service.

systemctl --user daemon-reload
systemctl --user start mypod.service

Hier lauert eine kleine Stolperfalle, die einen anfangs gern verwirrt: ein klassisches systemctl enable mypod.service läuft ins Leere. Die Unit wird ja erst zur Laufzeit vom Generator erzeugt und existiert nicht als feste Datei, die man aktivieren könnte. Genau dafür ist die Zeile WantedBy=default.target im [Install]-Block da – sie übernimmt das „enable“ quasi automatisch. Einmal daemon-reload, und der Service ist beim nächsten Boot mit dabei.

Bleibt noch ein Detail bei rootless-Betrieb. Ein User-Service läuft normalerweise nur, solange der Benutzer angemeldet ist – beim Server ohne Login wäre unser pod also nach dem Boot erstmal aus. Die Lösung heißt lingering: damit startet systemd unsere User-Services schon beim Hochfahren, ganz ohne Anmeldung.

loginctl enable-linger $USER

Bei docker spart man sich diesen Schritt – dort erledigt der immer laufende Daemon den Autostart. Der Preis dafür ist eben jener Daemon mit root-Rechten, der permanent im Hintergrund wacht. Bei podman haben wir ein bisschen mehr zu tippen, dafür läuft am Ende alles unprivilegiert unter unserem eigenen Benutzer.

Und was ist, wenn man den pod doch als root betreibt? Dann kann man sich das lingering komplett sparen. root arbeitet nämlich mit der System-Instanz von systemd (Unit unter /etc/containers/systemd/, Bedienung ohne --user, Autostart klassisch über WantedBy=multi-user.target) – und die läuft ohnehin ab dem Boot, ganz ohne Anmeldung. Der lingering-Schritt gilt also ausschließlich dem rootless-Betrieb. Wir bleiben hier aber bewusst beim rootless-Weg, denn das ist bei podman wie gesagt der eigentliche Sinn der Sache.

Den Service im Griff behalten

Und das ist der eigentliche Gewinn der ganzen Aktion: unser pod ist jetzt ein vollwertiger systemd-Service. Wir bedienen ihn mit denselben Kommandos wie jeden anderen Dienst auch – kein podman-Spezialwissen nötig.

systemctl --user status mypod.service
systemctl --user restart mypod.service
systemctl --user stop mypod.service

Auch die Logs landen dort, wo man sie auf einem systemd-System erwartet – im Journal. Ein Blick genügt, und wir sehen, was der pod so treibt:

journalctl --user -u mypod.service -f

Updates automatisch ziehen

Der pod läuft jetzt als Service – aber aktuell hält er sich damit noch nicht von selbst. Im docker-Umfeld greift man an dieser Stelle gern zu Watchtower: ein zusätzlicher Container, der die laufenden Container beobachtet und bei neuen Images austauscht. podman braucht dafür kein Drittwerkzeug, sondern bringt die Funktion mit – podman auto-update. Sie macht genau das, was wir oben von Hand erledigt haben: neues Image prüfen, ziehen und den Service neu starten.

Wir müssen podman nur verraten, welche Container es im Auge behalten soll. Bei einem pod aus einer yaml machen wir das am saubersten über eine Annotation direkt in der Datei.

apiVersion: v1
kind: Pod
metadata:
  annotations:
    io.containers.autoupdate/container1: registry
    io.containers.autoupdate/container2: registry
  name: myname_pod
spec:
  containers:
  - name: container1
    image: registry.bla/image1:latest
  - name: container2
    image: registry.bla/image2:latest

Der Teil hinter dem Schrägstrich ist übrigens der Container-Name aus der spec – der muss also passen. Wer die Policy lieber für alle Container auf einmal setzen möchte, nutzt einfach io.containers.autoupdate: registry ohne den Namen dahinter. Der Wert registry sorgt dafür, dass podman in der Registry nachschaut, ob es ein neueres Image gibt. Wichtig dabei: das Image muss vollständig angegeben sein, also inklusive Registry und Tag (z.B. registry.bla/image1:latest) – sonst weiß podman schlicht nicht, wo es nachsehen soll. Das ist strenger als docker es beim beliebten :latest gern nimmt, erspart uns dafür aber böse Überraschungen.

Damit die Prüfung auch regelmäßig läuft, aktivieren wir noch den passenden Timer. Den bringt podman bereits mit – standardmäßig läuft er einmal täglich um Mitternacht. Wichtig ist nur, dass der Timer auf derselben Ebene läuft wie unser Service, in unserem rootless-Fall also als User.

systemctl --user enable --now podman-auto-update.timer

Bevor wir das Ganze der Automatik überlassen, testen wir am besten einmal mit einem dry-run, ob podman auch das findet, was wir erwarten. Der Lauf zieht noch nichts, sondern zeigt nur an, was passieren würde:

podman auto-update --dry-run

Sieht alles gut aus, können wir das Update natürlich auch jederzeit von Hand anstoßen – praktisch, wenn man nicht bis Mitternacht warten möchte:

podman auto-update

Ob und was dabei aktualisiert wurde, verrät uns hinterher wieder das Journal – diesmal für den auto-update-Service:

journalctl --user -u podman-auto-update.service -n 50

Wenn ein update schiefgeht: Rollback

Automatische Updates sind bequem – aber was, wenn das neue Image kaputt ist und der Container gar nicht mehr sauber startet? Auch hier hat podman vorgesorgt: auto-update rollt im Fehlerfall standardmäßig auf das vorherige Image zurück und startet den Service noch einmal mit der alten Version. Dieses Verhalten ist per Default aktiv (steuerbar über --rollback).

Ein wichtiges Detail steckt aber im Kleingedruckten: podman muss merken, dass der Start fehlgeschlagen ist. Ein Container, der zwar startet, aber Sekunden später intern abstürzt, gilt für systemd erstmal als „läuft“. Damit der Rollback verlässlich greift, sollte der Container also selbst melden, ob er gesund ist – am einfachsten über einen Health-Check in der pod-Beschreibung:

spec:
  containers:
  - name: container1
    image: registry.bla/image1:latest
    livenessProbe:
      exec:
        command:
        - curl
        - -f
        - http://localhost:8080/
      initialDelaySeconds: 15
      periodSeconds: 20

Erst mit so einem Signal weiß podman zuverlässig, ob das Update wirklich geglückt ist – und kann im Zweifel zurückrudern. Und genau hier zeigt sich noch einmal ein Unterschied zu docker: ein automatischer Rollback ist dort nicht eingebaut. Werkzeuge wie Watchtower tauschen das Image aus und hoffen auf das Beste – läuft der neue Container nicht, steht man erstmal ohne Netz da. Ein sicherheitshalber aufgehobenes altes Image und die gesicherte yaml schaden bei podman trotzdem nicht – man weiß ja nie.

Fazit: von Hand, als Service, vollautomatisch

Damit haben wir den Bogen komplett gespannt. Angefangen bei den drei Kommandos für das manuelle Update, über den pod als sauberen systemd-Service, bis hin zur Automatik, die sich selbst aktuell hält und im Fehlerfall sogar zurückrollt. Aus einer Handvoll Handgriffe ist ein Dienst geworden, um den wir uns im Alltag kaum noch kümmern müssen.

Der rote Faden dabei war der Unterschied in der Philosophie: docker bündelt vieles in seinem Daemon – Autostart, restart-policies, und mit Zusatzwerkzeugen auch Updates. podman verteilt dieselben Aufgaben auf Bordmittel, die auf jedem Linux ohnehin schon da sind. Das bedeutet anfangs etwas mehr zu lernen – Quadlet, systemd-Units, lingering – dafür bewegen wir uns durchgängig in vertrautem, unprivilegiertem Standard-Werkzeug statt in einer eigenen Welt.

Und wenn die Automatik dann mitten in der Nacht klaglos das Update zieht, während wir seelenruhig schlafen – dann war der kleine Mehraufwand am Anfang wohl doch keine schlechte Investition.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Datenschutz
Ich, Frank Lüdke, c/o IP-Management #7563 (Wohnort: Deutschland), verarbeite zum Betrieb dieser Website personenbezogene Daten nur im technisch unbedingt notwendigen Umfang. Alle Details dazu in meiner Datenschutzerklärung.
Datenschutz
Ich, Frank Lüdke, c/o IP-Management #7563 (Wohnort: Deutschland), verarbeite zum Betrieb dieser Website personenbezogene Daten nur im technisch unbedingt notwendigen Umfang. Alle Details dazu in meiner Datenschutzerklärung.