Der Spickzettel zur ersten Hälfte des Seminars Docker, Podman und Kubernetes für Entwickler: Wie ein Image entsteht, warum der Cache greift oder nicht, was rootless anders macht, wie ein Compose-Aufbau verlässlich hochfährt und woran eine Lieferkette nachweisbar wird. Alles, was danach im Cluster passiert, steht auf dem zweiten Blatt: Kubernetes im Betrieb.
Maßgeblich für Flags und Versionsdetails ist die Dokumentation der jeweiligen Werkzeuge; dieses Blatt trifft eine Auswahl — die Entscheidungsraster, die einen Versionssprung überleben.
Dockerfile-Anweisungen
| Anweisung | Wirkung |
|---|---|
| FROM | wählt das Basis-Image und eröffnet eine Bau-Etappe |
| WORKDIR | setzt das Arbeitsverzeichnis für alle folgenden Zeilen |
| COPY | kopiert Dateien aus dem Build-Kontext ins Image |
| ADD | wie COPY, entpackt zusätzlich Archive und lädt URLs |
| RUN | führt einen Befehl beim Bauen aus und schreibt das Ergebnis fest |
| EXPOSE | dokumentiert den Port, öffnet ihn aber nicht |
| USER | legt fest, unter welcher Kennung der Prozess später läuft |
| ENTRYPOINT | bestimmt das Programm, das beim Start ausgeführt wird |
| CMD | liefert die Vorgabe-Argumente, die überschrieben werden dürfen |
ADD nur für Archive, sonst COPY — das ist die vorhersagbarere Wahl. ENTRYPOINT bleibt beim Start stehen, CMD wird von jedem Argument überschrieben; in der Klammer-Schreibweise startet der Prozess direkt und empfängt Stopp-Signale, ohne Klammern hängt eine Shell davor, die das Signal verschluckt.
Modul: Container- und Image-Grundlagen
Was den Cache ungültig macht
| Änderung | Folge für den Cache |
|---|---|
| Basis-Image zeigt auf eine neue Fassung | alles wird neu gebaut |
| Text einer RUN-Zeile geändert | ab dieser Zeile neu |
| Datei im COPY-Ziel geändert | ab diesem COPY neu |
| Datei geändert, die kein COPY erfasst | kein Einfluss |
| Reihenfolge zweier Anweisungen getauscht | ab der ersten Abweichung neu |
Der Zeitstempel einer Datei zählt nicht, nur ihr Inhalt. Daraus folgt die Sortierregel: Abhängigkeiten nach vorn, Quellcode nach hinten.
# selten geändert - liegt vorn und bleibt im Cache
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# oft geändert - liegt hinten
COPY src ./src
Der Build-Kontext wandert vollständig zur Bau-Maschine, bevor die erste
Anweisung läuft — eine .dockerignore ist deshalb kein Kosmetikschritt.
Modul: Container- und Image-Grundlagen
Basis-Images für die letzte Etappe
| Basis | Einordnung |
|---|---|
| Vollbild | vollständige Distribution, bequem, groß |
| Schlank | dieselbe Distribution ohne Zusatzpakete |
| Alpine | sehr klein, andere Standardbibliothek, gelegentlich Überraschungen |
| Distroless | nur Laufzeit, keine Shell, schwer zu untersuchen |
| Scratch | leer, nur für Programme ohne jede Abhängigkeit |
Ohne Shell lässt sich nichts mehr im laufenden Container nachsehen — das ist Absicht und Preis zugleich. Der Multi-Stage-Schnitt liefert das Ergebnis der ersten Etappe in eine schlanke zweite:
FROM node:22 AS build
RUN npm ci && npm run build
FROM node:22-alpine
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
Modul: Container- und Image-Grundlagen
Tag oder Digest
| Eigenschaft | Tag | Digest |
|---|---|---|
| Lesbar | ja | nein |
| Beweglich | ja | nein |
| Eignung zum Veröffentlichen | gut | unhandlich |
| Eignung im Deployment | riskant | richtig |
| Prüfbar beim Herunterladen | nein | ja |
Faustregel: mit Tags veröffentlichen, mit Digests deployen. Der Digest kommt aus dem Push und gehört in die Pipeline-Variable, nicht in eine Notiz:
docker inspect --format '{{index .RepoDigests 0}}' \
registry.example/api:1.4.0
Der OCI-Standard regelt das in drei Spezifikationen: Image (Aufbau aus Schichten, Konfiguration, Manifest), Runtime (wie daraus ein Prozess wird) und Distribution (wie Registries Images annehmen und ausliefern). Deshalb sind Werkzeuge und Registries einzeln austauschbar.
Modul: Container- und Image-Grundlagen
BuildKit: Cache-Mounts und Architekturen
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example/api:1.4.0 --push .
Der Zwischenspeicher liegt neben dem Image, nicht darin — er vergrößert das Ergebnis nicht. Das Ergebnis eines Mehr-Architektur-Laufs ist eine Manifest-Liste; beim Herunterladen wählt die Maschine ihre Architektur selbst. Geheimnisse gehören dabei nie in ein Bau-Argument, wo sie in der Bau-Historie sichtbar bleiben.
Modul: Container- und Image-Grundlagen
Docker und Podman im Vergleich
| Merkmal | Docker | Podman |
|---|---|---|
| Ausführendes Bauteil | zentraler Dienst | der Aufruf selbst |
| Rechte im Standardfall | Administrator | eigene Kennung |
| Container im Prozessbaum | Kind des Dienstes | Kind der Sitzung |
| Neustart des Dienstes | trifft alle Container | betrifft nichts |
| Sichtbarkeit für Andere | alle sehen alles | je Kennung getrennt |
| Sitzungsdienste | über den Dienst | über die Sitzung |
Beim Rootless-Betrieb kehren sich die Voreinstellungen um:
| Frage | Docker | Podman |
|---|---|---|
| Standardeinstellung | rootful | rootless |
| Einrichtung nötig | ja, eigenes Skript | nein |
| Ablage der Images | je Dienst getrennt | im Heimverzeichnis |
| Wechsel im Nachhinein | zweite Ablage | Aufruf mit Vorsatz |
| Zusätzliche Kennungen | Pflicht | Pflicht |
Rootless und rootful teilen nie eine Ablage — dasselbe Image liegt danach zweimal. Die zusätzlichen Kennungen sind in beiden Fällen Pflicht:
sudo usermod --add-subuids 100000-165535 mkoester
sudo usermod --add-subgids 100000-165535 mkoester
grep mkoester /etc/subuid /etc/subgid
Ein Leerzeichen am Zeilenende dieser beiden Dateien lässt den Eintrag stillschweigend ausfallen.
Modul: Lokale Entwicklung mit Docker und Podman
Netzwerk im rootless Betrieb
| Eigenschaft | rootful | rootless mit pasta |
|---|---|---|
| Netzgerät auf dem Rechner | echte Brücke | keines |
| Adressumschreibung | ja | im Standardfall nein |
| Absenderadresse | wird ersetzt | bleibt erhalten |
| Ports unter 1024 | ohne Weiteres | erst nach Freigabe |
| Durchsatz im Messfall | rund 18 Gbit/s | rund 8 bis 10 Gbit/s |
Die Durchsatzwerte stammen aus einer einzelnen Messreihe — im eigenen Aufbau
nachmessen. Für Ports unter 1024 ist der ruhigere Weg, hoch zu veröffentlichen
(-p 8080:3000) statt net.ipv4.ip_unprivileged_port_start systemweit zu
senken. Die Wahl des Hilfsprogramms steht an genau einer Stelle:
# ~/.config/containers/containers.conf
[network]
default_rootless_network_cmd = "pasta"
Modul: Lokale Entwicklung mit Docker und Podman
Compose, Sockets und lokale Cluster
| Weg | Umsetzung | Einordnung |
|---|---|---|
| docker compose | Erweiterung von Docker | vollständigster Sprachumfang |
| podman compose | Hülle um ein fremdes Werkzeug | Vorrang für docker-compose |
| podman-compose | eigenständig, in Python | deckt weniger Schlüssel ab |
Der Podman-Socket ist die Brücke für alles, was eine Docker-Schnittstelle erwartet — Compose, Testcontainers, k3d:
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix:///run/user/1000/podman/podman.sock
| Merkmal | Docker Desktop | Podman Desktop |
|---|---|---|
| Lizenz | für größere Firmen kostenpflichtig | Apache 2.0 |
| Engine darunter | Docker | Podman, auch andere |
| Lokale Cluster | eigener Schalter | Kind, Minikube und weitere |
| Nach Kubernetes senden | über Umwege | Pod aus der Oberfläche |
Beide Oberflächen ändern nichts an den Images — sie bedienen nur, was ohnehin da ist. Für den Cluster auf dem Arbeitsplatz stehen vier Wege zur Wahl:
| Werkzeug | Unterbau | Einordnung |
|---|---|---|
| kind | Kubernetes in Containern | Mehrknoten und Pipelines |
| minikube | ein Knoten, viele Treiber | reich an Zusatzmodulen |
| k3d | k3s in Containern | sehr schnell und schlank |
| Rancher Desktop | k3s mit Oberfläche | Umstieg ohne Kommandozeile |
Für Podman gilt bei kind und k3d ausdrücklich der Vermerk „in Erprobung”. Ein lokaler Cluster sieht außerdem andere Images als die eigene Engine.
Modul: Lokale Entwicklung mit Docker und Podman
Compose: die tragenden Schlüssel
| Schlüssel | Wirkung |
|---|---|
| image | nimmt ein fertiges Image aus einer Registry |
| build | baut aus einem Bauplan im Projekt, statt zu ziehen |
| environment | setzt Umgebungswerte für den Prozess im Container |
| ports | veröffentlicht einen Port auf dem eigenen Rechner |
| volumes | bindet Ablagen und Verzeichnisse ein |
| depends_on | legt Reihenfolge und Bedingung des Starts fest |
| profiles | hält einen Dienst zurück, bis er ausdrücklich gewählt wird |
image und build schließen sich nicht aus — zusammen sind sie Bauplan mit
Veröffentlichungsziel. Der Dienstname ist der Rechnername im internen Netz;
deshalb steht er in der Datenbankadresse und nicht ein Verzeichnisname.
Modul: Containerisierte Entwicklungs-Workflows
Volumes und Bind-Mounts
| Art | Herkunft | Typischer Einsatz |
|---|---|---|
| Benannte Ablage | von der Engine verwaltet | Datenbankdateien |
| Verzeichnis vom Rechner | ein Pfad im Projekt | Quellcode beim Entwickeln |
| Namenlose Ablage | bei Bedarf erzeugt | selten gewollt, oft versehen |
| Arbeitsspeicher | nichts, flüchtig | Zwischendateien und Geheimnisse |
Namenlose Ablagen entstehen, wenn nur ein Ziel angegeben wird — sie sammeln sich
unbemerkt an. Abhängigkeiten gehören ins Image, nicht in ein eingebundenes
Verzeichnis: ein Mount über node_modules verdeckt genau das, was im Image
installiert wurde.
Modul: Containerisierte Entwicklungs-Workflows
Healthchecks und Startreihenfolge
| Bedingung | Bedeutung |
|---|---|
| service_started | der Startbefehl ist abgesetzt, mehr nicht |
| service_healthy | der Healthcheck des Dienstes meldet Bereitschaft |
| service_completed_successfully | der Dienst ist fehlerfrei zu Ende gelaufen |
| restart | der abhängige Dienst wird nach einem Ersetzen mit erneuert |
| required | ohne diesen Schalter bricht ein fehlender Dienst den Start ab |
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dienst"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Die Anlaufzeit (start_period) zählt Fehlversuche nicht als krank — dafür ist
sie da. Ohne Healthcheck im Ziel-Dienst kann condition: service_healthy nie
erfüllt werden. Ein Prüfbefehl, der nur auf einen offenen Port sieht, meldet oft
Bereitschaft, die es nicht gibt.
Modul: Containerisierte Entwicklungs-Workflows
Testcontainers und Devcontainers
| Einstellung | Zweck |
|---|---|
| DOCKER_HOST | Adresse des Sockets, auch für Podman |
| TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE | Pfad für Container, die selbst Container starten |
| TESTCONTAINERS_HOST_OVERRIDE | Rechner, auf dem die Ports erscheinen |
| TESTCONTAINERS_RYUK_DISABLED | schaltet das Aufräum-Bauteil ab |
Der veröffentlichte Port wird bei jedem Lauf zufällig gewählt — er darf nirgends fest stehen. Für rootless Podman nennt die Dokumentation das Abschalten des Aufräum-Bauteils ausdrücklich; dann muss die Pipeline selbst aufräumen.
Die Lebenszyklus-Befehle eines Devcontainers in ihrer Reihenfolge:
| Befehl | Läuft |
|---|---|
| initializeCommand | auf dem eigenen Rechner, vor dem Container |
| onCreateCommand | einmal, direkt nach dem Erzeugen |
| updateContentCommand | danach, wenn Inhalte aufgefrischt werden |
| postCreateCommand | zuletzt beim Erzeugen, für die eigentliche Einrichtung |
| postStartCommand | bei jedem Start des Containers |
| postAttachCommand | sobald sich ein Werkzeug verbindet |
Nur der erste läuft außerhalb des Containers. Abhängigkeiten beim Erzeugen installieren, nicht beim Verbinden — sonst verlängert sich jeder Start.
Modul: Containerisierte Entwicklungs-Workflows
Bauen in der Pipeline
| Ziel | Einordnung |
|---|---|
| type=gha | Speicher der Pipeline, ohne eigene Einrichtung |
| type=registry | eigenes Ziel in der Registry, auch über Projekte hinweg |
| type=inline | im Image selbst, nur die letzte Schichtenfolge |
| type=local | Verzeichnis auf der Bau-Maschine, für eigene Läufer |
Mit dem Zusatz mode=max wandern auch die Zwischenetappen in den Speicher, nicht
nur das Ergebnis. Auf einem fremden Bau-Dienst stehen drei Wege zur Wahl:
| Weg | Einordnung |
|---|---|
| Dienst im Dienst | eigener Dienst je Auftrag, verlangt weitgehende Rechte |
| Socket einbinden | ohne Sonderrechte, gibt aber den Rechner preis |
| Werkzeug ohne Dienst | baut ohne Hintergrunddienst, empfohlener Ausweg |
Die Dokumentation nennt das Einbinden des Sockets ausdrücklich als Weg zur Rechteausweitung.
Modul: Containerisierte Entwicklungs-Workflows
Das Image härten
| Einstellung | Wirkung |
|---|---|
| runAsNonRoot | verweigert den Start unter der Kennung des Verwalters |
| runAsUser | legt die Kennung ausdrücklich fest |
| allowPrivilegeEscalation | verhindert Rechtezuwachs im laufenden Prozess |
| capabilities drop ALL | nimmt alle Sonderrechte des Kerns weg |
| readOnlyRootFilesystem | macht das Dateisystem des Containers unveränderlich |
| seccompProfile | beschränkt die erlaubten Aufrufe an den Kern |
Genau diese sechs prüft das strenge Profil der Pod Security Standards. Eine
numerische Kennung (USER 10001) wirkt auch dann, wenn im Image gar keine
Benutzerdatenbank liegt. Ein gehärtetes Image lässt sich unsicher betreiben —
erst Bauplan und Manifest zusammen tragen.
Modul: Container-Sicherheit und Supply Chain
Scannen, Stückliste, Signatur
| Prüfer | Findet |
|---|---|
| vuln | bekannte Schwachstellen in Paketen und Abhängigkeiten |
| secret | versehentlich mitgelieferte Zugangsdaten und Schlüssel |
| misconfig | riskante Einstellungen in Bauplänen und Manifesten |
| license | Lizenzen der enthaltenen Bestandteile |
Voreingestellt laufen die ersten zwei. Ohne Abbruchcode ist ein Scan Dekoration:
trivy image --severity HIGH,CRITICAL --ignore-unfixed \
--exit-code 1 registry.example/api@$DIGEST
| Format | Einordnung |
|---|---|
| SPDX | aus der Lizenzarbeit, breit anerkannt, auch als ISO-Norm |
| CycloneDX | aus der Sicherheitsarbeit, nah an Schwachstellendaten |
| beide zugleich | üblich, weil Abnehmer unterschiedliche Formate verlangen |
Stückliste und Signatur entstehen immer über den Digest, nie über den Tag. Eine belastbare Prüfung braucht vier Bestandteile:
| Bestandteil | Bedeutung |
|---|---|
| Digest | benennt genau die Fassung, die geprüft wird |
| Identität | wer signiert hat, als Muster oder exakter Wert |
| Ausgeber | welcher Identitätsdienst das bestätigt hat |
| Protokolleintrag | belegt, dass die Signatur zum Zeitpunkt existierte |
cosign verify "$IMAGE" \
--certificate-identity-regexp 'https://github.com/beispiel/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Eine Prüfung ohne Identität und Ausgeber beantwortet nur, dass irgendwer signiert hat.
Modul: Container-Sicherheit und Supply Chain
SLSA-Stufen
| Stufe | Verlangt |
|---|---|
| Build L0 | keine Zusage, kein Nachweis |
| Build L1 | Provenance existiert und beschreibt den Bauvorgang |
| Build L2 | die Bauplattform erzeugt und signiert sie selbst |
| Build L3 | gehärtete Bauläufe, die einander nicht beeinflussen können |
Eine vierte Stufe gibt es in der aktuellen Fassung nicht mehr — sie stammt aus der Entwurfsfassung. Provenance und Stückliste sind zweierlei: die eine beschreibt den Weg, die andere den Inhalt.
Modul: Container-Sicherheit und Supply Chain
Pod Security Standards und Kyverno
| Betriebsart | Folge bei einem Verstoß |
|---|---|
| enforce | der Pod wird abgelehnt und startet nicht |
| audit | der Pod läuft, der Verstoß landet im Prüfprotokoll |
| warn | der Pod läuft, der Einreichende bekommt eine Warnung |
Erzwingen greift nur bei Pods; Deployments erreicht man über die beiden anderen Arten. Bestehende Pods bleiben unberührt — erst der nächste Start wird geprüft.
| Schalter | Wirkung |
|---|---|
| imageReferences | auf welche Images die Regel überhaupt zielt |
| failureAction | erzwingen oder nur berichten |
| mutateDigest | ersetzt den Tag beim Zulassen durch den Digest |
| verifyDigest | verlangt, dass überhaupt ein Digest angegeben ist |
| required | verlangt für jedes passende Image eine Signatur |
Die drei letzten stehen voreingestellt auf wahr. Seit Kyverno 1.13 sitzt der
Schalter für Erzwingen in der Regel, nicht mehr oben in der Spezifikation. Ohne
attestors prüft die Regel nur, dass signiert wurde — nicht von wem.
Modul: Container-Sicherheit und Supply Chain
Typische Fallen
- EXPOSE für eine Freigabe halten — der Port wird erst beim Start veröffentlicht.
- Zugangsdaten per COPY oder Bau-Argument ins Image — sie bleiben in jeder Schicht und in der Bau-Historie nachlesbar.
- Den beweglichen Standard-Tag in die Produktion lassen — Fassungen tauschen unbemerkt; im Deployment gehört der Digest hin.
- Zusätzliche Kennungen vergessen — der rootless Start scheitert mit einer unverständlichen Meldung.
- Standardports im Compose-Aufbau festschreiben — ein fester Port 80 stoppt den rootless Lauf sofort.
- Eine feste Wartezeit ins Startskript legen — auf der Bau-Maschine ist sie dann doch zu kurz; dafür gibt es Healthchecks.
- Die benannte Ablage beim Aufräumen mitentfernen — der Datenstand der Entwicklung ist weg.
- Den Port der Testdatenbank festschreiben — Testcontainers vergibt ihn bei jedem Lauf neu.
- Das Dateisystem unveränderlich machen ohne Pfad für Zwischendateien — Zwischenspeicher und Protokollpfade klemmen zuerst.
- Nachträglich vom Tag eine Stückliste erzeugen — sie bezieht sich dann auf eine andere Fassung.
- Eine Signatur für eine Aussage über den Inhalt halten — sie belegt die Herkunft, nicht die Sicherheit.
- Sofort erzwingen statt erst berichtend prüfen — der halbe Namensraum verschwindet beim nächsten Neustart, im schlimmsten Fall samt Systemdiensten.