Dies ist eine alte Version des Dokuments!
Container verstehen und betreiben
Von Docker über Podman bis OpenShift — ein Reiseführer
Ein Tutorial für alle, die Container bisher nur vom Hörensagen kennen und wissen wollen, warum plötzlich alle davon reden. Von den Grundlagen über den Werkzeugvergleich bis zum fertig eingerichteten Podman-Server unter RHEL.
Wenn man in „Per Anhalter durch die Galaxis„ eines lernen kann, dann dieses: Ein guter Reiseführer ist nicht der, der alles erklärt, sondern der, der einem die Panik nimmt. Genau das ist der Anspruch dieses Dokuments. Container wirken von außen wie ein Haufen Buzzwords, hinter denen sich vermutlich Magie oder zumindest ein Vogonisches Bauamt verbirgt. Tatsächlich stecken dahinter ein paar erstaunlich handfeste Linux-Funktionen, die man an einem Nachmittag verstehen kann.
Die farbig hinterlegten Kästen benötigen das DokuWiki-Plugin wrap. Fehlt es, erscheint der Text unformatiert, bleibt aber lesbar.
Zeitbedarf: Kapitel 1–5 an einem Tag, der Rest je nach Vertiefung. Das HowTo in Kapitel 14 lässt sich in etwa zwei Stunden abarbeiten.
1. Einleitung
1.1 Was ist ein Container — und was nicht
Ein Container ist ein ganz normaler Prozess auf einem ganz normalen Linux-System, dem man vorgaukelt, er sei allein auf der Welt.
Das ist keine Vereinfachung für Einsteiger, das ist die ganze Wahrheit. Wenn du auf dem Host ps aux eingibst, siehst du die Prozesse deiner Container einfach in der Liste stehen. Sie laufen auf demselben Kernel, sie verwenden dieselbe CPU, sie werden vom selben Scheduler verwaltet. Der Unterschied ist nur: Der Prozess selbst sieht das nicht. Er hat seine eigene Sicht auf das Dateisystem, auf die Prozessliste, auf das Netzwerk und auf den Hostnamen.
Ein Container ist also kein:
- kleiner virtueller Computer,
- eigenes Betriebssystem,
- Sicherheitsgefängnis mit Panzerglas,
- magischer Ort, an dem Software plötzlich funktioniert, die vorher kaputt war.
Was ein Container hingegen tatsächlich mitbringt: ein fest verpacktes Dateisystem samt aller Bibliotheken, die die Anwendung braucht, und eine Beschreibung, wie sie zu starten ist. Das klingt unspektakulär, ist aber der Grund für den ganzen Wirbel — dazu gleich mehr.
1.2 Abgrenzung zur Virtualisierung
Der Vergleich mit virtuellen Maschinen ist der übliche Einstieg, und er ist auch der nützlichste.
Eine virtuelle Maschine simuliert Hardware. Darauf installiert man ein vollständiges Betriebssystem mit eigenem Kernel, eigenem Bootvorgang, eigenem systemd, eigener Paketverwaltung. Das kostet Speicher, es kostet Startzeit, und es kostet Pflegeaufwand — jede VM will gepatcht werden.
Ein Container teilt sich den Kernel mit dem Host. Er bringt nur das mit, was oberhalb des Kernels liegt: Bibliotheken, Konfiguration, die Anwendung selbst.
Virtuelle Maschinen Container ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ App │ │ App │ │ App │ │ App │ │ App │ │ App │ │ Libs │ │ Libs │ │ Libs │ │ Libs │ │ Libs │ │ Libs │ │ Gast-│ │ Gast-│ │ Gast-│ └──────┘ └──────┘ └──────┘ │ OS + │ │ OS + │ │ OS + │ ┌────────────────────────┐ │Kernel│ │Kernel│ │Kernel│ │ Container-Laufzeit │ └──────┘ └──────┘ └──────┘ └────────────────────────┘ ┌────────────────────────┐ ┌────────────────────────┐ │ Hypervisor │ │ Host-Kernel (Linux) │ ├────────────────────────┤ ├────────────────────────┤ │ Hardware │ │ Hardware │ └────────────────────────┘ └────────────────────────┘
| Aspekt | Virtuelle Maschine | Container |
|---|---|---|
| Startzeit | 30 Sekunden bis Minuten | Millisekunden bis Sekunden |
| Größe | mehrere GB | 5 MB bis wenige hundert MB |
| Kernel | eigener | gemeinsam mit dem Host |
| Isolation | sehr stark (Hardware-Ebene) | gut, aber schwächer (Kernel-Ebene) |
| Fremdes Betriebssystem | ja (Windows auf Linux) | nein |
| Dichte pro Server | Dutzende | Hunderte |
| Patchen | jedes Gastsystem einzeln | Image neu bauen |
Achtung: Der letzte Punkt in der Isolationszeile ist wichtiger, als er aussieht. Container teilen sich den Kernel. Eine Kernel-Lücke, die aus einem Container ausbricht, betrifft potenziell alle anderen auf demselben Host. Deshalb ist die Antwort auf „Container oder VM?“ oft „beides„ — Container laufen in der Praxis sehr häufig in virtuellen Maschinen. Das ist keine Doppelung, sondern gestaffelte Sicherheit.
1.3 Ein kurzer historischer Abriss
Container sind keine Erfindung von 2013, auch wenn es sich manchmal so anfühlt.
| Jahr | Meilenstein | Was dazukam |
|---|---|---|
| 1979 | chroot in Unix V7 | getrennte Sicht auf das Dateisystem |
| 2000 | FreeBSD Jails | echte Isolation inklusive Netzwerk |
| 2005 | Solaris Zones | Ressourcenverwaltung |
| 2006 | cgroups im Linux-Kernel (von Google) | Begrenzung von CPU und Speicher |
| 2008 | LXC | erste brauchbare Container unter Linux |
| 2013 | Docker | das Image-Format und eine einfache Bedienung |
| 2015 | Open Container Initiative (OCI) | Standardisierung |
| 2018 | Podman 1.0 | Container ohne Daemon und ohne Root |
| 2014–heute | Kubernetes | Orchestrierung über viele Hosts |
Die eigentliche Leistung von Docker war nicht die Technik — Namespaces und cgroups gab es vorher. Die Leistung war die Verpackung: ein standardisiertes Image-Format, eine Registry zum Teilen und eine Kommandozeile, die man in zehn Minuten versteht. Das ist ungefähr so, als hätte jemand den Babelfisch erfunden — die Sprachen gab es vorher auch schon, sie waren nur unbenutzbar für alle, die nicht Jahre investieren wollten.
1.4 Voraussetzungen und Aufbau dieses Tutorials
Was du mitbringen solltest: Umgang mit der Linux-Kommandozeile, ein Gefühl für Dateirechte und Prozesse, Grundkenntnisse in Netzwerk (IP, Port, DNS). Programmieren musst du nicht können.
Was du brauchst: Ein Linux-System, auf dem du Root-Rechte hast. Ein Raspberry Pi 4 oder 5 reicht für fast alles in diesem Tutorial. Für Kapitel 14 brauchst du RHEL 9 (oder AlmaLinux/Rocky 9, das funktioniert identisch).
Welche Befehle stehen hier? Die Beispiele sind in Podman-Syntax geschrieben, mit dem Docker-Äquivalent in Klammern, wo es abweicht. Der Grund: Podman ist unter RHEL der Standard, und die Befehle sind zu über 95 Prozent identisch. Wer Docker nutzt, ersetzt gedanklich podman durch docker und liegt fast immer richtig.
2. Technische Grundlagen
Dieses Kapitel erklärt, wie die Illusion funktioniert. Man kann Container auch ohne dieses Wissen bedienen — aber sobald etwas nicht funktioniert, trennt sich hier die Spreu vom Weizen. Wer weiß, dass ein Container nur ein Prozess mit ein paar Namespaces ist, findet Fehler in Minuten statt in Stunden.
2.1 Namespaces — die Sicht des Prozesses auf die Welt
Namespaces sind eine Kernel-Funktion, die einem Prozess eine eigene, isolierte Sicht auf eine bestimmte Systemressource gibt. Der Linux-Kernel kennt derzeit diese:
| Namespace | Isoliert | Praktische Wirkung |
|---|---|---|
pid | Prozess-IDs | Im Container ist die Anwendung PID 1 und sieht keine Host-Prozesse |
mnt | Einhängepunkte | Eigenes Wurzeldateisystem |
net | Netzwerk | Eigene Interfaces, IP-Adressen, Ports, Routing |
uts | Hostname | hostname im Container liefert etwas anderes |
ipc | Shared Memory, Semaphore | Prozesse können nicht querfunken |
user | UID/GID-Zuordnung | Grundlage für rootless — siehe Kapitel 6.2 |
cgroup | Sicht auf die cgroup-Hierarchie | Container sieht seine eigene Begrenzung |
time | Systemzeit-Offset | selten genutzt |
Das lässt sich direkt nachvollziehen:
# Prozessliste auf dem Host ps aux | wc -l # z.B. 180 Prozesse # Prozessliste im Container podman run --rm registry.access.redhat.com/ubi9/ubi ps aux # -> genau ein Prozess, nämlich ps selbst, mit PID 1
Und der Beweis, dass trotzdem alles auf demselben Kernel läuft:
podman run --rm registry.access.redhat.com/ubi9/ubi uname -r uname -r # -> beide Male dieselbe Kernel-Version
Merksatz: Ein Container hat kein eigenes Betriebssystem. Er hat die Dateien eines Betriebssystems, aber der Kernel gehört dem Host. Genau deshalb kann man auf einem Ubuntu-Host ein RHEL-Image starten — die Bibliotheken kommen aus dem Image, der Kernel vom Host. Und genau deshalb kann man keinen Windows-Container auf einem Linux-Host starten: Da fehlt der passende Kernel.
2.2 cgroups — Ressourcenbegrenzung
Namespaces regeln, was ein Prozess sieht. Control Groups regeln, wie viel er verbrauchen darf. Ohne cgroups könnte ein einziger amoklaufender Container den ganzen Server lahmlegen — was in der Praxis ungefähr so unterhaltsam ist wie ein Vogonisches Gedicht.
# Halbe CPU, maximal 256 MB Arbeitsspeicher podman run -d --name test \ --cpus 0.5 \ --memory 256m \ --memory-swap 256m \ docker.io/library/nginx:alpine # Live-Verbrauch beobachten podman stats
RHEL 9 und Ubuntu 22.04 aufwärts nutzen cgroups v2. Das ist relevant, weil rootless-Container mit v2 deutlich besser funktionieren — unter v1 waren Ressourcenlimits für nicht-privilegierte Benutzer praktisch nicht durchsetzbar.
# Prüfen, welche Version aktiv ist stat -fc %T /sys/fs/cgroup/ # cgroup2fs = v2, tmpfs = v1
2.3 Capabilities, seccomp und SELinux
Drei weitere Schutzschichten, die zusammenwirken:
Capabilities zerlegen die Allmacht von Root in etwa 40 Einzelrechte — etwa CAP_NET_BIND_SERVICE (Ports unter 1024 belegen) oder CAP_SYS_ADMIN (praktisch alles). Container starten standardmäßig mit einer stark reduzierten Auswahl.
# Alle Capabilities entziehen, nur eine zurückgeben podman run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
seccomp filtert Systemaufrufe. Der Standardprofil-Filter blockiert rund 60 der über 300 Syscalls — darunter solche, die kein normales Programm je braucht, die aber gern für Kernel-Exploits verwendet werden.
SELinux (RHEL) bzw. AppArmor (Ubuntu) kontrolliert den Zugriff auf Dateien und Geräte auf einer weiteren, unabhängigen Ebene. Unter RHEL bekommen Container den Typ container_t und dürfen damit von Haus aus fast nichts auf dem Host anfassen.
Achtung: Diese drei Schichten sind der Grund, warum ein Container-Ausbruch aufwendig ist — und warum man sie nicht abschalten sollte. Wer bei einem Problem reflexartig –privileged setzt, hat sämtliche Schutzmechanismen gleichzeitig deaktiviert. Das ist die Container-Variante von „chmod 777, dann geht's“.
2.4 Union-Dateisysteme und das Schichtenmodell
Ein Image besteht nicht aus einer großen Datei, sondern aus übereinandergelegten Schichten (Layers). Jede Schicht enthält nur die Änderungen gegenüber der darunterliegenden.
┌──────────────────────────────┐ │ Schreibschicht (Container) │ ← flüchtig, weg beim Löschen ├──────────────────────────────┤ │ Schicht 4: App-Code │ ┐ ├──────────────────────────────┤ │ │ Schicht 3: Abhängigkeiten │ │ nur lesbar, ├──────────────────────────────┤ │ gemeinsam nutzbar │ Schicht 2: Laufzeitumgebung │ │ ├──────────────────────────────┤ │ │ Schicht 1: Basis-Image │ ┘ └──────────────────────────────┘
Daraus folgen drei Dinge, die im Alltag ständig relevant sind:
- Speicherersparnis. Zehn Container aus demselben Image belegen den Image-Platz genau einmal. Nur die Schreibschichten sind individuell.
- Alles im Container ist flüchtig. Wird der Container gelöscht, ist die Schreibschicht weg. Wer Daten behalten will, braucht Volumes (Kapitel 3.4).
- Löschen macht Images nicht kleiner. Wenn Schicht 3 eine 500-MB-Datei anlegt und Schicht 4 sie löscht, ist sie in Schicht 3 immer noch enthalten. Das Image bleibt groß. Deshalb gehören
dnf installunddnf clean allin eineRUN-Anweisung.
2.5 Was ein Container-Image wirklich ist
Ein Image ist im Kern:
- ein Satz komprimierter tar-Archive (die Schichten),
- eine Konfigurationsdatei in JSON (Startbefehl, Umgebungsvariablen, Arbeitsverzeichnis, Benutzer, exponierte Ports),
- ein Manifest, das beides zusammenhält.
Das lässt sich ansehen:
podman inspect docker.io/library/nginx:alpine | less podman history docker.io/library/nginx:alpine
Hinweis zu Tags und Digests: Ein Tag wie nginx:latest ist nur ein beweglicher Zeiger. Er kann morgen auf ein anderes Image zeigen. Der Digest (sha256:…) identifiziert ein Image dagegen eindeutig und unveränderlich. In Produktivumgebungen arbeitet man mit Digests — das ist der Unterschied zwischen „wir betreiben nginx„ und „wir betreiben genau dieses nginx“.
podman inspect --format '{{.Digest}}' nginx:alpine podman run docker.io/library/nginx@sha256:abc123...
2.6 Die OCI-Standards
Damit nicht jeder Hersteller sein eigenes Format kocht, gibt es seit 2015 die Open Container Initiative. Sie definiert drei Spezifikationen:
| Spezifikation | Regelt |
|---|---|
| Image Spec | Aufbau von Images: Schichten, Manifest, Konfiguration |
| Runtime Spec | Wie ein Container aus einem entpackten Image gestartet wird |
| Distribution Spec | Wie Registries Images ausliefern (das HTTP-API) |
Praktische Konsequenz: Ein Image ist ein Image. Was Docker baut, läuft unter Podman. Was Podman baut, läuft unter Kubernetes und OpenShift. Was du auf deinem Raspberry Pi zusammenbastelst, läuft unverändert in einer Azure-Umgebung — vorausgesetzt, die Prozessorarchitektur passt.
Die eigentlichen Laufzeiten, die den Container am Ende starten, heißen runc (in C++/Go, der Klassiker) und crun (in C geschrieben, schneller und sparsamer, Standard unter RHEL).
3. Erste Schritte mit Containern
Genug Theorie. Ab hier wird getippt.
3.1 Container starten, stoppen, auflisten
# Der Klassiker podman run docker.io/library/hello-world # Etwas Nützlicheres: ein Webserver im Hintergrund podman run -d --name meinweb -p 8080:80 docker.io/library/nginx:alpine # Was läuft gerade? podman ps podman ps -a # auch gestoppte # Stoppen, starten, entfernen podman stop meinweb podman start meinweb podman rm -f meinweb
| Option | Bedeutung |
|---|---|
-d | detached — läuft im Hintergrund |
-it | interaktiv mit Terminal |
–rm | Container nach Beenden automatisch löschen |
–name | fester Name statt zufälliger Wortkombination |
-p HOST:CONTAINER | Portweiterleitung |
-v | Volume oder Bind Mount |
-e | Umgebungsvariable |
Tipp: Ohne –name vergibt Podman lustige Zufallsnamen wie nostalgic_hawking. Das ist charmant, aber in Skripten unbrauchbar. Gewöhne dir –name von Anfang an an.
3.2 Interaktiver Zugriff und Logs
# Shell in einem laufenden Container podman exec -it meinweb /bin/sh # Einmal-Container mit Shell, verschwindet danach spurlos podman run -it --rm registry.access.redhat.com/ubi9/ubi /bin/bash # Logs ansehen und mitverfolgen podman logs meinweb podman logs -f --tail 50 meinweb # Prozesse im Container podman top meinweb
Wichtig: podman exec ist ein Diagnosewerkzeug, kein Betriebsverfahren. Wenn du dich regelmäßig in einen Container einloggst, um dort etwas zu reparieren oder zu konfigurieren, läuft etwas grundsätzlich schief. Änderungen gehören ins Image oder in die Konfiguration — sonst sind sie beim nächsten Neustart weg. Container sind Vieh, keine Haustiere.
3.3 Portweiterleitung und Netzwerkgrundlagen
Ein Container hat sein eigenes Netzwerk-Namespace und damit seine eigene IP. Damit man ihn von außen erreicht, braucht es eine Weiterleitung:
podman run -d -p 8080:80 nginx:alpine # ↑ ↑ # │ └── Port im Container # └─────── Port auf dem Host
Eigene Netzwerke sind der saubere Weg, mehrere Container zusammenarbeiten zu lassen:
podman network create appnetz podman run -d --name db --network appnetz \ -e POSTGRES_PASSWORD=geheim \ docker.io/library/postgres:16 podman run -d --name app --network appnetz -p 8080:8080 meine-app:1.0
In einem selbst angelegten Netzwerk erreichen sich Container über ihren Namen: Die Anwendung verbindet sich einfach zu db:5432. Eine IP-Adresse muss niemand kennen — es gibt einen eingebauten DNS-Dienst.
Achtung: Das funktioniert nur in selbst angelegten Netzwerken. Im Standardnetzwerk gibt es keine Namensauflösung zwischen Containern. Das ist eine der häufigsten Stolperfallen überhaupt.
3.4 Volumes und Bind Mounts
Container sind flüchtig. Daten sollen es nicht sein. Dafür gibt es zwei Mechanismen:
# Named Volume - von Podman verwaltet, empfohlen für Datenbanken podman volume create dbdaten podman run -d --name db -v dbdaten:/var/lib/postgresql/data postgres:16 # Bind Mount - ein Host-Verzeichnis wird eingehängt podman run -d -v /srv/web:/usr/share/nginx/html:ro,Z nginx:alpine # Verwaltung podman volume ls podman volume inspect dbdaten podman volume rm dbdaten
| Named Volume | Bind Mount | |
|---|---|---|
| Ort | von Podman verwaltet | frei wählbar |
| Rechte/SELinux | automatisch korrekt | muss man selbst regeln |
| Sicherung | über podman volume export | normales Dateisystem-Backup |
| Typischer Einsatz | Datenbanken, Anwendungsdaten | Konfiguration, Entwicklung, statische Inhalte |
Die SELinux-Falle unter RHEL. Ein Bind Mount ohne passenden SELinux-Kontext führt zu Permission denied — und zwar auch dann, wenn die Dateirechte offensichtlich stimmen. Die Lösung ist das Suffix Z (großes Z: exklusiv für diesen Container) oder z (kleines z: von mehreren Containern nutzbar):
-v /srv/web:/usr/share/nginx/html:ro,Z
Wer stattdessen SELinux deaktiviert, löst das Problem so, wie man ein klemmendes Fenster mit einem Vorschlaghammer löst.
3.5 Umgebungsvariablen und Konfiguration
podman run -d --name db \ -e POSTGRES_USER=app \ -e POSTGRES_PASSWORD=geheim \ -e POSTGRES_DB=produktion \ postgres:16 # Aus einer Datei podman run -d --env-file ./app.env meine-app:1.0
Achtung: Passwörter als Umgebungsvariablen sind bequem und unsicher — sie stehen in podman inspect, in der Prozessliste und oft in den Logs. Für echte Geheimnisse gibt es podman secret (Kapitel 13.4).
3.6 Ressourcenlimits setzen
podman run -d --name app \ --cpus 1.5 \ --memory 512m \ --memory-swap 512m \ --pids-limit 200 \ meine-app:1.0
Tipp: Setze Limits immer, auch auf dem Testsystem. Ein Container ohne Speicherlimit darf sich den gesamten Hauptspeicher nehmen, und dann entscheidet der OOM-Killer des Kernels, welcher Prozess sterben muss. Seine Auswahl ist selten die, die man sich gewünscht hätte.
3.7 Aufräumen
Container-Systeme neigen zur Vermüllung. Alte Images, verwaiste Volumes, gestoppte Container — irgendwann ist /var voll, und zwar immer freitags um 17 Uhr.
# Was belegt eigentlich Platz? podman system df # Gestoppte Container, ungenutzte Netzwerke und dangling Images podman system prune # Zusätzlich alle ungenutzten Images und Volumes - Vorsicht! podman system prune -a --volumes
podman system prune -a –volumes löscht alle Volumes, die gerade an keinem laufenden Container hängen. Wenn deine Datenbank zufällig gestoppt ist, sind ihre Daten anschließend weg. Dieser Befehl gehört nicht in einen Cronjob.
4. Eigene Images bauen
4.1 Containerfile — Aufbau und Anweisungen
Ein Containerfile (identisch zu Dockerfile, nur ohne Markennamen) ist ein Rezept. Jede Zeile erzeugt potenziell eine neue Schicht.
- Containerfile
# Basis-Image FROM registry.access.redhat.com/ubi9/ubi-minimal:latest # Metadaten LABEL maintainer="andre@example.org" \ version="1.0" \ description="Beispielanwendung" # Pakete installieren - alles in EINER Schicht RUN microdnf install -y python3 python3-pip && \ microdnf clean all && \ rm -rf /var/cache/yum # Arbeitsverzeichnis WORKDIR /app # Abhängigkeiten zuerst - wegen des Caches, siehe 4.2 COPY requirements.txt . RUN pip3 install --no-cache-dir -r requirements.txt # Erst danach der Anwendungscode COPY src/ ./src/ # Unprivilegierter Benutzer RUN useradd -u 1001 -r -g 0 -m -d /app -s /sbin/nologin appuser && \ chown -R 1001:0 /app && \ chmod -R g=u /app USER 1001 # Dokumentation des Ports (öffnet nichts!) EXPOSE 8080 # Gesundheitsprüfung HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Startbefehl ENTRYPOINT ["python3"] CMD ["src/app.py"]
| Anweisung | Bedeutung |
|---|---|
FROM | Basis-Image |
RUN | Befehl beim Bauen ausführen |
COPY | Dateien aus dem Build-Kontext ins Image |
ADD | wie COPY, kann zusätzlich entpacken und URLs laden — meide es |
WORKDIR | Arbeitsverzeichnis setzen |
ENV | Umgebungsvariable im Image |
USER | Benutzer für alle folgenden Anweisungen und zur Laufzeit |
EXPOSE | reine Dokumentation, öffnet keinen Port |
ENTRYPOINT | das Programm, das läuft |
CMD | Standardargumente dafür (überschreibbar) |
HEALTHCHECK | Zustandsprüfung |
Bauen und starten:
podman build -t meine-app:1.0 . podman run -d -p 8080:8080 meine-app:1.0
4.2 Schichten verstehen und Cache nutzen
Podman und Docker speichern jede Schicht zwischen. Ändert sich eine Zeile im Containerfile, werden diese und alle folgenden Schichten neu gebaut, alle vorherigen kommen aus dem Cache.
Daraus folgt die wichtigste Bauregel überhaupt:
Sortiere vom Selten-Änderndem zum Oft-Änderndem.
Basis-Image und Systempakete ändern sich vielleicht monatlich. Abhängigkeiten wöchentlich. Der eigene Code zwanzigmal am Tag. Wer den Code vor die Abhängigkeiten stellt, installiert bei jeder Codeänderung alle Pakete neu — aus einem 8-Sekunden-Build wird ein 4-Minuten-Build. Zwanzigmal am Tag.
# Schlecht # Gut
COPY . . COPY requirements.txt .
RUN pip install -r req.txt RUN pip install -r requirements.txt
COPY src/ ./src/
Eine .containerignore (bzw. .dockerignore) hält Ballast aus dem Build-Kontext heraus:
- .containerignore
.git .gitignore *.md __pycache__/ node_modules/ tests/ .env
4.3 Multi-Stage-Builds
Die eleganteste Technik im ganzen Kapitel. Man baut in einer fetten Umgebung mit Compiler und Werkzeugen — und kopiert dann nur das Ergebnis in ein winziges Laufzeit-Image.
- Containerfile.multistage
# ---------- Stufe 1: Bauen ---------- FROM registry.access.redhat.com/ubi9/go-toolset:latest AS builder WORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /build/anwendung ./cmd/server # ---------- Stufe 2: Laufzeit ---------- FROM registry.access.redhat.com/ubi9/ubi-micro:latest COPY --from=builder /build/anwendung /usr/local/bin/anwendung USER 1001 EXPOSE 8080 ENTRYPOINT ["/usr/local/bin/anwendung"]
Das Ergebnis: Aus einem Build-Image von rund 1,2 GB wird ein Laufzeit-Image von vielleicht 30 MB. Der Go-Compiler, die Quelltexte, die Testdaten und die Build-Abhängigkeiten sind im fertigen Image schlicht nicht vorhanden — und was nicht da ist, kann auch keine Sicherheitslücke haben.
4.4 Basis-Images auswählen
| Basis | Größe | Paketmanager | Wofür |
|---|---|---|---|
ubi9/ubi | ~220 MB | dnf | RHEL-Umfeld, voller Funktionsumfang, Red-Hat-Support |
ubi9/ubi-minimal | ~90 MB | microdnf | RHEL-Umfeld, schlank — guter Standard |
ubi9/ubi-micro | ~25 MB | keiner | nur mit Multi-Stage sinnvoll |
alpine | ~7 MB | apk | sehr klein, aber musl statt glibc |
debian:slim | ~75 MB | apt | breite Kompatibilität |
gcr.io/distroless/* | ~20 MB | keiner | maximale Härtung, keine Shell |
scratch | 0 MB | keiner | nur für statisch gelinkte Binärdateien |
Die Alpine-Falle. Alpine verwendet musl libc statt der üblichen glibc. Das ist meistens egal — bis es das nicht mehr ist. Typische Symptome: DNS-Auflösung verhält sich anders, Python-Pakete mit C-Erweiterungen müssen mühsam kompiliert werden statt fertige Wheels zu nutzen, und exotische Laufzeitfehler tauchen erst in Produktion auf. Wenn die 80 MB Unterschied nicht wehtun, ist ubi-minimal oder debian:slim die ruhigere Wahl.
Die Red Hat Universal Base Images (UBI) sind übrigens frei verwendbar und weiterverteilbar — auch außerhalb einer RHEL-Subskription und auch kommerziell. Sie sind damit für RHEL-Umgebungen die naheliegende Wahl, weil Basis-Image und Host aus derselben Quelle stammen.
4.5 Gute Praxis
- Nie als Root laufen lassen.
USER 1001gehört in jedes Containerfile. OpenShift erzwingt das ohnehin (Kapitel 9.2). - Feste Versionen verwenden.
FROM ubi9/ubi-minimal:9.4stattlatest. „Latest„ ist kein Zustand, sondern ein Versprechen ohne Deckung. - Ein Prozess pro Container. Kein systemd, kein supervisord, kein SSH-Server im Container. Wer SSH in einen Container einbaut, hat eine VM gebaut und ist einen Umweg gegangen.
- Keine Geheimnisse ins Image. Sie bleiben in der Schicht erhalten, auch wenn eine spätere Schicht sie löscht. Jeder mit Zugriff auf das Image kommt heran.
- Cache und temporäre Dateien in derselben RUN-Anweisung aufräumen.
HEALTHCHECKdefinieren. Ein laufender Prozess ist nicht dasselbe wie ein funktionierender Dienst.
4.6 Tagging-Strategien
podman build -t meine-app:1.4.2 -t meine-app:1.4 -t meine-app:latest .
Bewährt hat sich die Kombination aus semantischer Version (1.4.2), Git-Commit (1.4.2-a3f9c1) und einem beweglichen Zeiger (stable). In Produktion referenziert man den Digest — dann ist unstrittig, was läuft.
5. Registries
5.1 Öffentliche Registries
Eine Registry ist ein Dateiserver für Images mit standardisiertem API.
| Registry | Betreiber | Besonderheit |
|---|---|---|
docker.io | Docker Inc. | größte Auswahl, Rate Limits für anonyme Nutzer |
quay.io | Red Hat | integriertes Schwachstellen-Scanning |
registry.access.redhat.com | Red Hat | UBI-Images, keine Anmeldung nötig |
registry.redhat.io | Red Hat | Produkt-Images, Subskription erforderlich |
ghcr.io | GitHub | eng an GitHub Actions gekoppelt |
gcr.io | Distroless-Images |
Unter Podman muss man die Registry meist vollqualifiziert angeben. Das ist Absicht und ein Sicherheitsmerkmal: Bei Docker bedeutet nginx automatisch docker.io/library/nginx. Podman möchte, dass du dir darüber im Klaren bist, woher deine Software kommt.
podman pull docker.io/library/nginx:alpine # eindeutig podman pull nginx:alpine # fragt ggf. nach
Die Suchreihenfolge steht in /etc/containers/registries.conf.
5.2 Authentifizierung und Rate Limits
podman login docker.io podman login registry.redhat.io podman logout --all
Die Zugangsdaten landen bei rootless-Podman in ${XDG_RUNTIME_DIR}/containers/auth.json.
Hinweis: Docker Hub begrenzt anonyme Downloads pro IP-Adresse. In Firmennetzen teilen sich unter Umständen hunderte Rechner eine öffentliche IP — dann sind die Kontingente schnell aufgebraucht, und Builds schlagen scheinbar grundlos fehl. Abhilfe: anmelden, eine Spiegel-Registry betreiben oder auf Quay bzw. die Red-Hat-Registries ausweichen.
5.3 Eine eigene Registry betreiben
Für den Hausgebrauch reicht die Referenzimplementierung:
podman run -d --name registry \ -p 5000:5000 \ -v registrydaten:/var/lib/registry \ docker.io/library/registry:2 podman tag meine-app:1.0 localhost:5000/meine-app:1.0 podman push localhost:5000/meine-app:1.0
Für ernsthaften Betrieb: Quay (Red Hat), Harbor (CNCF), Nexus oder GitLab Container Registry. Diese bringen Rechteverwaltung, Spiegelung, Aufbewahrungsregeln und Schwachstellen-Scanning mit.
5.4 Image-Signaturen und Vertrauensketten
Ein Image aus dem Internet ist erst einmal fremder Code, der bei dir Prozesse startet. Signaturen belegen Herkunft und Unversehrtheit.
# Signieren mit GPG podman push --sign-by andre@example.org localhost:5000/meine-app:1.0
Die Datei /etc/containers/policy.json legt fest, welchen Quellen vertraut wird — bis hin zu „nur signierte Images aus der eigenen Registry dürfen laufen“. In regulierten Umgebungen ist das kein Luxus, sondern Pflichtprogramm.
Im Kubernetes-Umfeld hat sich zusätzlich sigstore/cosign etabliert, das ohne eigene Schlüsselverwaltung auskommt.
5.5 Schwachstellen-Scanning
# Trivy - schnell, weit verbreitet podman run --rm -v /var/run/podman/podman.sock:/var/run/docker.sock \ docker.io/aquasec/trivy:latest image meine-app:1.0 # Grype - Alternative grype meine-app:1.0
Der eigentliche Gewinn beim Patchen. Auf einem klassischen Server pflegt man Pakete nach: dnf update, hoffen, dass nichts bricht. Bei Containern baut man das Image neu — mit aktualisiertem Basis-Image — und tauscht den laufenden Container aus. Geht etwas schief, startet man die alte Version. Der Rollback dauert Sekunden statt eines Wiederherstellungsvorgangs aus dem Backup.
Voraussetzung ist allerdings, dass der Neubau automatisiert und reproduzierbar ist. Ein Image, das nur Kollege Müller auf seinem Notebook bauen kann, ist keine Verbesserung — das ist ein Single Point of Failure mit Urlaubsanspruch.
6. Docker und Podman im Vergleich
Die Frage „Docker oder Podman?„ wird gern zur Glaubensfrage aufgeblasen. Sie ist keine. Beide bauen und starten OCI-Container, beide sprechen fast dieselbe Kommandozeile. Der Unterschied liegt in der Architektur — und der hat handfeste Konsequenzen.
6.1 Architektur: Daemon vs. daemonless
Docker arbeitet mit einem zentralen Hintergrunddienst. Der docker-Befehl ist nur ein Client, der über einen Socket mit dem dockerd-Daemon spricht. Der Daemon läuft als Root und ist Elternprozess aller Container.
Podman hat keinen Daemon. Der Befehl startet den Container direkt als Kindprozess der eigenen Shell und beendet sich. Ein Hilfsprozess namens conmon bleibt als Aufpasser zurück.
Docker Podman
┌──────────────┐ ┌──────────────┐
│ docker (CLI) │ │ podman │
└──────┬───────┘ └──────┬───────┘
│ REST über Socket │ fork/exec
┌──────▼───────┐ ┌──────▼───────┐
│ dockerd │ ← als root │ conmon │
│ (Daemon) │ └──────┬───────┘
└──────┬───────┘ ┌──────▼───────┐
┌──────▼───────┐ │ Container │ ← als du
│ Container │ ← Kind von dockerd└──────────────┘
└──────────────┘
Warum das im Alltag zählt:
| Docker | Podman | |
|---|---|---|
| Zentraler Angriffspunkt | ja, dockerd als root | nein |
| Daemon-Absturz | reißt potenziell alles mit | existiert nicht |
| Zugriff auf den Socket | entspricht faktisch Root auf dem Host | entfällt |
| Prozessbaum | alle Container hängen am Daemon | Container gehören dem Benutzer |
| systemd-Integration | Docker verwaltet Neustarts selbst | systemd verwaltet Container direkt |
Der Docker-Socket ist eine Root-Shell mit Umweg. Wer Mitglied der Gruppe docker ist, kann einen Container starten, der das gesamte Host-Dateisystem einhängt — und ist damit faktisch Root. Das ist kein Fehler, sondern eine Konsequenz der Architektur. Genau diesen Umstand wollte Podman abschaffen.
6.2 Rootful und rootless
Das ist das Kernargument für Podman, deshalb etwas ausführlicher.
Bei rootless-Podman startet ein normaler Benutzer Container, ohne irgendwann Root-Rechte zu benötigen. Möglich macht das der User-Namespace: Innerhalb des Containers ist der Prozess UID 0 — auf dem Host wird diese UID aber auf einen ganz gewöhnlichen, unprivilegierten Bereich abgebildet.
cat /etc/subuid # andre:100000:65536 # Bedeutung: Dem Benutzer andre gehören die Host-UIDs 100000 bis 165535. # Container-UID 0 -> Host-UID 100000 # Container-UID 1000 -> Host-UID 101000
Praktisch heißt das: Bricht ein Prozess aus dem Container aus, steht er auf dem Host als UID 100000 da — ein Benutzer ohne jede Berechtigung, der nicht einmal seine eigenen Dateien wiederfindet. Verglichen mit „Angreifer ist root auf dem Host“ ist das ein erheblicher Unterschied.
Der Preis dafür sind ein paar Einschränkungen:
| Einschränkung | Lösung |
|---|---|
| Ports unter 1024 nicht belegbar | Sysctl anpassen oder Reverse Proxy davorstellen |
| Kein Zugriff auf beliebige Host-Geräte | bewusste Freigabe nötig |
| Manche Dateisysteme (NFS) machen Probleme | lokale Volumes verwenden |
| Etwas langsameres Netzwerk | für die meisten Lasten irrelevant |
# Ports ab 80 auch für unprivilegierte Nutzer freigeben echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-rootless.conf sudo sysctl --system
Docker kennt inzwischen ebenfalls einen rootless-Modus, aber er ist eine nachgerüstete Betriebsart. Bei Podman ist er der Normalfall.
Zum Netzwerk unter rootless: Historisch übernahm slirp4netns die Anbindung. Seit Podman 5 ist pasta der Standard — deutlich schneller und mit dem angenehmen Nebeneffekt, dass die Quell-IP eingehender Verbindungen erhalten bleibt. Wer noch mit Podman 4.x arbeitet, kann pasta über –network pasta anfordern.
6.3 Kompatibilität der Kommandozeile
alias docker=podman
Damit funktionieren geschätzt 95 Prozent aller Docker-Anleitungen aus dem Netz unverändert. Podman bietet zusätzlich ein Docker-kompatibles API an, sodass Werkzeuge wie Testcontainers oder docker-compose bedient werden können:
systemctl --user enable --now podman.socket export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock docker-compose up -d
Was nicht funktioniert: Docker Swarm. Podman hat dafür kein Gegenstück und wird auch keines bekommen — für Orchestrierung verweist Red Hat auf Kubernetes.
6.4 Pods bei Podman
Ein Pod ist eine Gruppe von Containern, die sich Netzwerk-Namespace und damit die IP-Adresse teilen. Sie erreichen einander über localhost. Das Konzept stammt aus Kubernetes, und Podman hat es übernommen — daher der Name.
podman pod create --name webstack -p 8080:80 podman run -d --pod webstack --name datenbank \ -e POSTGRES_PASSWORD=geheim postgres:16 podman run -d --pod webstack --name anwendung meine-app:1.0 # Die Anwendung erreicht die Datenbank unter localhost:5432 podman pod ps podman pod stop webstack
Das ist mehr als eine Spielerei: Wer lokal mit Pods arbeitet, denkt bereits in Kubernetes-Strukturen — und kann die Konfiguration später direkt exportieren (Kapitel 15.1).
6.5 Das Podman-Umfeld
Docker ist ein Werkzeug für alles. Podman folgt der Unix-Philosophie und zerlegt die Aufgaben:
| Werkzeug | Aufgabe |
|---|---|
| Podman | Container starten und verwalten |
| Buildah | Images bauen, auch ohne Containerfile per Skript |
| Skopeo | Images zwischen Registries kopieren, prüfen, signieren — ohne sie herunterzuladen |
| podman-compose | Compose-Dateien verarbeiten |
Besonders Skopeo ist im Betrieb Gold wert:
# Image prüfen, ohne es zu laden skopeo inspect docker://docker.io/library/nginx:alpine # Zwischen Registries kopieren, ohne lokalen Speicherplatz zu verbrauchen skopeo copy docker://docker.io/library/nginx:alpine \ docker://registry.firma.de/spiegel/nginx:alpine # In eine Datei für den Transport ins abgeschottete Netz skopeo copy docker://nginx:alpine oci-archive:nginx.tar
6.6 systemd-Integration und Quadlet
Hier zeigt Podman seine größte Stärke im klassischen Serverbetrieb.
Docker verwaltet Neustarts über eigene Richtlinien (–restart=always). Podman überlässt das systemd — dem Werkzeug, das auf dem Server ohnehin für genau diese Aufgabe zuständig ist. Seit Podman 4.4 gibt es dafür Quadlet: Man schreibt eine Unit-Datei im INI-Format, und ein systemd-Generator erzeugt daraus zur Startzeit die vollständige Service-Unit.
- ~/.config/containers/systemd/webserver.container
[Unit] Description=Nginx Webserver After=network-online.target [Container] Image=docker.io/library/nginx:alpine PublishPort=8080:80 Volume=web-inhalt:/usr/share/nginx/html:ro,Z AutoUpdate=registry [Service] Restart=always [Install] WantedBy=default.target
systemctl --user daemon-reload systemctl --user start webserver.service systemctl --user status webserver.service journalctl --user -u webserver.service -f
Der Container ist damit ein vollwertiger systemd-Dienst: Er startet beim Booten, wird bei Absturz neu gestartet, protokolliert nach journald und lässt sich mit denselben Befehlen bedienen wie jeder andere Dienst auf dem System. Für einen Administrator, der seit Jahren mit systemctl arbeitet, ist das der kürzeste Weg vom Bekannten zum Neuen.
Hinweis: Der ältere Befehl podman generate systemd erzeugte Unit-Dateien statisch und gilt inzwischen als überholt. Bei Neuaufbauten führt kein Weg an Quadlet vorbei.
6.7 Docker Desktop, Podman Desktop und die Lizenzfrage
Beide Projekte sind Open Source. Der Unterschied liegt beim Desktop-Produkt für Windows und macOS: Docker Desktop erfordert seit 2021 eine kostenpflichtige Lizenz für Unternehmen ab einer bestimmten Größe (Stand der Regelung: mehr als 250 Beschäftigte oder mehr als 10 Millionen US-Dollar Jahresumsatz). Podman Desktop ist frei.
Für viele Unternehmen war das der eigentliche Auslöser, sich mit Podman zu beschäftigen. Ein selten ausgesprochener, aber wirksamer Migrationsgrund.
6.8 Gegenüberstellung: Wann welches Werkzeug
| Situation | Empfehlung | Begründung |
|---|---|---|
| RHEL-Server im Unternehmen | Podman | mitgeliefert, unterstützt, systemd-integriert |
| Einzelner Linux-Server, Selbstbetrieb | Podman | rootless, Quadlet, kein Daemon |
| Raspberry Pi / Heimserver | Podman oder Docker | Podman sparsamer, Docker mehr Anleitungen im Netz |
| Entwicklung unter macOS/Windows in großer Firma | Podman Desktop | Lizenzfrage |
| Bestehende große Docker-Compose-Landschaft | Docker vorerst | funktionierende Systeme migriert man nicht ohne Grund |
| CI/CD-Pipeline ohne privilegierte Läufer | Podman/Buildah | Bauen ohne Daemon und ohne Root |
| Team, das ausschließlich Docker kennt | Docker, dann schrittweise | Werkzeugwechsel ist kein Selbstzweck |
| Kubernetes-Vorbereitung | Podman | Pods, kube generate, gleiche Denkweise |
Die pragmatische Antwort: Das Image ist standardisiert, also ist die Entscheidung reversibel. Was du heute mit Docker baust, läuft morgen unter Podman und übermorgen in Kubernetes. Diese Frage verdient keine dreitägige Grundsatzdiskussion — sie verdient einen Nachmittag Ausprobieren.
7. Wenn ein Host nicht mehr reicht: Orchestrierung
7.1 Welche Probleme Orchestrierung löst
Solange alles auf einem Server läuft, ist die Welt überschaubar. Die Probleme beginnen bei Fragen wie:
- Der Server fällt aus — wer startet die Container woanders?
- Drei Instanzen der Anwendung laufen — wer verteilt die Anfragen?
- Die Last verdoppelt sich — wer startet zusätzliche Instanzen?
- Neue Version — wie tauscht man aus, ohne dass jemand einen Fehler sieht?
- Auf welchem der zwanzig Server ist eigentlich noch Platz?
- Ein Container ist zwar am Leben, antwortet aber nicht mehr — wer merkt das?
Ein Orchestrator beantwortet diese Fragen automatisch. Man beschreibt den Sollzustand („drei Instanzen dieses Images, erreichbar unter dieser Adresse„), und das System stellt ihn her und hält ihn aufrecht. Wer Ansible kennt: dasselbe Denkmuster, nur dauerhaft statt einmalig.
7.2 Zwischenstufen
Zwischen „ein Container von Hand“ und „Kubernetes-Cluster„ liegen mehrere sinnvolle Stufen, die gern übersprungen werden:
| Stufe | Werkzeug | Leistet | Grenze |
|---|---|---|---|
| 1 | podman run | einzelne Container | nichts überlebt einen Neustart |
| 2 | Quadlet / systemd | Autostart, Neustart, Logs | ein Host |
| 3 | Compose | mehrere Container als Verbund | ein Host |
| 4 | Podman-Pods | geteilte Netzwerke, Kubernetes-Denke | ein Host |
| 5 | Swarm / Nomad | mehrere Hosts, einfach | begrenztes Ökosystem |
| 6 | Kubernetes | alles | hoher Betriebsaufwand |
Die Stufen 2 bis 4 decken erstaunlich viel ab. Ein einzelner ordentlich gepflegter Server mit Quadlet-Units und Git-Anbindung trägt problemlos zwanzig Dienste — mit einem Bruchteil des Aufwands eines Clusters. Genau diesen Aufbau beschreibt Kapitel 14.
7.3 Docker Swarm und Nomad
Docker Swarm ist in Docker eingebaut und in zehn Minuten eingerichtet. Es kann Cluster, Lastverteilung und rollierende Updates. Die Weiterentwicklung ist aber weitgehend zum Erliegen gekommen — für Neuaufbauten schwer zu empfehlen.
HashiCorp Nomad ist deutlich schlanker als Kubernetes, verwaltet neben Containern auch normale Binärdateien und VMs. Kleines Ökosystem, aber echte Anhängerschaft. Seit 2023 unter der BSL-Lizenz, was für manche Unternehmen ein Ausschlusskriterium ist.
7.4 Warum sich Kubernetes durchgesetzt hat
Nicht, weil es einfach wäre. Sondern weil es:
- von Google aus jahrzehntelanger Erfahrung mit Containern entstand,
- früh an eine herstellerneutrale Stiftung übergeben wurde (CNCF),
- ein erweiterbares API hat, sodass Dritte es ausbauen können,
- von allen großen Cloud-Anbietern als Dienst angeboten wird,
- dadurch zum gemeinsamen Nenner wurde — und ein gemeinsamer Nenner gewinnt.
Netzwerkeffekte schlagen technische Eleganz. Das war bei VHS so und ist bei Kubernetes nicht anders.
8. Kubernetes
8.1 Architektur
Ein Kubernetes-Cluster besteht aus Control Plane (dem Gehirn) und Worker Nodes (den Arbeitstieren).
┌─────────────────── Control Plane ───────────────────┐
│ kube-apiserver ← einzige Eintrittstür │
│ etcd ← Datenbank des Sollzustands │
│ kube-scheduler ← entscheidet: welcher Node? │
│ controller-manager ← wacht über Soll vs. Ist │
└──────────────────────────┬──────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
┌────▼──────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│ kubelet │ │ kubelet │ │ kubelet │
│ kube-proxy │ kube-proxy │ kube-proxy
│ containerd/CRI-O │ ... │ ...
│ [Pods] │ │ [Pods] │ │ [Pods] │
└───────────┘ └───────────┘ └───────────┘
Das Grundprinzip ist eine Regelschleife: Der Controller vergleicht permanent Soll- und Ist-Zustand und handelt bei Abweichung. Fällt ein Node aus, sind plötzlich zwei statt drei Instanzen da — also startet der Controller eine neue woanders. Niemand muss eingreifen.
Hinweis zur Container-Laufzeit: Seit Version 1.24 spricht Kubernetes nicht mehr direkt mit Docker. Es verwendet containerd oder CRI-O. Das war seinerzeit Anlass für einige aufgeregte Schlagzeilen, hatte für Anwender aber keinerlei Folgen — die Images blieben ja dieselben.
8.2 Die zentralen Objekte
| Objekt | Aufgabe |
|---|---|
| Pod | kleinste Einheit: ein oder mehrere Container mit gemeinsamer IP |
| ReplicaSet | sorgt für die gewünschte Anzahl gleicher Pods |
| Deployment | verwaltet ReplicaSets, ermöglicht rollierende Updates und Rollback |
| Service | stabile interne Adresse und Lastverteilung |
| Ingress | HTTP-Zugang von außen, Namensbasiert, TLS |
| StatefulSet | für Zustandsbehaftetes wie Datenbanken |
| DaemonSet | genau ein Pod je Node (Logsammler, Monitoring) |
| Job / CronJob | einmalige bzw. wiederkehrende Aufgaben |
Ein minimales Deployment:
- deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: webserver spec: replicas: 3 selector: matchLabels: app: webserver template: metadata: labels: app: webserver spec: containers: - name: nginx image: docker.io/library/nginx:alpine ports: - containerPort: 80 resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "128Mi" cpu: "500m" --- apiVersion: v1 kind: Service metadata: name: webserver spec: selector: app: webserver ports: - port: 80 targetPort: 80
kubectl apply -f deployment.yaml kubectl get pods -w kubectl scale deployment webserver --replicas=5 kubectl rollout undo deployment webserver
8.3 ConfigMaps, Secrets, PersistentVolumes
- ConfigMap — Konfiguration getrennt vom Image. Als Umgebungsvariable oder eingehängte Datei nutzbar.
- Secret — dasselbe für Vertrauliches. Achtung: Secrets sind standardmäßig lediglich Base64-kodiert, also nicht verschlüsselt. Wer echten Schutz will, aktiviert Verschlüsselung in etcd oder nutzt einen externen Tresor wie HashiCorp Vault.
- PersistentVolumeClaim — die Anwendung fordert Speicher an, ohne wissen zu müssen, ob dahinter NFS, iSCSI, Ceph oder eine Cloud-Festplatte steckt.
8.4 Namespaces, RBAC und Kontingente
Namespaces unterteilen den Cluster logisch (etwa nach Team oder Umgebung). RBAC regelt, wer was darf. ResourceQuotas und LimitRanges verhindern, dass ein Team den halben Cluster belegt.
8.5 Deklaratives Arbeiten
Der wichtigste Denkumschwung bei Kubernetes: Man führt keine Befehle aus, man beschreibt Zustände. Statt „starte drei Container“ sagt man „es sollen drei laufen„. Das System kümmert sich um den Rest — auch um drei Uhr nachts, wenn niemand hinschaut.
Konsequenz für die Praxis: Die YAML-Dateien gehören in Git, nicht auf einen Server. Wer per kubectl edit direkt am Cluster herumschraubt, hat den Sinn der Sache verfehlt und produziert einen Zustand, den niemand reproduzieren kann.
Mit steigender Größe wird die YAML-Menge unhandlich. Werkzeuge wie Helm (Paketmanager mit Vorlagen) und Kustomize (Überlagerungen je Umgebung, in kubectl eingebaut) schaffen Abhilfe.
8.6 Distributionen
| Distribution | Passt zu | Anmerkung |
|---|---|---|
| k3s | Raspberry Pi, Edge, kleine Cluster | eine Binärdatei, ~70 MB, SQLite statt etcd |
| k0s | ähnlich wie k3s | zunehmend verbreitet |
| MicroShift | Edge auf RHEL | OpenShift-Kern, minimaler Fußabdruck |
| kubeadm | eigenes Rechenzentrum | der Standardweg, viel Handarbeit |
| OpenShift | Unternehmen | siehe Kapitel 9 |
| EKS / AKS / GKE | Cloud | Control Plane wird verwaltet |
| kind / minikube | Entwicklung | Cluster im Container bzw. in der VM |
8.7 Was Kubernetes tatsächlich kostet
Das wird in Vorträgen gern unterschlagen, deshalb hier deutlich:
Ein selbst betriebener Kubernetes-Cluster braucht dauerhaft Aufmerksamkeit für: Aktualisierungen der Control Plane (etwa dreimal jährlich, mit veralteten APIs als Stolperfalle), Zertifikate, Netzwerk (CNI-Plugin), Speicheranbindung (CSI-Treiber), Monitoring, Logsammlung, Backup von etcd, Rechteverwaltung und Netzwerkrichtlinien.
Als Faustregel gilt: eine halbe bis eine ganze Vollzeitstelle, nur für die Plattform. Nicht für die Anwendungen darauf.
Kubernetes löst Probleme, die man ab einer gewissen Größe hat. Wer diese Probleme nicht hat, tauscht sie gegen neue ein — und die neuen sind komplizierter. Drei Container auf einem Server rechtfertigen keinen Cluster, so gern man das im Lebenslauf stehen hätte.
9. OpenShift
9.1 Was OpenShift zu Kubernetes hinzufügt
OpenShift ist Kubernetes — mit allem, was man ohnehin nachrüsten müsste, bereits integriert, getestet und supportet.
| Bereich | Kubernetes pur | OpenShift |
|---|---|---|
| Installation | kubeadm, viel Handarbeit | geführter Installer, auch für Bare Metal |
| Weboberfläche | Dashboard nachrüsten | vollwertige Konsole eingebaut |
| Zugang von außen | Ingress-Controller wählen | Routes, eingebaut |
| Registry | selbst betreiben | integriert |
| Bauen im Cluster | externe Pipeline | BuildConfig, Source-to-Image |
| Monitoring | Prometheus/Grafana selbst | vorkonfiguriert |
| Logsammlung | ELK/Loki selbst | integrierbar |
| Sicherheitsvorgaben | Pod Security Standards | SCCs, streng voreingestellt |
| CI/CD | selbst wählen | Pipelines (Tekton), GitOps (Argo CD) |
| Aktualisierung | jede Komponente einzeln | Cluster-Update als Ganzes |
| Support | Community | Red Hat |
9.2 Sicherheit ab Werk
Der praktisch spürbarste Unterschied. OpenShift verwendet Security Context Constraints und weist jedem Projekt einen zufälligen UID-Bereich zu. Ein Container läuft dort nicht als UID 1001, sondern etwa als 1000680000 — und zwar ohne dass man das im Image vorhersehen könnte.
Der Klassiker bei der ersten OpenShift-Begegnung: Ein Image, das lokal einwandfrei läuft, startet im Cluster nicht. Ursache ist fast immer, dass es Root voraussetzt oder auf feste UIDs angewiesen ist.
Die Lösung ist kein Ausnahmerecht, sondern ein besseres Image:
- Gruppe 0 (
root-Gruppe) statt fester UID für Dateirechte verwenden chmod -R g=uauf alle Verzeichnisse, in die geschrieben wird- keine Ports unter 1024 belegen
- nicht in
/oder Systemverzeichnisse schreiben
Ein Image, das unter OpenShift läuft, läuft überall. Der umgekehrte Schluss gilt nicht. Insofern ist die Strenge ein kostenloser Qualitätsprüfer.
9.3 Routes, Registry und Source-to-Image
Eine Route ist OpenShifts Antwort auf Ingress, nur einfacher:
oc expose service webserver oc create route edge --service=webserver --hostname=app.firma.de
Source-to-Image (S2I) baut aus Quelltext direkt ein lauffähiges Image, ohne dass jemand ein Containerfile schreibt:
oc new-app python:3.11~https://git.firma.de/team/anwendung.git oc logs -f bc/anwendung
Für Entwickler ist das bemerkenswert bequem: Git-URL rein, laufende Anwendung raus. Der Preis ist ein Stück weniger Kontrolle über den Bauvorgang.
9.4 Operatoren
Ein Operator ist Betriebswissen in Softwareform. Statt eine Datenbank von Hand aufzusetzen, zu sichern und zu aktualisieren, installiert man einen Operator, der genau das übernimmt — inklusive Ausfallsicherheit und Versionswechsel.
oc get csv -n openshift-operators
Der OperatorHub in der Konsole bietet Hunderte davon an. Das ist einer der stärksten Gründe für OpenShift: Betriebswissen, das man sonst mühsam selbst aufbauen müsste, kommt als installierbares Paket.
9.5 Betriebsmodelle
| Variante | Beschreibung | Passt zu |
|---|---|---|
| OpenShift Container Platform | selbst betrieben, eigenes RZ | volle Kontrolle, Datenhoheit |
| ROSA | verwaltet auf AWS | Cloud-Strategie mit AWS |
| ARO | verwaltet auf Azure | Cloud-Strategie mit Azure, gemeinsamer Support Microsoft/Red Hat |
| OpenShift Dedicated | von Red Hat verwaltet | wenig eigenes Personal |
| MicroShift | Edge, ein einzelner Knoten | Filiale, Fahrzeug, Maschine |
| OpenShift Local (CRC) | Notebook, Einzelknoten | Entwicklung und Lernen |
9.6 OpenShift Local zum Ausprobieren
crc setup crc start --memory 16384 --cpus 6 eval $(crc oc-env) oc login -u developer
Braucht mindestens 16 GB RAM und vier CPU-Kerne — auf einem Raspberry Pi wird das nichts. Für Lernzwecke aber der schnellste Weg zu einem echten OpenShift.
9.7 Lizenz- und Kostenbetrachtung
OpenShift wird nach Kernen bzw. Kernpaaren lizenziert. Die Listenpreise liegen im vierstelligen Bereich pro Kernpaar und Jahr, in der Praxis wird verhandelt. Der ehrliche Vergleich lautet nicht „Kubernetes kostenlos gegen OpenShift teuer“, sondern:
- Kubernetes: keine Lizenzkosten, dafür Personalkosten für Integration, Betrieb und Aktualisierung — plus das Risiko, dass dieses Wissen an einzelnen Personen hängt.
- OpenShift: Lizenzkosten, dafür weniger Integrationsaufwand, ein Ansprechpartner bei Problemen und definierte Lebenszyklen.
Ab einer gewissen Clustergröße kippt die Rechnung regelmäßig zugunsten von OpenShift. Darunter selten.
9.8 Wann OpenShift den Aufpreis rechtfertigt
Dafür spricht: regulierte Branchen mit Nachweispflichten (Versicherung, Bank, Gesundheitswesen), bestehende Red-Hat-Landschaft, mehrere Entwicklerteams mit Bedarf an Mandantentrennung, fehlendes Plattform-Team, Anforderung an belastbaren Support.
Dagegen spricht: kleine Umgebungen, ausgeprägtes eigenes Kubernetes-Know-how, starke Cloud-Bindung mit vorhandenem Managed-Angebot, knappes Budget bei überschaubarem Risiko.
10. Die Einordnungsfrage: Welche Umgebung wann
Das wichtigste Kapitel dieses Tutorials. Die Technik ist dokumentiert, die Angemessenheit nicht.
10.1 Die entscheidenden Kriterien
| Kriterium | Frage |
|---|---|
| Anzahl Dienste | 3, 30 oder 300? |
| Verfügbarkeit | Darf es nachts stehen? Ist ein Ausfall teuer? |
| Teamgröße | Wer betreibt das, wenn diese eine Person Urlaub hat? |
| Betriebsreife | Gibt es Monitoring, Backup, Änderungsprozesse? |
| Änderungsrate | Ein Update im Quartal oder zwanzig am Tag? |
| Regulierung | DORA, BaFin, DSGVO, ISO 27001? |
| Budget | Lizenz- und Personalkosten |
Die ehrlichste Frage lautet nicht „Was ist die beste Technik?„, sondern „Was können wir am Donnerstagabend um 22 Uhr noch reparieren, wenn die Kollegin mit dem Wissen im Urlaub ist?“
Diese Frage hat schon mehr Architekturentscheidungen richtig gestellt als jedes Whitepaper.
10.2 Stufe 1 — Raspberry Pi und Heimserver
Lage: 1–10 Dienste, eine Person, Ausfall ist ärgerlich, aber nicht teuer.
Empfehlung: Podman rootless mit Quadlet-Units. Kein Cluster, kein Compose-Wildwuchs.
Warum kein k3s? Man kann. k3s läuft auf einem Pi 4 mit 4 GB. Aber der Cluster verbraucht selbst schon spürbar Speicher und will gepflegt werden — für Pi-hole, Nextcloud und einen Medienserver ist das Aufwand ohne Gegenwert. Wer Kubernetes lernen möchte, ist mit k3s auf dem Pi hingegen bestens bedient. Das ist ein anderes Ziel und ein legitimes.
Zu beachten: ARM-Architektur. Nicht jedes Image gibt es für arm64.
podman run --platform linux/arm64 ... podman manifest inspect docker.io/library/nginx:alpine
10.3 Stufe 2 — Einzelner Firmenserver
Lage: 5–30 Dienste, kleines Team, geplante Wartungsfenster sind akzeptabel, Nachvollziehbarkeit wird verlangt.
Empfehlung: RHEL mit rootless Podman, Quadlet und Git als einzige Quelle der Wahrheit. Genau der Aufbau aus Kapitel 14.
Warum das oft die richtige Antwort ist: Man bekommt Reproduzierbarkeit, Versionierung, Rollback, Trennung der Dienste, saubere Protokollierung und ein prüffähiges Änderungsverfahren — mit Werkzeugen, die jeder Linux-Administrator bereits kennt. Der Sprung zu Kubernetes bringt an dieser Stelle Hochverfügbarkeit, kostet aber ein Vielfaches an Komplexität.
Hochverfügbarkeit ist nicht kostenlos, und sie ist nicht immer nötig. Ein Dienst, der monatlich für zehn Minuten wegen eines Updates steht, hat eine Verfügbarkeit von 99,98 Prozent. Das reicht für die allermeisten internen Anwendungen aus. Die Frage ist nicht „Wollen wir Ausfallsicherheit?„ — natürlich will die jeder — sondern „Was kostet uns eine Stunde Stillstand, und was kostet die Vermeidung?“
10.4 Stufe 3 — Kleiner Cluster
Lage: 20–100 Dienste, Ausfälle sind teuer, es gibt ein Team von mehreren Personen.
Empfehlung: k3s auf 3–5 Knoten, oder ein verwaltetes Kubernetes, falls Cloud in Frage kommt.
Warum jetzt: Ab hier sind die Probleme echt, die Kubernetes löst — ein Serverausfall darf nicht mehr zum Ereignis werden, Rollouts sollen ohne Unterbrechung laufen, mehrere Personen brauchen abgestufte Rechte.
10.5 Stufe 4 — Unternehmens-Rechenzentrum
Lage: 100+ Dienste, mehrere Teams, regulierte Branche, dediziertes Plattform-Team.
Empfehlung: OpenShift oder ein professionell aufgesetztes Kubernetes.
Warum OpenShift hier oft gewinnt: Nachweispflichten. Wenn ein Prüfer wissen will, wer wann welche Version in Produktion gebracht hat, wer Zugriff hat und wie Schwachstellen behandelt werden, liefert OpenShift die Antworten aus dem Karton. Bei selbst gebautem Kubernetes ist das eine Integrationsaufgabe — machbar, aber sie kostet.
10.6 Stufe 5 — Cloud und Hybrid
Lage: Elastische Last, mehrere Standorte oder Anbieter, Cloud-Strategie ist gesetzt.
Empfehlung: Verwaltetes Kubernetes (AKS, EKS, GKE) oder ROSA/ARO.
Zu bedenken: Verwaltete Dienste nehmen Arbeit ab, aber nicht die Verantwortung. Anwendungen, Netzwerkrichtlinien, Rechte, Kosten und Datenschutz bleiben beim Betreiber. Und: Datenübertragung nach außen kostet Geld. Rechnungen mit vierstelligen Beträgen für Egress-Traffic sind in Cloud-Projekten ein wiederkehrendes Motiv.
10.7 Entscheidungsmatrix
| Podman einzeln | Podman + Quadlet + Git | k3s | Kubernetes | OpenShift | |
|---|---|---|---|---|---|
| Dienste | 1–5 | 5–30 | 20–100 | 50+ | 100+ |
| Personen im Betrieb | 1 | 1–3 | 2–5 | 3–8 | 5+ |
| Ausfallsicherheit | nein | eingeschränkt | ja | ja | ja |
| Einarbeitungszeit | Stunden | Tage | Wochen | Monate | Monate |
| Laufender Aufwand | sehr gering | gering | mittel | hoch | mittel (bezahlt) |
| Lizenzkosten | keine | keine | keine | keine | ja |
| Prüffähigkeit | selbst bauen | über Git gut | selbst bauen | selbst bauen | eingebaut |
| Raspberry Pi | ja | ja | ja | nein | nein |
10.8 Typische Fehlentscheidungen
- Kubernetes für drei Container. Der Klassiker. Man tauscht ein einfaches Problem gegen ein kompliziertes und nennt es Fortschritt.
- Compose-Dateien in Produktion. Compose ist ein Entwicklungswerkzeug. Kein Autostart, keine Neustartlogik, keine ordentliche Protokollierung.
- Alles in einen Container. Wer Datenbank, Anwendung und Webserver in ein Image packt, hat eine schlecht wartbare VM gebaut.
- Kein Backup, weil „Container sind ja ersetzbar„. Container schon. Volumes nicht.
latestin Produktion. Man weiß dann nicht, was läuft. Bei einem Vorfall ist das ein echtes Problem.- Die Plattform statt des Problems optimieren. Sechs Monate Cluster-Aufbau, während die Fachabteilung noch immer auf ihre Anwendung wartet.
11. Warum sich Containerisierung lohnt
Bisher ging es um das Wie. Jetzt um das Warum — und zwar mit Beispielen, nicht mit Adjektiven.
11.1 Betrieb: reproduzierbare Auslieferung
Vorher: Eine Anwendung wird auf einen Server ausgerollt. Sie braucht Python 3.9, eine bestimmte Bibliotheksversion und drei Umgebungsvariablen. Dokumentiert ist das in einer Wiki-Seite von 2021. Der Server läuft seit vier Jahren, in dieser Zeit hat niemand mehr genau nachvollziehen können, was alles darauf installiert wurde. Man nennt das einen „gewachsenen Server“, und alle sind sich einig, dass man ihn besser nicht anfasst.
Nachher: Die Anwendung ist ein Image. Das Image enthält Python 3.9 und die exakte Bibliotheksversion. Es wird aus einem Containerfile gebaut, das in Git liegt. Jeder kann es bauen, jeder kann es lesen, und was auf dem Testsystem lief, läuft auch in Produktion — es ist bitweise dasselbe.
11.2 Betrieb: Rollback in Sekunden
# Neue Version podman stop app && podman rm app podman run -d --name app registry.firma.de/app:2.4.0 # Es klemmt. Zurück. podman stop app && podman rm app podman run -d --name app registry.firma.de/app:2.3.8
Zwei Befehle, wenige Sekunden. Zum Vergleich der klassische Weg: Paket zurückrollen, Konfiguration wiederherstellen, Datenbankmigration rückabwickeln, hoffen. Der Unterschied ist nicht graduell.
Der Nebeneffekt ist wichtiger als der Effekt. Wenn ein Rollback fünf Sekunden dauert, verliert das Ausrollen seinen Schrecken. Teams, die schnell zurückkönnen, liefern häufiger aus. Wer häufiger ausliefert, liefert kleinere Änderungen aus. Kleinere Änderungen brechen seltener — und wenn doch, weiß man sofort, welche es war.
Das ist der eigentliche Grund, warum Container die Art verändert haben, wie Software betrieben wird. Nicht die Technik. Die veränderte Risikorechnung.
11.3 Betrieb: Ressourcendichte
Auf einem Server mit 32 GB RAM laufen vielleicht 15 VMs mit je 2 GB — von denen jede rund 500 MB allein für ihr Betriebssystem verbraucht. Macht 7,5 GB Overhead, bevor irgendeine Anwendung eine einzige Anfrage bearbeitet hat.
Dieselben 15 Dienste als Container: 15-mal 50 bis 200 MB. Auf demselben Blech passen statt 15 Diensten problemlos 60. Bei einem Serverpreis von 8.000 Euro ist das kein akademischer Unterschied.
11.4 Entwicklung: identische Umgebung überall
Das Problem: Ein Azubi startet am Montag. Bis die Entwicklungsumgebung steht, vergehen zwei Tage — Datenbank installieren, Version anpassen, Bibliotheken nachziehen, drei Fehlermeldungen googeln, einmal Kollegen fragen.
Die Lösung:
- compose.yaml
services: db: image: docker.io/library/postgres:16 environment: POSTGRES_PASSWORD: entwicklung POSTGRES_DB: appdb volumes: - dbdaten:/var/lib/postgresql/data cache: image: docker.io/library/redis:7-alpine app: build: . ports: - "8080:8080" depends_on: - db - cache volumes: - ./src:/app/src:Z volumes: dbdaten:
podman-compose up -d
Aus zwei Tagen werden zwei Minuten. Und alle im Team haben exakt dieselbe Umgebung — inklusive derselben Postgres-Nebenversion, was genau die Sorte Fehler verhindert, die man sonst erst in Produktion findet.
11.5 Entwicklung: „Läuft bei mir" endgültig beenden
Der Satz „Bei mir funktioniert es aber„ hat eine gewisse Ähnlichkeit mit dem Unendlichkeitsantrieb: Beide erzeugen aus einem unwahrscheinlichen Zustand heraus Ergebnisse, die niemand reproduzieren kann. Der Unterschied ist, dass der Unendlichkeitsantrieb wenigstens unterhaltsam ist.
Mit Containern verschwindet der Satz. Nicht, weil Fehler verschwinden, sondern weil die Umgebung aufhört, eine Variable zu sein. Wenn es im Container klemmt, klemmt es überall — und damit lässt es sich untersuchen.
11.6 Sicherheit: minimale Angriffsfläche
Ein klassischer Anwendungsserver enthält typischerweise: einen SSH-Dienst, einen Cron-Daemon, einen Mailer, Paketverwaltung, Compiler, Editoren, diverse Bibliotheken aus der Grundinstallation — und irgendwo darin die eigentliche Anwendung.
Ein Container aus einem Multi-Stage-Build enthält: die Anwendung und ihre Laufzeitbibliotheken. Sonst nichts. Keine Shell, kein Paketmanager, kein Compiler.
Für einen Angreifer, der Code ausführen kann, ist das ein erheblicher Unterschied. Kein curl zum Nachladen, kein Paketmanager zum Installieren, keine Shell zum Weiterarbeiten. Das verhindert keinen Einbruch, aber es macht die Ausweitung eines Angriffs mühsam — und Mühsal ist im Sicherheitsbereich eine harte Währung.
11.7 Sicherheit: Patchen durch Neubau
# Basis-Image aktualisieren, Image neu bauen, austauschen podman build --pull -t app:2.4.1 . podman push registry.firma.de/app:2.4.1
Der Container ist unveränderlich. Es gibt keine über Jahre gewachsene Konfiguration, keine manuell nachgezogene Bibliothek, keine vergessene Testdatei aus 2019. Jeder Neubau erzeugt einen definierten Zustand.
Für Compliance-Anforderungen ist das erheblich: Die Frage „Läuft auf diesem System noch eine verwundbare Version von Bibliothek X?“ ist bei Containern eine Abfrage gegen die Image-Liste. Auf gewachsenen Servern ist sie eine Suchaktion mit ungewissem Ausgang.
11.8 Nachvollziehbarkeit
podman inspect --format '{{.ImageDigest}}' app
Der Digest beantwortet die Frage „Was läuft hier genau?„ eindeutig — nicht ungefähr, nicht dem Namen nach, sondern kryptografisch belegbar. In Verbindung mit einer signierten Registry und einem Git-Verlauf ergibt das eine lückenlose Kette von der Codeänderung bis zum laufenden Prozess.
In regulierten Umgebungen ist genau diese Kette der Punkt, an dem Prüfungen sonst unangenehm werden.
11.9 Wo Container nicht die richtige Antwort sind
Damit dieses Kapitel keine Werbebroschüre wird:
- Anwendungen mit direktem Hardwarezugriff — spezielle Karten, Echtzeitanforderungen, exotische Treiber.
- Zustandsbehaftete Software mit eigener Clusterlogik — geht, ist aber oft unnötig kompliziert. Eine gut gepflegte Oracle-Instanz auf einer VM ist keine Schande.
- Windows-Anwendungen — Windows-Container existieren, brauchen aber Windows-Hosts und sind ein eigenes Thema.
- Ein einzelner, stabiler Dienst, der seit acht Jahren läuft und nie geändert wird. Wenn nichts wehtut, muss man nichts heilen.
- Teams ohne Linux-Grundlagen. Container setzen Linux-Verständnis voraus, sie ersetzen es nicht. Wer Namespaces, Dateirechte und Netzwerk nicht kennt, wird beim ersten
Permission deniedim Dunkeln stehen.
12. Praxisbeispiele
12.1 Datenbank auf Abruf
# Postgres für zwanzig Minuten, danach spurlos verschwunden podman run -d --rm --name testdb \ -e POSTGRES_PASSWORD=test \ -p 5432:5432 \ docker.io/library/postgres:16 # ... arbeiten ... podman stop testdb # weg, inklusive aller Testdaten
Wer schon einmal eine Datenbank auf seinem Arbeitsrechner installiert und Monate später beim Aufräumen wiedergefunden hat, weiß den Unterschied zu schätzen.
12.2 Reproduzierbare Build-Umgebungen
Ein Projekt braucht GCC 9, ein anderes GCC 13. Früher hieß das: zwei Rechner oder viel Verdruss.
podman run --rm -v "$PWD":/src:Z -w /src docker.io/gcc:9 make podman run --rm -v "$PWD":/src:Z -w /src docker.io/gcc:13 make
Beide Toolchains nebeneinander, keine gegenseitige Beeinflussung, kein Eingriff ins Hostsystem.
12.3 Eine Testumgebung pro Feature-Branch
BRANCH=$(git rev-parse --abbrev-ref HEAD) PORT=$((8000 + $(git rev-list --count HEAD) % 1000)) podman build -t "app:${BRANCH}" . podman run -d --name "app-${BRANCH}" -p "${PORT}:8080" "app:${BRANCH}" echo "Testumgebung: http://localhost:${PORT}"
In CI/CD-Pipelines gebaut, bekommt jeder Merge Request eine eigene, vollständig funktionsfähige Umgebung. Der Fachbereich kann eine Änderung anschauen, bevor sie zusammengeführt wird — statt sie sich anhand von Bildschirmfotos vorzustellen.
12.4 Altanwendungen kontrolliert weiterbetreiben
Es gibt in jedem Unternehmen diese eine Anwendung. Sie ist geschäftskritisch, sie braucht PHP 5.6 und eine Bibliothek, die seit 2016 nicht mehr gepflegt wird, und der Entwickler ist längst woanders. Sie ist die Marvin-Anwendung: niemand mag sie, alle brauchen sie, und sie wird alle überleben.
Ein Container ist hier eine ehrliche Zwischenlösung:
- Containerfile.legacy
FROM docker.io/library/php:5.6-apache COPY anwendung/ /var/www/html/ # Keine Illusionen: das ist ein Käfig, keine Heilung
Die veraltete Umgebung ist eingesperrt, sie verunreinigt keinen aktuellen Server, sie lässt sich mit Netzwerkregeln abschotten und auf einen Reverse Proxy mit aktuellem TLS setzen. Das Sicherheitsproblem ist nicht gelöst — es ist eingegrenzt und wird beherrschbar, bis der Ersatz steht.
12.5 Werkzeuge ohne Installation nutzen
# Schwachstellenscan, ohne Trivy zu installieren podman run --rm docker.io/aquasec/trivy:latest image nginx:alpine # YAML prüfen podman run --rm -v "$PWD":/data:Z docker.io/cytopia/yamllint . # LaTeX kompilieren ohne 4 GB TeX Live auf dem Rechner podman run --rm -v "$PWD":/doc:Z -w /doc docker.io/texlive/texlive \ pdflatex dokument.tex
Der Rechner bleibt sauber, die Werkzeuge sind trotzdem da. Und wenn man sie nicht mehr braucht, verschwinden sie mit einem podman image rm.
12.6 Selbstgehostete Dienste auf dem Raspberry Pi
Ein realistischer Aufbau auf einem Pi 5 mit 8 GB:
| Dienst | Zweck | RAM |
|---|---|---|
| Pi-hole | DNS-Filter | ~80 MB |
| Nextcloud | Dateien und Kalender | ~500 MB |
| Home Assistant | Hausautomation | ~400 MB |
| Vaultwarden | Passwortverwaltung | ~50 MB |
| Jellyfin | Medienserver | ~300 MB |
| Uptime Kuma | Überwachung | ~100 MB |
Alle als Quadlet-Units, alle rootless, alle starten beim Booten. Gesamt etwa 1,5 GB — auf einem Gerät für unter 100 Euro. Vor zehn Jahren wären dafür fünf Rechner nötig gewesen, und das Netzteil hätte mehr gekostet als heute der ganze Pi.
13. Sicherheit im Containerbetrieb
13.1 Rootless als Standard
podman info | grep -i rootless podman unshare cat /proc/self/uid_map
Falls doch einmal rootful nötig ist (etwa für Ports unter 1024 ohne Sysctl-Anpassung oder für bestimmte Netzwerkfunktionen), sollte das eine bewusste, dokumentierte Ausnahme sein — nicht der Standardweg.
13.2 Read-only und Capabilities
podman run -d \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size=64m \ --cap-drop ALL \ --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges \ --user 1001 \ meine-app:1.0
| Option | Wirkung |
|---|---|
–read-only | Dateisystem des Containers nicht beschreibbar |
–tmpfs | gezielt ein beschreibbares Verzeichnis im RAM |
–cap-drop ALL | alle Sonderrechte entziehen |
–security-opt no-new-privileges | kein Rechtezuwachs über setuid-Programme |
–user | feste unprivilegierte UID |
13.3 SELinux und Volumes
# Falsch - führt zu Permission denied podman run -v /srv/daten:/daten nginx # Richtig podman run -v /srv/daten:/daten:Z nginx # exklusiv podman run -v /srv/daten:/daten:z nginx # geteilt # Kontext prüfen ls -Z /srv/daten
–privileged und –security-opt label=disable schalten die Schutzmechanismen ab. Sie tauchen in Anleitungen im Netz auf, weil sie jedes Rechteproblem sofort beseitigen — so, wie das Abklemmen der Feuermelder jedes Fehlalarmproblem beseitigt. Wenn du eine solche Option in einer Anleitung findest, ist das ein Hinweis darauf, dass der Autor das eigentliche Problem nicht gelöst hat.
13.4 Secrets korrekt handhaben
printf 'MeinGeheimesPasswort' | podman secret create db_passwort - podman secret ls podman run -d --name db \ --secret db_passwort,type=env,target=POSTGRES_PASSWORD \ postgres:16
Damit steht das Passwort weder in der Prozessliste noch in podman inspect noch in der Shell-Historie. In Quadlet-Units referenziert man es über Secret=db_passwort,type=env,target=POSTGRES_PASSWORD.
13.5 Netzwerksegmentierung
podman network create --internal backend podman network create frontend # Datenbank: nur im internen Netz, keine Verbindung nach außen podman run -d --name db --network backend postgres:16 # Anwendung: in beiden Netzen podman run -d --name app --network backend,frontend -p 8080:8080 app:1.0
–internal verhindert jede Verbindung ins Internet. Eine kompromittierte Datenbank kann damit nichts nachladen und nichts abfließen lassen.
13.6 Lieferkette
- Images nur aus bekannten Registries beziehen
- Basis-Images auf feste Versionen festnageln, nicht auf
latest - regelmäßig scannen, nicht nur einmal beim Bauen
- in Produktion Digests statt Tags verwenden
- eigene Images signieren
- einen internen Spiegel betreiben, damit man nicht von der Verfügbarkeit fremder Registries abhängt
Ein Image aus dem Internet ist fremder Code, der auf deinem Server Prozesse startet. Ein Image mit 50 Millionen Downloads ist deshalb nicht automatisch vertrauenswürdig — es ist nur beliebt. Wer bei Software aus dem Netz sonst genau hinschaut, sollte das bei Containern erst recht tun. Ein Handtuch bei sich zu haben ist gut; zu wissen, wo es herkommt, ist besser.
13.7 Protokollierung und Auditierbarkeit
# Podman-Ereignisse podman events --since 24h # Container-Logs über journald journalctl --user -u webserver.service --since today # Auditd auf Container-Aktivitäten sudo auditctl -w /usr/bin/podman -p x -k container_exec
Für regulierte Umgebungen sind drei Dinge nachzuweisen: was läuft (Image-Digest), wer es gestartet hat (journald, auditd) und woher es kommt (Registry und Signatur). Alle drei sind mit Bordmitteln erreichbar — man muss sie nur von Anfang an einschalten und nicht erst, wenn die Prüfung angekündigt ist.
14. HowTo: Podman-Server mit Git-Repository unter RHEL
Dieses Kapitel ist bewusst so geschrieben, dass es eigenständig abgearbeitet werden kann. Wer die vorherigen Kapitel übersprungen hat, kommt hier trotzdem durch. Dafür wiederholt es an einigen Stellen Dinge, die vorher schon erklärt wurden.
Zeitbedarf: etwa zwei Stunden. Voraussetzung: RHEL 9 (oder AlmaLinux/Rocky 9), Root-Zugang, ein erreichbarer Git-Server.
14.1 Zielbild
Am Ende steht ein Server, auf dem gilt:
- Container laufen rootless unter einem eigenen Dienstbenutzer.
- Die einzige Quelle der Wahrheit ist ein Git-Repository. Was dort nicht steht, existiert nicht.
- Container sind systemd-Dienste — sie starten beim Booten und werden bei Absturz neu gestartet.
- Ein Deployment ist ein Skriptaufruf: auschecken, bauen, neu starten.
- Alles landet in journald und ist damit nachvollziehbar.
- Ein Rollback ist ein
git checkoutplus Neustart.
Git-Server (GitLab/Gitea/GitHub)
│ git pull (Deploy-Key, nur lesen)
▼
┌──────────────────────────────────────────────┐
│ RHEL 9 — Benutzer: conops (kein Root) │
│ │
│ ~/repo/ ← Git-Arbeitskopie │
│ ~/.config/containers/systemd/ ← Quadlet │
│ ~/.local/share/containers/ ← Images │
│ │
│ systemd --user │
│ ├── webapp.service (Container) │
│ ├── datenbank.service (Container) │
│ └── podman-auto-update.timer │
│ │
│ firewalld ── 80/443 ──► Reverse Proxy │
└──────────────────────────────────────────────┘
14.2 RHEL 9 vorbereiten
# Als root dnf update -y dnf install -y \ podman \ buildah \ skopeo \ git-core \ policycoreutils-python-utils \ firewalld systemctl enable --now firewalld # Versionen prüfen podman --version git --version
Hinweis: Podman 4.4 oder neuer ist Pflicht, sonst gibt es kein Quadlet. RHEL 9.2 und aufwärts liefern das mit. Bei älteren Ständen hilft dnf update podman.
14.3 Dienstbenutzer anlegen
# Als root useradd --create-home --shell /bin/bash --comment "Container-Betrieb" conops # subuid/subgid prüfen - RHEL 9 vergibt sie normalerweise automatisch grep conops /etc/subuid /etc/subgid # Erwartet etwa: conops:100000:65536
Falls die Einträge fehlen:
usermod --add-subuids 100000-165535 --add-subgids 100000-165535 conops loginctl terminate-user conops 2>/dev/null || true su - conops -c 'podman system migrate'
Lingering aktivieren — ohne das funktioniert nichts. Standardmäßig beendet systemd alle Benutzerdienste, sobald sich der Benutzer abmeldet. Deine Container würden also beim Ausloggen sterben und nach einem Neustart nicht wiederkommen.
Das ist mit Abstand der häufigste Fehler bei diesem Aufbau. Wenn nach einem Reboot alles weg ist: hier nachsehen.
loginctl enable-linger conops loginctl show-user conops | grep Linger # Linger=yes
14.4 Podman rootless einrichten und prüfen
Ab jetzt wird als conops gearbeitet. Wichtig ist eine vollwertige Anmeldesitzung:
# Als root - saubere Sitzung öffnen machinectl shell conops@ # Alternativ per SSH direkt als conops anmelden
Achtung: Ein einfaches su - conops liefert keine korrekte D-Bus-Sitzung, und systemctl –user scheitert dann mit „Failed to connect to bus“. Falls du dennoch mit su arbeitest:
export XDG_RUNTIME_DIR=/run/user/$(id -u) export DBUS_SESSION_BUS_ADDRESS=unix:path=${XDG_RUNTIME_DIR}/bus
Funktionsprüfung:
podman info --format '{{.Host.Security.Rootless}}' # true podman run --rm registry.access.redhat.com/ubi9/ubi echo "Es funktioniert." # UID-Abbildung ansehen podman unshare cat /proc/self/uid_map systemctl --user status
14.5 Git einrichten und Deploy-Key hinterlegen
Der Server soll nur lesen dürfen. Ein Deploy-Key mit Lesezugriff ist dafür genau das richtige Mittel.
# Als conops ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -N "" -C "conops@$(hostname -s)" cat ~/.ssh/deploy_key.pub
Den öffentlichen Schlüssel im Git-Server hinterlegen:
- GitLab: Projekt → Settings → Repository → Deploy Keys (ohne Schreibrecht)
- Gitea/Forgejo: Repository → Einstellungen → Deploy-Schlüssel
- GitHub: Repository → Settings → Deploy keys (Häkchen „Allow write access„ nicht setzen)
SSH-Konfiguration:
- ~/.ssh/config
Host git.firma.de HostName git.firma.de User git IdentityFile ~/.ssh/deploy_key IdentitiesOnly yes
chmod 600 ~/.ssh/config ~/.ssh/deploy_key ssh -T git@git.firma.de # Begrüßung erwartet
14.6 Aufbau des Repositories
container-betrieb/
├── README.md
├── quadlet/
│ ├── webapp.container
│ ├── datenbank.container
│ ├── appdaten.volume
│ └── appnetz.network
├── images/
│ └── webapp/
│ ├── Containerfile
│ └── src/
├── config/
│ └── nginx.conf
├── scripts/
│ ├── deploy.sh
│ └── rollback.sh
└── docs/
└── betriebshandbuch.md
Die Quadlet-Dateien:
- quadlet/appnetz.network
[Unit] Description=Internes Netzwerk der Anwendung [Network] NetworkName=appnetz Subnet=10.89.10.0/24
- quadlet/appdaten.volume
[Unit] Description=Persistente Daten der Datenbank [Volume] VolumeName=appdaten
- quadlet/datenbank.container
[Unit] Description=PostgreSQL Datenbank After=network-online.target Requires=appdaten-volume.service appnetz-network.service [Container] Image=docker.io/library/postgres:16.3 ContainerName=datenbank Network=appnetz.network Volume=appdaten.volume:/var/lib/postgresql/data:Z Environment=POSTGRES_DB=appdb Environment=POSTGRES_USER=app Secret=db_passwort,type=env,target=POSTGRES_PASSWORD HealthCmd=pg_isready -U app -d appdb HealthInterval=30s HealthRetries=3 Memory=1g AutoUpdate=registry [Service] Restart=always TimeoutStartSec=120 [Install] WantedBy=default.target
- quadlet/webapp.container
[Unit] Description=Webanwendung After=datenbank.service Requires=datenbank.service [Container] Image=localhost/webapp:aktuell ContainerName=webapp Network=appnetz.network PublishPort=8080:8080 Environment=DB_HOST=datenbank Environment=DB_NAME=appdb Environment=DB_USER=app Secret=db_passwort,type=env,target=DB_PASSWORD Volume=%h/repo/config/nginx.conf:/etc/nginx/nginx.conf:ro,Z HealthCmd=curl -f http://localhost:8080/health || exit 1 HealthInterval=30s ReadOnly=true Tmpfs=/tmp:rw,size=64m NoNewPrivileges=true DropCapability=ALL AddCapability=NET_BIND_SERVICE Memory=512m [Service] Restart=always [Install] WantedBy=default.target
Hinweis zu den Namen: Quadlet erzeugt aus webapp.container den Dienst webapp.service, aus appdaten.volume den Dienst appdaten-volume.service und aus appnetz.network den Dienst appnetz-network.service. Diese Namen braucht man in Requires= und After=. Das %h in Pfaden ist eine systemd-Ersetzung für das Heimatverzeichnis.
14.7 Repository auschecken
# Als conops cd ~ git clone git@git.firma.de:betrieb/container-betrieb.git repo cd ~/repo mkdir -p ~/.config/containers/systemd
Die Quadlet-Dateien werden verlinkt, nicht kopiert. So wirkt ein git pull unmittelbar:
for f in ~/repo/quadlet/*.container ~/repo/quadlet/*.volume ~/repo/quadlet/*.network; do ln -sf "$f" ~/.config/containers/systemd/ done ls -l ~/.config/containers/systemd/
Erzeugte Units kontrollieren, bevor irgendetwas startet:
/usr/libexec/podman/quadlet -dryrun -user
14.8 Secret anlegen und Image bauen
# Datenbankpasswort als Podman-Secret read -rsp "DB-Passwort: " pw && printf '%s' "$pw" | podman secret create db_passwort - && unset pw podman secret ls # Image bauen cd ~/repo/images/webapp podman build -t localhost/webapp:$(git rev-parse --short HEAD) . podman tag localhost/webapp:$(git rev-parse --short HEAD) localhost/webapp:aktuell podman images
Hinweis: Der Tag aktuell ist ein beweglicher Zeiger, den die Quadlet-Unit referenziert. Der Commit-Tag daneben bleibt erhalten und ermöglicht den Rollback aus 14.12. So muss man die Unit-Datei bei jedem Deployment nicht anfassen.
14.9 Container als systemd-Dienst starten
systemctl --user daemon-reload systemctl --user start datenbank.service systemctl --user start webapp.service systemctl --user status webapp.service podman ps
Quadlet-Units werden nicht mit systemctl enable aktiviert. Sie sind zur Laufzeit erzeugt, und generierte Units lassen sich nicht aktivieren. Der Autostart kommt ausschließlich aus dem Abschnitt
[Install] WantedBy=default.target
in der .container-Datei. Nach daemon-reload ist das erledigt. Wer hier systemctl –user enable webapp.service versucht, bekommt eine Fehlermeldung und sucht anschließend lange an der falschen Stelle.
Neustart-Test — der einzige Test, der zählt:
sudo reboot # nach dem Hochfahren, als conops: systemctl --user status webapp.service podman ps
14.10 Persistente Daten und SELinux
Für Bind Mounts außerhalb des Heimatverzeichnisses setzt man den SELinux-Kontext dauerhaft:
# Als root mkdir -p /srv/containerdaten chown -R conops:conops /srv/containerdaten semanage fcontext -a -t container_file_t "/srv/containerdaten(/.*)?" restorecon -Rv /srv/containerdaten ls -Z /srv/containerdaten
In der Quadlet-Unit dann:
Volume=/srv/containerdaten/uploads:/app/uploads:Z
Hinweis: Bei rootless-Podman gehören die Dateien auf dem Host der abgebildeten UID, nicht der Container-UID. Sieht man Dateien mit Besitzer 100999 — das ist normal und richtig. Für Zugriff aus dem Container hilft podman unshare:
podman unshare chown -R 1001:1001 /srv/containerdaten/uploads
14.11 Netzwerk, Firewall und Reverse Proxy
Rootless-Container dürfen standardmäßig keine Ports unter 1024 belegen. Es gibt zwei Wege.
Weg A — Sysctl anpassen (einfach, für kleine Aufbauten):
# Als root echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-rootless-ports.conf sysctl --system
Weg B — Reverse Proxy als root-Dienst (empfohlen, weil TLS und Zertifikate zentral liegen):
# Als root dnf install -y nginx
- /etc/nginx/conf.d/webapp.conf
server { listen 443 ssl http2; server_name app.firma.de; ssl_certificate /etc/pki/tls/certs/app.firma.de.crt; ssl_certificate_key /etc/pki/tls/private/app.firma.de.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
# Als root setsebool -P httpd_can_network_connect 1 # sonst blockt SELinux den Proxy systemctl enable --now nginx firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reload firewall-cmd --list-all
Achtung: httpd_can_network_connect ist der SELinux-Schalter, an dem Reverse-Proxy-Aufbauten unter RHEL regelmäßig scheitern. Ohne ihn liefert nginx ein 502 Bad Gateway, obwohl die Anwendung einwandfrei läuft.
14.12 Deployment-Skript
- scripts/deploy.sh
#!/usr/bin/env bash # # Deployment: Repository aktualisieren, Image bauen, Dienste neu starten. # Aufruf: ./scripts/deploy.sh [branch] # set -euo pipefail REPO="${HOME}/repo" BRANCH="${1:-main}" LOG="${HOME}/deploy.log" log() { printf '%(%F %T)T %s\n' -1 "$*" | tee -a "$LOG"; } log "=== Deployment gestartet (Branch: ${BRANCH}) ===" # --- 1. Stand sichern, um zurückrollen zu koennen --- cd "$REPO" ALT_COMMIT="$(git rev-parse --short HEAD)" log "Aktueller Stand: ${ALT_COMMIT}" # --- 2. Repository aktualisieren --- git fetch --all --prune git checkout "$BRANCH" git pull --ff-only NEU_COMMIT="$(git rev-parse --short HEAD)" if [[ "$ALT_COMMIT" == "$NEU_COMMIT" ]]; then log "Keine Aenderungen. Deployment beendet." exit 0 fi log "Neuer Stand: ${NEU_COMMIT}" # --- 3. Quadlet-Verknuepfungen auffrischen --- mkdir -p "${HOME}/.config/containers/systemd" find "${HOME}/.config/containers/systemd" -type l -delete for f in "${REPO}"/quadlet/*.{container,volume,network,pod}; do [[ -e "$f" ]] && ln -sf "$f" "${HOME}/.config/containers/systemd/" done # --- 4. Image bauen --- log "Baue Image ${NEU_COMMIT} ..." podman build --pull \ -t "localhost/webapp:${NEU_COMMIT}" \ "${REPO}/images/webapp" podman tag "localhost/webapp:${NEU_COMMIT}" localhost/webapp:aktuell # --- 5. Dienste neu starten --- systemctl --user daemon-reload systemctl --user restart webapp.service # --- 6. Zustand pruefen --- log "Warte auf Gesundheitsstatus ..." for i in {1..30}; do if podman healthcheck run webapp &>/dev/null; then log "Deployment erfolgreich: ${ALT_COMMIT} -> ${NEU_COMMIT}" podman image prune -f &>/dev/null || true exit 0 fi sleep 2 done # --- 7. Automatischer Rollback --- log "FEHLER: Gesundheitspruefung fehlgeschlagen. Rollback auf ${ALT_COMMIT}." podman tag "localhost/webapp:${ALT_COMMIT}" localhost/webapp:aktuell git checkout "$ALT_COMMIT" systemctl --user daemon-reload systemctl --user restart webapp.service log "Rollback abgeschlossen." exit 1
chmod +x ~/repo/scripts/deploy.sh ~/repo/scripts/deploy.sh main
Das ist der eigentliche Gewinn dieses Aufbaus. Ein Deployment ist ein Skriptaufruf. Es ist wiederholbar, es protokolliert sich selbst, und es rollt bei einem Fehlschlag automatisch zurück. Der Git-Verlauf beantwortet lückenlos, wer wann was in Produktion gebracht hat.
Damit hast du auf einem einzelnen Server ohne Cluster, ohne Lizenzkosten und ohne neue Werkzeuge das erreicht, was anderswo unter „GitOps“ firmiert — mit git, systemd und einem Shell-Skript.
14.13 Automatische Aktualisierung
Für Container, die aus einer Registry kommen (etwa die Datenbank), kann Podman selbstständig auf neue Images prüfen:
systemctl --user enable --now podman-auto-update.timer systemctl --user list-timers # Probelauf ohne Änderung podman auto-update --dry-run # Manuell auslösen podman auto-update
Voraussetzung ist AutoUpdate=registry in der Quadlet-Unit. Schlägt der Start des neuen Images fehl, rollt Podman selbstständig auf das vorherige zurück.
Achtung: Für selbst gebaute Images ist das der falsche Weg — die kommen über deploy.sh. Und in kritischen Umgebungen möchte man Aktualisierungen bewusst auslösen statt sie nachts automatisch geschehen zu lassen. Der Timer ist bequem, aber Bequemlichkeit und Kontrolle sind hier gegenläufig.
14.14 Protokollierung und Zustandsprüfung
# Logs eines Dienstes journalctl --user -u webapp.service -f journalctl --user -u webapp.service --since "1 hour ago" # Alle Container-Dienste journalctl --user -t webapp --since today # Gesundheitsstatus podman healthcheck run webapp podman inspect webapp --format '{{.State.Health.Status}}' # Ressourcenverbrauch podman stats --no-stream
Ein einfaches Überwachungsskript für Cron oder einen systemd-Timer:
- scripts/check.sh
#!/usr/bin/env bash set -uo pipefail FEHLER=0 for dienst in datenbank webapp; do if ! systemctl --user is-active --quiet "${dienst}.service"; then echo "KRITISCH: ${dienst}.service laeuft nicht" FEHLER=1 continue fi status="$(podman inspect "$dienst" --format '{{.State.Health.Status}}' 2>/dev/null || echo unbekannt)" if [[ "$status" != "healthy" && "$status" != "unbekannt" ]]; then echo "WARNUNG: ${dienst} ist ${status}" FEHLER=1 fi done exit $FEHLER
14.15 Sicherung und Wiederherstellung
Zu sichern sind drei Dinge — und nur drei:
| Was | Wie | Warum |
|---|---|---|
| Git-Repository | auf dem Git-Server | enthält die gesamte Konfiguration |
| Volumes | podman volume export | die eigentlichen Daten |
| Secrets | getrennt und verschlüsselt | nicht im Repository |
- scripts/backup.sh
#!/usr/bin/env bash set -euo pipefail ZIEL="/srv/backup/container" DATUM="$(date +%F)" mkdir -p "${ZIEL}/${DATUM}" # Datenbank konsistent sichern podman exec datenbank pg_dump -U app appdb \ | gzip > "${ZIEL}/${DATUM}/appdb.sql.gz" # Volumes for vol in $(podman volume ls -q); do podman volume export "$vol" | gzip > "${ZIEL}/${DATUM}/${vol}.tar.gz" done # Liste der laufenden Images samt Digest - fuer die Nachweispflicht podman ps --format '{{.Names}} {{.Image}} {{.ImageID}}' \ > "${ZIEL}/${DATUM}/laufende-images.txt" # Aufbewahrung: 30 Tage find "$ZIEL" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
Eine Datenbank sichert man nicht durch Kopieren ihres Volumes im laufenden Betrieb. Das Ergebnis ist ein inkonsistenter Stand, der beim Zurückspielen mit einiger Wahrscheinlichkeit nicht startet — und das merkt man erfahrungsgemäß genau dann, wenn man ihn braucht. Für Datenbanken immer den mitgelieferten Dump-Mechanismus verwenden (pg_dump, mysqldump, mongodump).
Und: Eine Sicherung, die nie zurückgespielt wurde, ist keine Sicherung. Sie ist eine Hoffnung mit Speicherverbrauch.
Wiederherstellung auf einem neuen Server:
# 1. Kapitel 14.2 bis 14.5 erneut abarbeiten # 2. Repository auschecken git clone git@git.firma.de:betrieb/container-betrieb.git ~/repo # 3. Secrets neu anlegen # 4. Volumes zurückspielen gunzip -c appdaten.tar.gz | podman volume import appdaten - # 5. Starten ~/repo/scripts/deploy.sh main
14.16 Die zehn wichtigsten Befehle im Alltag
# 1. Was läuft? podman ps # 2. Zustand eines Dienstes systemctl --user status webapp.service # 3. Logs verfolgen journalctl --user -u webapp.service -f # 4. Neu starten systemctl --user restart webapp.service # 5. Deployment ~/repo/scripts/deploy.sh main # 6. Ressourcenverbrauch podman stats --no-stream # 7. In den Container schauen (nur zur Diagnose!) podman exec -it webapp /bin/sh # 8. Speicherplatz prüfen podman system df # 9. Aufräumen podman image prune -f # 10. Rollback auf einen bekannten Stand cd ~/repo && git checkout <commit> && systemctl --user daemon-reload \ && systemctl --user restart webapp.service
14.17 Fehlersuche
| Symptom | Wahrscheinliche Ursache | Prüfung |
|---|---|---|
| Nach Reboot läuft nichts | Lingering fehlt | loginctl show-user conops \| grep Linger |
Failed to connect to bus | keine echte Sitzung | machinectl shell conops@ statt su |
Unit not found nach daemon-reload | Quadlet-Datei fehlerhaft | /usr/libexec/podman/quadlet -dryrun -user |
Permission denied bei Volumes | SELinux-Kontext | ls -Z, Suffix Z ergänzen |
| Port kann nicht belegt werden | Port unter 1024 | Sysctl oder Reverse Proxy |
| Container startet und stirbt sofort | Prozess beendet sich | podman logs <name> |
502 Bad Gateway vom Proxy | SELinux-Boolean | getsebool httpd_can_network_connect |
| Container erreichen sich nicht | falsches Netzwerk | podman network inspect appnetz |
no space left on device | alte Images | podman system df, dann prune |
systemctl enable scheitert | Quadlet-Unit ist generiert | [Install]-Abschnitt nutzen |
| Image wird nicht aktualisiert | Tag zeigt auf alten Stand | podman pull bzw. build –pull |
Ein Diagnoseskript für den Ernstfall:
#!/usr/bin/env bash echo "=== Podman ==="; podman --version; podman info --format '{{.Host.Security.Rootless}}' echo "=== Lingering ==="; loginctl show-user "$USER" | grep -i linger echo "=== Dienste ==="; systemctl --user list-units 'podman*' '*.service' --no-pager | head -30 echo "=== Container ==="; podman ps -a echo "=== Quadlet ==="; /usr/libexec/podman/quadlet -dryrun -user 2>&1 | head -40 echo "=== Speicher ==="; podman system df echo "=== SELinux ==="; getenforce; ausearch -m AVC -ts recent 2>/dev/null | tail -20 echo "=== Git ==="; git -C "$HOME/repo" log --oneline -5
15. Ausblick
15.1 Von Podman zu Kubernetes
Wenn der eine Server irgendwann nicht mehr reicht, ist der Weg kürzer als gedacht:
podman kube generate webapp datenbank > k8s-anwendung.yaml podman kube play k8s-anwendung.yaml kubectl apply -f k8s-anwendung.yaml
Das Ergebnis ist keine produktionsreife Kubernetes-Konfiguration — Ressourcenanforderungen, Ausfallsicherheit und Speicherklassen muss man nacharbeiten. Aber als Ausgangspunkt und als Lernhilfe ist es ausgezeichnet: Man sieht unmittelbar, wie sich die eigene Podman-Konfiguration in Kubernetes-Objekte übersetzt.
15.2 GitOps
Der Aufbau aus Kapitel 14 ist GitOps im Kleinen: Git ist die Quelle der Wahrheit, ein Automatismus stellt den beschriebenen Zustand her. In Kubernetes übernehmen das Argo CD oder Flux — mit dem Zusatz, dass sie permanent nachprüfen und Abweichungen selbstständig korrigieren.
15.3 Container in CI/CD
- .gitlab-ci.yml
stages: [build, test, deploy] build: stage: build image: quay.io/podman/stable script: - podman build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" . - podman login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - podman push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" scan: stage: test script: - trivy image --exit-code 1 --severity HIGH,CRITICAL \ "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
Buildah und Podman sind hier im Vorteil: Sie bauen Images ohne privilegierten Läufer und ohne Daemon — anders als der klassische Docker-in-Docker-Aufbau, der einen privilegierten Container erfordert.
15.4 Weiterführendes
| Thema | Quelle |
|---|---|
| Podman | man podman, docs.podman.io |
| Quadlet | man podman-systemd.unit — die beste verfügbare Referenz |
| Kubernetes | kubernetes.io/docs, Interactive Tutorials |
| OpenShift | docs.redhat.com, Developer Sandbox (kostenlos) |
| Sicherheit | CIS Benchmarks für Docker und Kubernetes |
| Zertifizierung | Red Hat EX188 (Containers), EX280 (OpenShift), CKA/CKAD |
Der praktischste Tipp zum Schluss: man podman-systemd.unit ist die vollständige Referenz aller Quadlet-Optionen und deutlich aktueller als das meiste, was man im Netz findet. Container-Anleitungen im Internet altern schnell — die Handbuchseite auf dem eigenen System passt immer zur installierten Version.
Anhang A: Befehls-Spickzettel Docker ↔ Podman
| Aufgabe | Docker | Podman |
|---|---|---|
| Container starten | docker run | podman run |
| Liste | docker ps -a | podman ps -a |
| Logs | docker logs -f | podman logs -f |
| Shell | docker exec -it | podman exec -it |
| Image bauen | docker build -t | podman build -t oder buildah bud |
| Images | docker images | podman images |
| Herunterladen | docker pull | podman pull |
| Hochladen | docker push | podman push |
| Anmelden | docker login | podman login |
| Netzwerk anlegen | docker network create | podman network create |
| Volume anlegen | docker volume create | podman volume create |
| Aufräumen | docker system prune | podman system prune |
| Details | docker inspect | podman inspect |
| Compose | docker compose up | podman-compose up |
| Autostart | –restart=always | Quadlet + systemd |
| Pods | — | podman pod create |
| Registry-Werkzeug | — | skopeo |
| Kubernetes-Export | — | podman kube generate |
Anhang B: Podman ↔ Kubernetes ↔ OpenShift
| Podman | Kubernetes | OpenShift | |
|---|---|---|---|
| Umfang | ein Host | Cluster | Cluster mit Zubehör |
| Kleinste Einheit | Container/Pod | Pod | Pod |
| Konfiguration | CLI, Quadlet | YAML-Objekte | YAML + eigene Objekte |
| Autostart | systemd | Controller | Controller |
| Lastverteilung | nein | Service | Service + Route |
| Ausfallsicherheit | nein | ja | ja |
| Skalierung | manuell | automatisch | automatisch |
| Weboberfläche | Podman Desktop | Dashboard nachrüsten | eingebaut |
| Rechteverwaltung | Unix-Rechte | RBAC | RBAC + SCC |
| Registry | extern | extern | integriert |
| CI/CD | extern | extern | Pipelines integriert |
| Einarbeitung | Stunden | Monate | Monate |
| Lizenzkosten | keine | keine | ja |
| Raspberry Pi | ja | k3s | nein |
Anhang C: Entscheidungsbaum
Wie viele Dienste?
│
├─ 1–5 ─────────────────────────► Podman + Quadlet
│ (Kapitel 14, verkürzt)
│
├─ 5–30 ─┬─ Ausfall verkraftbar? ─ ja ──► Podman + Quadlet + Git
│ │ (Kapitel 14 vollständig)
│ └──────────────────────── nein ─► k3s (3 Knoten)
│
├─ 30–100 ─┬─ eigenes Plattform-Team? ─ ja ──► Kubernetes
│ └─────────────────────────── nein ─► OpenShift oder Managed
│
└─ 100+ ─┬─ reguliert / Nachweispflicht? ─ ja ──► OpenShift
├─ Cloud-Strategie gesetzt? ────── ja ──► AKS/EKS/GKE, ROSA/ARO
└───────────────────────────────── sonst ► Kubernetes, professionell betrieben
Querfragen, die die Antwort kippen können:
* Wer betreibt das im Urlaub? → eine Stufe kleiner wählen
* Gibt es Monitoring und Backup schon? → sonst zuerst das bauen
* Ist Hochverfügbarkeit wirklich gefordert
oder nur gewünscht? → ehrlich beantworten
* Wie oft wird ausgeliefert? → selten = kleinere Stufe genügt
Anhang D: Vorlagen zum Abtippen
D.1 Containerfile (Python, unprivilegiert, OpenShift-tauglich)
- Containerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.4 LABEL maintainer="betrieb@firma.de" \ io.containers.autoupdate="registry" RUN microdnf install -y python3 python3-pip shadow-utils && \ microdnf clean all && \ rm -rf /var/cache/* WORKDIR /app COPY requirements.txt . RUN pip3 install --no-cache-dir -r requirements.txt COPY src/ ./src/ # Gruppe 0 statt fester UID -> laeuft auch unter OpenShift RUN chgrp -R 0 /app && chmod -R g=u /app USER 1001 EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" ENTRYPOINT ["python3"] CMD ["src/app.py"]
D.2 Quadlet-Unit (Minimalvariante)
- ~/.config/containers/systemd/dienst.container
[Unit] Description=Beispieldienst After=network-online.target [Container] Image=registry.firma.de/team/dienst:1.0 ContainerName=dienst PublishPort=8080:8080 Volume=dienst-daten.volume:/data:Z Environment=LOG_LEVEL=info Memory=512m ReadOnly=true Tmpfs=/tmp:rw,size=64m NoNewPrivileges=true DropCapability=ALL AutoUpdate=registry [Service] Restart=always TimeoutStartSec=90 [Install] WantedBy=default.target
Anhang E: Glossar
| Begriff | Bedeutung |
|---|---|
| Bind Mount | Host-Verzeichnis wird in den Container eingehängt |
| Buildah | Werkzeug zum Bauen von Images ohne Daemon |
| Capability | einzelnes Teilrecht aus der Allmacht von Root |
| cgroup | Kernel-Funktion zur Ressourcenbegrenzung |
| CNI / CNCF | Netzwerk-Schnittstelle / Cloud Native Computing Foundation |
| conmon | Aufpasserprozess für einen Podman-Container |
| CRI-O | schlanke Container-Laufzeit für Kubernetes |
| Digest | kryptografische Prüfsumme, identifiziert ein Image eindeutig |
| Image | unveränderliche Vorlage, aus der Container entstehen |
| Ingress / Route | HTTP-Zugang von außen in Kubernetes bzw. OpenShift |
| Namespace (Kernel) | isolierte Sicht eines Prozesses auf eine Ressource |
| Namespace (Kubernetes) | logische Unterteilung eines Clusters |
| OCI | Open Container Initiative, Standardisierungsgremium |
| Operator | Software, die Betriebswissen für einen Dienst automatisiert |
| Pod | Gruppe von Containern mit gemeinsamem Netzwerk |
| Quadlet | systemd-Integration für Podman-Container |
| Registry | Server zur Verteilung von Images |
| Rootless | Containerbetrieb ohne Root-Rechte |
| runc / crun | die eigentliche Container-Laufzeit |
| SCC | Security Context Constraints — OpenShifts Sicherheitsvorgaben |
| Skopeo | Werkzeug für Registry-Operationen ohne lokalen Download |
| UBI | Universal Base Image von Red Hat, frei verwendbar |
| Volume | von der Container-Laufzeit verwalteter Datenspeicher |
Ende des Tutorials. Und denk daran: Wer ein Handtuch dabei hat, weiß, wo seine Volumes liegen — und hat vermutlich auch ein Backup davon.
