Docker Kurs: Unterschied zwischen den Versionen

Aus Xinux Wiki
Zur Navigation springen Zur Suche springen
 
(8 dazwischenliegende Versionen von 3 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
== Themenliste für den Docker-Schulungskurs ==
+
'''Docker – Von Images über Handling bis Compose'''
  
=== Einführung in Docker ===
+
__TOC__
  
*[[Was ist Docker?]]
+
= Docker Images =
*[[Warum Docker verwenden?]]
 
*[[Docker-Komponenten und -Architektur]]
 
  
 +
== Container vs. virtuelle Maschine ==
  
*[[Beispiel Themen Docker (Muss noch bearbeitet werden)]]
+
;Was ein Container ist
 +
Ein Container ist ein isolierter Prozess, der sich den Kernel des Host-Systems mit allen anderen Containern teilt. Eine virtuelle Maschine bringt dagegen einen eigenen Kernel und ein eigenes Betriebssystem mit.
  
=Docker Sicherheit=
+
;Warum das den Unterschied ausmacht
*[[Docker Security Sheet]]
+
* Container starten in Millisekunden, VMs brauchen Sekunden bis Minuten
 +
* Ein Container-Image ist oft nur wenige MB groß, ein VM-Image mehrere GB
 +
* Auf einem Linux-Host können beliebig viele Linux-Distributionen als Container laufen (Ubuntu, Alpine, Debian, ...) – sie teilen sich alle denselben Kernel
 +
* Ein Windows-Container kann NICHT auf einem Linux-Host laufen (ohne Emulation), weil der Kernel fehlt
 +
 
 +
'''Aufgabe:''' Skizzieren Sie den Unterschied Host → Hypervisor → VM vs. Host → Docker-Engine → Container.
 +
 
 +
== Warum Docker? ==
 +
 
 +
;Die drei klassischen Probleme klassischer Softwareverteilung
 +
* "Works on my machine" – unterschiedliche Umgebungen auf verschiedenen Rechnern
 +
* Abhängigkeitskonflikte – Programm A braucht Python 2.7, Programm B Python 3.9
 +
* Aufwändiges Setup neuer Server – Stunden für Installation und Konfiguration
 +
 
 +
;Wie Docker das löst
 +
Ein Image kapselt Code, Bibliotheken und Konfiguration zu einem festen Zeitpunkt. Das Image läuft danach überall identisch, wo Docker installiert ist.
 +
 
 +
== Docker installieren und verifizieren ==
 +
 
 +
;Docker auf Debian/Ubuntu installieren
 +
*sudo apt update
 +
*sudo apt install docker.io
 +
*sudo systemctl enable --now docker
 +
*sudo usermod -aG docker $USER
 +
 
 +
Nach <code>usermod</code> ist ein Neu-Login (oder <code>newgrp docker</code>) nötig, damit die Gruppenmitgliedschaft aktiv wird.
 +
 
 +
;Installation prüfen
 +
*docker --version
 +
*docker run hello-world
 +
 
 +
;Erwartete Ausgabe von hello-world
 +
<syntaxhighlight lang="bash">
 +
Unable to find image 'hello-world:latest' locally
 +
latest: Pulling from library/hello-world
 +
...
 +
Hello from Docker!
 +
This message shows that your installation appears to be working correctly.
 +
</syntaxhighlight>
 +
 
 +
'''Aufgabe:''' Docker installieren, <code>hello-world</code> ausführen und die Ausgabe dem Dozenten zeigen.
 +
 
 +
== Images pullen und auflisten ==
 +
 
 +
;Ein Image von Docker Hub herunterladen
 +
*docker pull alpine
 +
*docker pull alpine:3.18
 +
 
 +
Ohne Tag wird automatisch <code>:latest</code> angenommen – eine reine Konvention, keine Garantie für "neueste stabile Version".
 +
 
 +
;Lokal vorhandene Images anzeigen
 +
*docker images
 +
*docker image ls
 +
 
 +
Beide liefern dieselbe Tabelle: REPOSITORY, TAG, IMAGE ID, CREATED, SIZE.
 +
 
 +
'''Aufgabe:''' <code>alpine</code>, <code>alpine:3.18</code> und <code>nginx:alpine</code> pullen und die Größen mit <code>docker images</code> vergleichen.
 +
 
 +
== Registries: Docker Hub, öffentlich und privat ==
 +
 
 +
;Was eine Registry ist
 +
Docker Hub ist die Standard-Registry – vergleichbar mit GitHub, nur für Images. Wird kein Registry-Präfix angegeben, nimmt Docker automatisch <code>docker.io</code> an.
 +
 
 +
;Offizielle vs. Community-Images unterscheiden
 +
* Offizielle Images haben keinen Schrägstrich im Namen: <code>ubuntu</code>, <code>nginx</code>, <code>python</code>
 +
* Community-Images enthalten Nutzer- oder Organisationsnamen: <code>myuser/myapp</code>
 +
 
 +
;Nach Images suchen
 +
*docker search nginx
 +
 
 +
;Exakte Version über den Digest pullen (unveränderlich, im Gegensatz zu Tags)
 +
*docker inspect alpine:3.18 | grep Digest
 +
*docker pull alpine@sha256:xxxxx
 +
 
 +
== Das Dockerfile: FROM, WORKDIR, COPY ==
 +
 
 +
;Was ein Dockerfile ist
 +
Eine Textdatei mit Bauanleitung für ein Image. Jede Instruktion erzeugt einen eigenen Layer. Konvention: Datei heißt <code>Dockerfile</code> (großes D, keine Endung).
 +
 
 +
{|
 +
|-
 +
| '''FROM''' || legt das Basis-Image fest, jedes Dockerfile beginnt damit (außer FROM scratch)
 +
|-
 +
| '''WORKDIR''' || setzt das Arbeitsverzeichnis für alle folgenden RUN/CMD/COPY-Instruktionen
 +
|-
 +
| '''COPY''' || kopiert Dateien vom Build-Kontext (lokales Verzeichnis) ins Image
 +
|}
 +
 
 +
;Minimalbeispiel
 +
<syntaxhighlight lang="dockerfile">
 +
FROM alpine:latest
 +
WORKDIR /app
 +
COPY index.html .
 +
</syntaxhighlight>
 +
 
 +
'''Aufgabe:''' Projektverzeichnis mit eigener <code>index.html</code> anlegen und ein Dockerfile mit FROM/WORKDIR/COPY dafür schreiben.
 +
 
 +
== RUN und Layer ==
 +
 
 +
;Was RUN macht
 +
Jede RUN-Zeile führt einen Befehl während des Builds aus und erzeugt dabei einen eigenen Layer, der dauerhaft Teil des Images wird.
 +
 
 +
<syntaxhighlight lang="dockerfile">
 +
FROM alpine:latest
 +
RUN apk add --no-cache curl
 +
</syntaxhighlight>
 +
 
 +
;Image bauen
 +
*docker build -t my-first-image .
 +
 
 +
Der Punkt am Ende gibt den Build-Kontext an (das Verzeichnis, in dem nach dem Dockerfile gesucht wird).
 +
 
 +
;Entstandene Layer ansehen
 +
*docker history my-first-image
 +
 
 +
'''Aufgabe:''' Das eigene Dockerfile um eine RUN-Zeile ergänzen, mit <code>docker build -t webtest .</code> bauen und mit <code>docker history</code> prüfen, wie viele Layer entstanden sind.
 +
 
 +
== ENV und EXPOSE ==
 +
 
 +
;Umgebungsvariablen im Image festlegen
 +
<syntaxhighlight lang="dockerfile">
 +
ENV APP_HOME=/app
 +
ENV NODE_ENV=production
 +
</syntaxhighlight>
 +
 
 +
Spätere Instruktionen können mit <code>$VARIABLE_NAME</code> darauf zugreifen. Beim Start lassen sich Werte überschreiben:
 +
 
 +
;Umgebungsvariable beim Start überschreiben
 +
*docker run -e APP_HOME=/newpath myimage
 +
 
 +
;Ports dokumentieren
 +
<syntaxhighlight lang="dockerfile">
 +
EXPOSE 80
 +
</syntaxhighlight>
 +
 
 +
EXPOSE veröffentlicht den Port NICHT tatsächlich, es ist reine Dokumentation. Freigegeben wird ein Port erst beim Start:
 +
 
 +
;Port tatsächlich auf den Host mappen
 +
*docker run -p 8080:80 mynginx
 +
 
 +
== CMD vs. ENTRYPOINT ==
 +
 
 +
;CMD – austauschbarer Standardbefehl
 +
<syntaxhighlight lang="dockerfile">
 +
FROM alpine
 +
CMD ["echo", "Hello from CMD"]
 +
</syntaxhighlight>
 +
<code>docker run myimage</code> führt den CMD aus, <code>docker run myimage echo overridden</code> ersetzt ihn komplett.
 +
 
 +
;ENTRYPOINT – fester Befehl
 +
<syntaxhighlight lang="dockerfile">
 +
FROM alpine
 +
ENTRYPOINT ["echo"]
 +
CMD ["Hello"]
 +
</syntaxhighlight>
 +
Der ENTRYPOINT bleibt fest, CMD liefert nur das austauschbare Standard-Argument.
 +
 
 +
;Faustregel
 +
* Nur CMD → wenn Nutzer den kompletten Befehl frei überschreiben können sollen
 +
* Nur ENTRYPOINT → wenn das Image sich wie ein eigenständiges Kommando verhalten soll
 +
* Beides → fester Befehl mit austauschbaren Argumenten
 +
 
 +
'''Aufgabe:''' Zwei Varianten desselben Mini-Images bauen (einmal nur CMD, einmal ENTRYPOINT+CMD) und live testen, was <code>docker run image ARGUMENT</code> jeweils tatsächlich ausführt.
 +
 
 +
== ADD vs. COPY ==
 +
 
 +
;Warum meistens COPY die richtige Wahl ist
 +
COPY kopiert lokale Dateien 1:1 ins Image – vorhersehbar, tut genau eine Sache.
 +
 
 +
;Was ADD zusätzlich kann
 +
* Lokale tar-Archive automatisch entpacken
 +
* Dateien direkt von einer URL herunterladen
 +
 
 +
Für URL-Downloads ist <code>RUN curl</code> oder <code>RUN wget</code> vorzuziehen – klarer und mit mehr Kontrolle als ADD.
 +
 
 +
== Layer-Caching und Instruktionsreihenfolge ==
 +
 
 +
;Wie das Caching funktioniert
 +
Docker berechnet für jeden Layer einen Hash aus Inhalt und Hash des vorherigen Layers. Ändert sich eine Zeile, wird ab dieser Zeile alles neu gebaut – die Layer darüber bleiben aus dem Cache.
 +
 
 +
;Empfohlene Reihenfolge (selten → häufig ändernd)
 +
# FROM
 +
# RUN apt/apk install (ändert sich selten)
 +
# COPY requirements.txt / package.json
 +
# RUN pip/npm install (hängt von obiger Datei ab)
 +
# COPY restlicher Code (ändert sich am häufigsten)
 +
 
 +
Schlechte Reihenfolge: <code>COPY . .</code> ganz am Anfang – dann invalidiert jede Codeänderung den kompletten nachfolgenden Cache.
 +
 
 +
'''Aufgabe:''' Ein eigenes Projekt zweimal hintereinander OHNE Änderung bauen (Cache sichtbar), dann eine Zeile im Anwendungscode ändern und beobachten, welche Layer neu gebaut werden. Danach bewusst schlechte Reihenfolge testen (COPY vor RUN install) und den Unterschied vergleichen.
 +
 
 +
== Image-Verkleinerung: Alpine und Multi-Stage-Builds ==
 +
 
 +
;Warum kleine Images wichtig sind
 +
* Schnelleres Pull/Push
 +
* Weniger Angriffsfläche
 +
* Weniger Speicherbedarf
 +
 
 +
;Alpine Linux
 +
Rund 5 MB statt 70–80 MB bei Ubuntu/Debian. Nutzt musl libc + busybox statt glibc + GNU coreutils. Viele offizielle Images bieten eine <code>-alpine</code>-Variante.
 +
 
 +
;Multi-Stage-Build
 +
<syntaxhighlight lang="dockerfile">
 +
FROM golang:alpine AS builder
 +
WORKDIR /app
 +
COPY main.go .
 +
RUN go build -o myapp
 +
 
 +
FROM alpine:latest
 +
WORKDIR /root/
 +
COPY --from=builder /app/myapp .
 +
CMD ["./myapp"]
 +
</syntaxhighlight>
 +
Die Build-Stage enthält Compiler und Quellcode, die finale Stage nur das fertige Binary.
 +
 
 +
'''Aufgabe:''' Eine kleine eigene Anwendung (Python-Flask, Node.js oder statische HTML-Seite) von Grund auf mit eigenem Dockerfile bauen – FROM/WORKDIR/COPY/RUN/CMD, sinnvolle Layer-Reihenfolge, Alpine-Basis.
 +
 
 +
== Images verwalten ==
 +
 
 +
;Image-Metadaten als JSON ansehen
 +
*docker inspect alpine:latest
 +
 
 +
;Einzelne Felder gezielt auslesen
 +
*docker inspect alpine:latest --format='{{.Architecture}}'
 +
 
 +
;Ein Image entfernen
 +
*docker rmi my-first-image
 +
 
 +
;Nicht mehr verwendete Images aufräumen
 +
*docker image prune
 +
 
 +
'''Aufgabe:''' Ein nicht mehr benötigtes Test-Image aus einer früheren Übung gezielt entfernen und danach mit <code>docker image prune</code> aufräumen.
 +
 
 +
== Images teilen ==
 +
 
 +
;Bei Docker Hub anmelden
 +
*docker login
 +
 
 +
;Image für den Upload mit dem eigenen Benutzernamen taggen
 +
*docker tag my-first-image johnsmith/my-first-image:1.0
 +
 
 +
;Image hochladen
 +
*docker push johnsmith/my-first-image:1.0
 +
 
 +
;Image auf einem anderen Rechner herunterladen und starten
 +
*docker pull johnsmith/my-first-image:1.0
 +
*docker run johnsmith/my-first-image:1.0
 +
 
 +
'''Aufgabe:''' Ein eigenes Image mit dem eigenen Docker-Hub-Konto taggen und pushen (oder alternativ gegen die spätere lokale Registry aus dem Security-/Registry-Teil).
 +
 
 +
== Images offline sichern ==
 +
 
 +
;Ein Image als Datei exportieren
 +
*docker save -o myimage.tar my-first-image:latest
 +
 
 +
;Ein Image aus einer Datei importieren
 +
*docker load -i myimage.tar
 +
 
 +
Sinnvoll für Umgebungen ohne Registry-Zugriff (Firewall, air-gapped Systeme) oder zur Archivierung einer bestimmten Version.
 +
 
 +
== Typische Anfängerfehler ==
 +
 
 +
;Änderung am Code wird nicht übernommen
 +
Das Image enthält noch den alten Stand – nach Codeänderungen muss neu gebaut werden: <code>docker build -t myimage .</code>
 +
 
 +
;latest ist keine sichere Wahl
 +
<code>ubuntu:latest</code> zeigt heute auf 22.04, in einem Monat womöglich auf 24.04 – für reproduzierbare Builds immer eine konkrete Version wie <code>ubuntu:22.04</code> verwenden.
 +
 
 +
;Riesige Images durch unnötige Dateien
 +
Mit einer <code>.dockerignore</code>-Datei (analog zu .gitignore) lassen sich node_modules, .git, Logs und Secrets vom Build-Kontext ausschließen.
 +
 
 +
;Container läuft standardmäßig als root
 +
Sicherheitsrisiko – siehe Abschnitt "Container-Security" weiter unten.
 +
 
 +
== Troubleshooting ==
 +
 
 +
;Build bricht mit "Unable to locate package" ab
 +
Meist fehlt ein vorheriges <code>apt update</code>/<code>apk update</code> vor der Installation.
 +
 
 +
;"Unable to find image ... locally"
 +
Das referenzierte Image existiert lokal nicht und konnte auch nicht von der Registry geladen werden – Image-Name/Tag prüfen oder zuerst selbst bauen.
 +
 
 +
;Cache greift nicht mehr
 +
Eine frühe Zeile im Dockerfile hat sich geändert – der Cache bricht ab dieser Stelle linear ab. Reihenfolge nach Änderungshäufigkeit prüfen (siehe oben).
 +
 
 +
;Image läuft bei mir, aber nicht beim Kollegen
 +
Häufigste Ursachen: unterschiedliche Auflösung von <code>latest</code>, fehlende COPY-Dateien (stattdessen lokale Bind Mounts genutzt), oder Umgebungsvariablen, die nur lokal gesetzt sind.
 +
 
 +
= Docker Container-Handling =
 +
 
 +
== Kurzwiederholung ==
 +
 
 +
Ein Image ist eine statische, read-only Vorlage. Ein Container ist eine laufende Instanz dieser Vorlage mit einer eigenen beschreibbaren Schicht obendrauf. Ab hier geht es um den täglichen Umgang mit laufenden Containern.
 +
 
 +
== Der Container-Lifecycle ==
 +
 
 +
;Einen neuen Container aus einem Image erzeugen und starten
 +
*docker run <image>
 +
 
 +
;Einen bestehenden, gestoppten Container fortsetzen
 +
*docker start <container>
 +
 
 +
<code>run</code> erzeugt jedes Mal einen NEUEN Container. <code>start</code> setzt einen vorhandenen fort – inklusive aller Änderungen in seiner beschreibbaren Schicht.
 +
 
 +
;Einen laufenden Container stoppen
 +
*docker stop <container>
 +
 
 +
;Einen Container neu starten
 +
*docker restart <container>
 +
 
 +
;Einen gestoppten Container endgültig entfernen
 +
*docker rm <container>
 +
 
 +
;Laufende bzw. alle Container anzeigen
 +
*docker ps
 +
*docker ps -a
 +
 
 +
'''Aufgabe:''' Einen Container starten, stoppen, wieder starten und schließlich entfernen – bei jedem Schritt <code>docker ps -a</code> prüfen.
 +
 
 +
== Wichtige Flags beim Starten ==
 +
 
 +
{|
 +
|-
 +
| '''-d''' || detached – Container läuft im Hintergrund
 +
|-
 +
| '''-it''' || interaktiv mit Pseudo-TTY – für Shells und interaktive Programme
 +
|-
 +
| '''--rm''' || Container wird nach dem Beenden automatisch entfernt
 +
|-
 +
| '''--name''' || vergibt einen festen, sprechenden Namen statt einer zufälligen ID
 +
|-
 +
| '''-p host:container''' || Port-Mapping vom Host auf den Container
 +
|}
 +
 
 +
;Container im Hintergrund mit festem Namen und Port-Mapping starten
 +
*docker run -d --name webserver -p 8080:80 nginx:alpine
 +
 
 +
== In laufende Container hineinschauen ==
 +
 
 +
;Logs eines Containers ansehen
 +
*docker logs <container>
 +
 
 +
;Der Ausgabe live folgen
 +
*docker logs -f <container>
 +
 
 +
;Einen zusätzlichen Befehl im laufenden Container ausführen
 +
*docker exec -it <container> /bin/sh
 +
*docker exec <container> ps aux
 +
 
 +
<code>exec</code> startet einen zusätzlichen Prozess IM bereits laufenden Container – der Hauptprozess läuft unbeeinflusst weiter.
 +
 
 +
'''Aufgabe:''' Einen nginx-Container starten, per <code>exec</code> eine Shell öffnen, dort eine Datei unter <code>/usr/share/nginx/html</code> ändern und die Änderung im Browser/mit curl vom Host aus prüfen.
 +
 
 +
== Metadaten auslesen: inspect ==
 +
 
 +
;Vollständige Metadaten eines Containers ansehen
 +
*docker inspect <container>
 +
 
 +
Liefert IP-Adresse, Mount-Punkte, Umgebungsvariablen, Netzwerkeinstellungen, Restart-Policy und mehr als JSON.
 +
 
 +
;Ein einzelnes Feld gezielt auslesen
 +
*docker inspect --format='{{.NetworkSettings.IPAddress}}' webserver
 +
 
 +
== Netzwerke: bridge, host, none ==
 +
 
 +
{|
 +
|-
 +
| '''bridge''' || Standard. Eigene IP in einem privaten Docker-Netz, NAT zum Host
 +
|-
 +
| '''host''' || teilt sich den Netzwerk-Stack direkt mit dem Host – kein NAT, keine Isolation
 +
|-
 +
| '''none''' || keine Netzwerkanbindung
 +
|}
 +
 
 +
== Eigene Netzwerke und Container-DNS ==
 +
 
 +
;Warum ein eigenes Netzwerk nötig ist
 +
Im Standard-bridge-Netzwerk können Container sich NICHT über ihren Namen erreichen, nur über IP. Benutzerdefinierte Netzwerke bringen automatisch Namensauflösung mit.
 +
 
 +
;Eigenes Netzwerk anlegen
 +
*docker network create meinnetz
 +
 
 +
;Zwei Container im selben Netzwerk starten
 +
*docker run -d --name db --network meinnetz postgres
 +
*docker run -d --name app --network meinnetz --env DB_HOST=db myapp
 +
 
 +
Innerhalb von <code>meinnetz</code> kann <code>app</code> den Hostnamen <code>db</code> direkt auflösen.
 +
 
 +
;Netzwerke verwalten
 +
*docker network ls
 +
*docker network inspect meinnetz
 +
*docker network rm meinnetz
 +
 
 +
'''Aufgabe:''' Zwei eigene Container in einem selbst angelegten Netzwerk starten und aus dem einen Container heraus den anderen per Namen anpingen bzw. curlen.
 +
 
 +
== Daten dauerhaft speichern: Volumes und Bind Mounts ==
 +
 
 +
;Warum das wichtig ist
 +
Ohne Vorkehrung gehen alle Daten eines Containers verloren, sobald er entfernt wird – sie liegen nur in seiner beschreibbaren Schicht.
 +
 
 +
;Ein Volume anlegen und einem Container zuweisen
 +
*docker volume create meindaten
 +
*docker run -d --name db -v meindaten:/var/lib/postgresql/data postgres
 +
 
 +
Docker verwaltet den Speicherort selbst – Standardweg für persistente Anwendungsdaten wie Datenbanken.
 +
 
 +
;Einen Host-Pfad direkt einhängen (Bind Mount)
 +
*docker run -d --name web -v /home/user/website:/usr/share/nginx/html nginx
 +
 
 +
Praktisch für Entwicklung (Codeänderungen sofort sichtbar), aber abhängig von der Verzeichnisstruktur des jeweiligen Hosts.
 +
 
 +
;Volumes verwalten
 +
*docker volume ls
 +
*docker volume inspect meindaten
 +
*docker volume rm meindaten
 +
*docker volume prune
 +
 
 +
'''Aufgabe:''' Einen Datenbank-Container mit einem Volume starten, einen Datensatz anlegen, den Container mit <code>docker rm -f</code> entfernen, einen neuen Container mit demselben Volume starten und prüfen, dass der Datensatz noch vorhanden ist.
 +
 
 +
== Ressourcenlimits ==
 +
 
 +
;Speicher und CPU eines Containers begrenzen
 +
*docker run -d --memory=512m --cpus=1.5 myapp
 +
 
 +
<code>--memory</code> ist eine harte RAM-Obergrenze – bei Überschreitung beendet der Kernel-OOM-Killer den Prozess. <code>--cpus</code> begrenzt die maximale Anzahl an CPU-Kernen, auch als Bruchteil.
 +
 
 +
== Neustart-Strategien ==
 +
 
 +
{|
 +
|-
 +
| '''no''' || Standard. Nie automatisch neu starten
 +
|-
 +
| '''on-failure''' || nur bei Fehler-Exitcode
 +
|-
 +
| '''always''' || immer, auch nach Host-Neustart
 +
|-
 +
| '''unless-stopped''' || wie always, außer der Container wurde manuell gestoppt
 +
|}
 +
 
 +
;Restart-Policy beim Start festlegen
 +
*docker run -d --restart unless-stopped nginx
 +
 
 +
Für produktiv laufende Dienste ist <code>unless-stopped</code> meist die sinnvollste Wahl.
 +
 
 +
== Healthchecks ==
 +
 
 +
;Warum ein laufender Prozess nicht reicht
 +
Ein Prozess kann laufen, ohne dass die Anwendung dahinter tatsächlich funktioniert (z. B. hängender Webserver). Healthchecks prüfen aktiv den Zustand.
 +
 
 +
<syntaxhighlight lang="dockerfile">
 +
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1
 +
</syntaxhighlight>
 +
 
 +
<code>docker ps</code> zeigt danach zusätzlich zu "Up" den Status "healthy" oder "unhealthy" an.
 +
 
 +
'''Aufgabe:''' Einem eigenen Dockerfile einen HEALTHCHECK hinzufügen, das Image bauen, den Container starten und in <code>docker ps</code> den Health-Status beobachten.
 +
 
 +
== Troubleshooting ==
 +
 
 +
;Container beendet sich sofort wieder
 +
Meist läuft der Hauptprozess kurz und terminiert dann. Prüfen mit <code>docker logs <container></code>.
 +
 
 +
;"Bind for 0.0.0.0:80 failed: port is already allocated"
 +
Der Host-Port ist bereits belegt. Prüfen mit <code>ss -tulpn | grep :80</code> oder anderen Host-Port wählen.
 +
 
 +
;Container erreicht anderen Container nicht per Namen
 +
Beide müssen im selben, benutzerdefinierten Netzwerk laufen – im Standard-bridge-Netz funktioniert Namensauflösung nicht.
 +
 
 +
;Daten sind nach docker rm weg
 +
Ohne Volume oder Bind Mount ist das erwartetes Verhalten, kein Fehler.
 +
 
 +
= Docker Compose =
 +
 
 +
== Warum Compose? ==
 +
 
 +
Sobald eine Anwendung aus mehreren Containern besteht (z. B. Webserver + Datenbank + Cache), wird das manuelle Verketten von <code>docker run</code>-Befehlen mit Netzwerken und Volumes schnell unübersichtlich. Docker Compose beschreibt den kompletten Stack in einer einzigen YAML-Datei und startet ihn mit einem Befehl.
 +
 
 +
== YAML-Grundlagen ==
 +
 
 +
;Grundsyntax
 +
YAML nutzt Einrückung (Leerzeichen, keine Tabs) statt Klammern. Listen beginnen mit einem Bindestrich, Schlüssel-Wert-Paare werden mit Doppelpunkt getrennt.
 +
 
 +
<syntaxhighlight lang="yaml">
 +
schluessel: wert
 +
liste:
 +
  - eintrag1
 +
  - eintrag2
 +
verschachtelt:
 +
  unterschluessel: wert
 +
</syntaxhighlight>
 +
 
 +
== Grundstruktur einer compose.yml ==
 +
 
 +
;Minimalbeispiel
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  web:
 +
    image: nginx:alpine
 +
    ports:
 +
      - "8080:80"
 +
 
 +
networks:
 +
  default:
 +
 
 +
volumes:
 +
  webdata:
 +
</syntaxhighlight>
 +
 
 +
Die drei Hauptblöcke: <code>services</code> (die eigentlichen Container), <code>networks</code> und <code>volumes</code> (beide optional, werden bei Bedarf automatisch mit Standardwerten angelegt).
 +
 
 +
== Ein erster Service ==
 +
 
 +
;Stack im Hintergrund starten
 +
*docker compose up -d
 +
 
 +
;Status aller Services anzeigen
 +
*docker compose ps
 +
 
 +
;Stack stoppen und entfernen
 +
*docker compose down
 +
 
 +
<code>up</code> baut/pullt bei Bedarf und startet alle Services. <code>down</code> entfernt Container und Standard-Netzwerke, Volumes bleiben standardmäßig erhalten.
 +
 
 +
'''Aufgabe:''' Eine minimale compose.yml mit einem nginx-Service und Port-Mapping schreiben, starten und im Browser aufrufen.
 +
 
 +
== build vs. image ==
 +
 
 +
;Ein eigenes Dockerfile über Compose bauen und gleichzeitig taggen
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  app:
 +
    build: .
 +
    image: meinapp:1.0
 +
</syntaxhighlight>
 +
 
 +
Mit <code>build</code> baut Compose ein eigenes Dockerfile aus dem angegebenen Verzeichnis. Mit <code>image</code> allein wird stattdessen ein vorhandenes Image gepullt.
 +
 
 +
== Mehrere Services verbinden ==
 +
 
 +
;Zwei Services, die sich gegenseitig per Name erreichen
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  app:
 +
    build: .
 +
    environment:
 +
      DB_HOST: db
 +
  db:
 +
    image: postgres:16
 +
    environment:
 +
      POSTGRES_PASSWORD: geheim
 +
</syntaxhighlight>
 +
 
 +
Alle Services eines Compose-Projekts landen automatisch im selben, projektspezifischen Netzwerk.
 +
 
 +
== Startreihenfolge: depends_on ==
 +
 
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  app:
 +
    build: .
 +
    depends_on:
 +
      - db
 +
  db:
 +
    image: postgres:16
 +
</syntaxhighlight>
 +
 
 +
<code>depends_on</code> wartet standardmäßig nur, bis der Container GESTARTET ist – nicht, bis der Dienst darin tatsächlich BEREIT ist. Für echtes Warten auf Betriebsbereitschaft: Kombination mit Healthchecks (siehe unten).
 +
 
 +
'''Aufgabe:''' Zwei Services (App + Datenbank) mit <code>depends_on</code> verbinden und beobachten, dass die App evtl. startet, bevor die Datenbank bereit ist.
 +
 
 +
== Volumes in Compose ==
 +
 
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  db:
 +
    image: postgres:16
 +
    volumes:
 +
      - dbdata:/var/lib/postgresql/data
 +
 
 +
volumes:
 +
  dbdata:
 +
</syntaxhighlight>
 +
 
 +
== Umgebungsvariablen und .env-Dateien ==
 +
 
 +
;Variablen aus einer .env-Datei einbinden
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  app:
 +
    build: .
 +
    env_file: .env
 +
</syntaxhighlight>
 +
 
 +
<syntaxhighlight lang="bash">
 +
# .env
 +
DB_PASSWORD=geheim
 +
APP_PORT=8080
 +
</syntaxhighlight>
 +
 
 +
Werte lassen sich zusätzlich direkt in der compose.yml referenzieren: <code>"${APP_PORT}:80"</code>. Das trennt Konfiguration von Code und verhindert, dass Zugangsdaten fest im Compose-File landen.
 +
 
 +
'''Aufgabe:''' Die Datenbank-Zugangsdaten aus der vorherigen Übung in eine <code>.env</code>-Datei auslagern und per <code>env_file</code> einbinden.
 +
 
 +
== Healthchecks als Startbedingung ==
 +
 
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  db:
 +
    image: postgres:16
 +
    healthcheck:
 +
      test: ["CMD-SHELL", "pg_isready -U postgres"]
 +
      interval: 5s
 +
      retries: 5
 +
  app:
 +
    build: .
 +
    depends_on:
 +
      db:
 +
        condition: service_healthy
 +
</syntaxhighlight>
 +
 
 +
Damit wartet <code>app</code> nicht nur, bis der db-Container läuft, sondern bis der Healthcheck tatsächlich "healthy" meldet.
 +
 
 +
'''Aufgabe:''' Den Stack aus der depends_on-Übung um einen Healthcheck für die Datenbank erweitern und prüfen, dass die App jetzt erst nach "healthy" startet.
 +
 
 +
== Profiles ==
 +
 
 +
<syntaxhighlight lang="yaml">
 +
services:
 +
  app:
 +
    build: .
 +
  debug-tools:
 +
    image: busybox
 +
    profiles: ["debug"]
 +
</syntaxhighlight>
 +
 
 +
;Nur Services mit einem bestimmten Profil zusätzlich starten
 +
*docker compose --profile debug up
 +
 
 +
Services mit <code>profiles</code> starten NICHT bei einem normalen <code>docker compose up</code>.
 +
 
 +
== Override-Files ==
 +
 
 +
Compose liest standardmäßig automatisch sowohl <code>docker-compose.yml</code> als auch <code>docker-compose.override.yml</code> und führt beide zusammen.
 +
 
 +
;Explizit mehrere Compose-Dateien kombinieren
 +
*docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
 +
 
 +
== Skalieren mit --scale ==
 +
 
 +
;Mehrere Instanzen eines Services starten
 +
*docker compose up -d --scale app=3
 +
 
 +
Voraussetzung: Der Service darf keinen festen Host-Port über <code>ports</code> belegen, sonst kommt es zum Portkonflikt.
 +
 
 +
== Die wichtigsten Compose-Befehle im Überblick ==
 +
 
 +
;Stack im Hintergrund starten
 +
*docker compose up -d
 +
 
 +
;Stack stoppen und entfernen
 +
*docker compose down
 +
 
 +
;Zusätzlich auch Volumes entfernen
 +
*docker compose down -v
 +
 
 +
;Status aller Services
 +
*docker compose ps
 +
 
 +
;Logs eines einzelnen Services live verfolgen
 +
*docker compose logs -f <service>
 +
 
 +
;Shell in einem laufenden Service öffnen
 +
*docker compose exec <service> sh
 +
 
 +
;Einzelnen Service neu starten
 +
*docker compose restart <service>
 +
 
 +
== Troubleshooting ==
 +
 
 +
;"service depends on undefined service"
 +
Tippfehler im Service-Namen bei <code>depends_on</code> – Name muss exakt mit dem Schlüssel unter <code>services:</code> übereinstimmen.
 +
 
 +
;Änderungen am Dockerfile werden nicht übernommen
 +
Nach Dockerfile-Änderungen: <code>docker compose up -d --build</code>.
 +
 
 +
;Service startet vor der Datenbank, obwohl depends_on gesetzt ist
 +
Ohne Healthcheck-Bedingung wartet Compose nur auf den Containerstart, nicht auf Betriebsbereitschaft.
 +
 
 +
'''Aufgabe (Mini-Stack):''' Einen kompletten Stack aus Webanwendung und Datenbank mit eigenem Dockerfile, Netzwerk, Volume, .env-Datei und Healthcheck-basiertem depends_on von Grund auf selbst schreiben und starten.
 +
 
 +
= Container-Security =
 +
 
 +
== Rootless Docker ==
 +
 
 +
;Warum das relevant ist
 +
Standardmäßig läuft der Docker-Daemon als root. Bricht jemand aus einem Container aus, hat er potenziell Root-Rechte auf dem Host. Rootless Docker führt den kompletten Daemon als normaler Benutzer aus.
 +
 
 +
;Kurzüberblick zur Einrichtung
 +
*dockerd-rootless-setuptool.sh install
 +
 
 +
Details hängen von der jeweiligen Distribution ab – im Kurs wird der Ablauf live an einer Testmaschine gezeigt.
 +
 
 +
== Als Non-Root-User laufen ==
 +
 
 +
;Eigenen Benutzer im Dockerfile anlegen und nutzen
 +
<syntaxhighlight lang="dockerfile">
 +
FROM ubuntu:22.04
 +
RUN useradd -m -s /bin/bash myuser
 +
USER myuser
 +
WORKDIR /home/myuser
 +
</syntaxhighlight>
 +
 
 +
Manche Basis-Images bringen bereits einen passenden Nutzer mit (<code>node</code> für Node.js, <code>www-data</code> für PHP) – diese nach Möglichkeit verwenden statt einen eigenen anzulegen.
 +
 
 +
'''Aufgabe:''' Das eigene Dockerfile aus den vorherigen Übungen um einen Non-Root-User erweitern und mit <code>docker exec <container> whoami</code> bestätigen, dass der Container nicht mehr als root läuft.
 +
 
 +
== Capabilities einschränken ==
 +
 
 +
;Alle Capabilities entfernen und nur die nötigen zurückgeben
 +
*docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
 +
 
 +
Reduziert die Angriffsfläche, falls ein Container kompromittiert wird – er bekommt nur die Kernel-Rechte, die er tatsächlich braucht.
 +
 
 +
= Private Registry und Ausblick =
 +
 
 +
== Eigene private Registry betreiben ==
 +
 
 +
;Eine lokale Registry als Container starten
 +
*docker run -d -p 5000:5000 --name registry registry:2
 +
 
 +
;Ein Image für die lokale Registry taggen
 +
*docker tag myimage localhost:5000/myimage
 +
 
 +
;In die lokale Registry pushen
 +
*docker push localhost:5000/myimage
 +
 
 +
;Aus der lokalen Registry pullen
 +
*docker pull localhost:5000/myimage
 +
 
 +
'''Aufgabe:''' Eine eigene lokale Registry starten, ein selbst gebautes Image dorthin pushen und auf einer zweiten Kurs-VM wieder pullen.
 +
 
 +
== Ausblick: Docker Swarm ==
 +
 
 +
Docker Swarm ist die in Docker eingebaute Orchestrierungslösung für mehrere Hosts – ein Compose-File lässt sich mit wenigen Anpassungen als "Stack" über mehrere Knoten verteilen. Sinnvoll, sobald ein einzelner Host für Last oder Ausfallsicherheit nicht mehr ausreicht.
 +
 
 +
== Ausblick: Kubernetes ==
 +
 
 +
Kubernetes verfolgt dasselbe Grundproblem wie Swarm – Container-Orchestrierung über viele Hosts – ist aber deutlich umfangreicher und in der Praxis der De-facto-Standard für größere produktive Umgebungen. Compose-Kenntnisse lassen sich konzeptionell übertragen (Services, Volumes, Umgebungsvariablen), die konkrete Umsetzung unterscheidet sich aber deutlich.
 +
 
 +
= Zusammenfassung =
 +
 
 +
* Ein Image ist eine statische Vorlage, ein Container eine laufende Instanz davon
 +
* Dockerfile-Instruktionen erzeugen Layer; deren Reihenfolge entscheidet über effektives Caching
 +
* Alpine und Multi-Stage-Builds halten Images klein
 +
* Benutzerdefinierte Netzwerke ermöglichen Namensauflösung zwischen Containern
 +
* Volumes sind der empfohlene Weg für persistente Daten, Bind Mounts eher für lokale Entwicklung
 +
* Docker Compose bündelt Mehr-Container-Anwendungen in einer YAML-Datei
 +
* Healthchecks erweitern depends_on von "Container läuft" zu "Dienst ist bereit"
 +
* Non-Root-User und eingeschränkte Capabilities reduzieren die Angriffsfläche produktiver Container
 +
* Private Registries und Orchestrierung (Swarm/Kubernetes) sind die nächsten Schritte, sobald ein einzelner Host nicht mehr reicht
 +
 
 +
'''Abschlussaufgabe:''' Einen eigenen Compose-Stack (mindestens zwei Services, Netzwerk, Volume, .env-Datei, Healthcheck-basiertes depends_on, Non-Root-User in mindestens einem eigenen Dockerfile) von Grund auf planen, umsetzen und im Plenum vorstellen.

Aktuelle Version vom 7. August 2026, 18:52 Uhr

Docker – Von Images über Handling bis Compose

Docker Images

Container vs. virtuelle Maschine

Was ein Container ist

Ein Container ist ein isolierter Prozess, der sich den Kernel des Host-Systems mit allen anderen Containern teilt. Eine virtuelle Maschine bringt dagegen einen eigenen Kernel und ein eigenes Betriebssystem mit.

Warum das den Unterschied ausmacht
  • Container starten in Millisekunden, VMs brauchen Sekunden bis Minuten
  • Ein Container-Image ist oft nur wenige MB groß, ein VM-Image mehrere GB
  • Auf einem Linux-Host können beliebig viele Linux-Distributionen als Container laufen (Ubuntu, Alpine, Debian, ...) – sie teilen sich alle denselben Kernel
  • Ein Windows-Container kann NICHT auf einem Linux-Host laufen (ohne Emulation), weil der Kernel fehlt

Aufgabe: Skizzieren Sie den Unterschied Host → Hypervisor → VM vs. Host → Docker-Engine → Container.

Warum Docker?

Die drei klassischen Probleme klassischer Softwareverteilung
  • "Works on my machine" – unterschiedliche Umgebungen auf verschiedenen Rechnern
  • Abhängigkeitskonflikte – Programm A braucht Python 2.7, Programm B Python 3.9
  • Aufwändiges Setup neuer Server – Stunden für Installation und Konfiguration
Wie Docker das löst

Ein Image kapselt Code, Bibliotheken und Konfiguration zu einem festen Zeitpunkt. Das Image läuft danach überall identisch, wo Docker installiert ist.

Docker installieren und verifizieren

Docker auf Debian/Ubuntu installieren
  • sudo apt update
  • sudo apt install docker.io
  • sudo systemctl enable --now docker
  • sudo usermod -aG docker $USER

Nach usermod ist ein Neu-Login (oder newgrp docker) nötig, damit die Gruppenmitgliedschaft aktiv wird.

Installation prüfen
  • docker --version
  • docker run hello-world
Erwartete Ausgabe von hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.

Aufgabe: Docker installieren, hello-world ausführen und die Ausgabe dem Dozenten zeigen.

Images pullen und auflisten

Ein Image von Docker Hub herunterladen
  • docker pull alpine
  • docker pull alpine:3.18

Ohne Tag wird automatisch :latest angenommen – eine reine Konvention, keine Garantie für "neueste stabile Version".

Lokal vorhandene Images anzeigen
  • docker images
  • docker image ls

Beide liefern dieselbe Tabelle: REPOSITORY, TAG, IMAGE ID, CREATED, SIZE.

Aufgabe: alpine, alpine:3.18 und nginx:alpine pullen und die Größen mit docker images vergleichen.

Registries: Docker Hub, öffentlich und privat

Was eine Registry ist

Docker Hub ist die Standard-Registry – vergleichbar mit GitHub, nur für Images. Wird kein Registry-Präfix angegeben, nimmt Docker automatisch docker.io an.

Offizielle vs. Community-Images unterscheiden
  • Offizielle Images haben keinen Schrägstrich im Namen: ubuntu, nginx, python
  • Community-Images enthalten Nutzer- oder Organisationsnamen: myuser/myapp
Nach Images suchen
  • docker search nginx
Exakte Version über den Digest pullen (unveränderlich, im Gegensatz zu Tags)
  • docker inspect alpine:3.18 | grep Digest
  • docker pull alpine@sha256:xxxxx

Das Dockerfile: FROM, WORKDIR, COPY

Was ein Dockerfile ist

Eine Textdatei mit Bauanleitung für ein Image. Jede Instruktion erzeugt einen eigenen Layer. Konvention: Datei heißt Dockerfile (großes D, keine Endung).

FROM legt das Basis-Image fest, jedes Dockerfile beginnt damit (außer FROM scratch)
WORKDIR setzt das Arbeitsverzeichnis für alle folgenden RUN/CMD/COPY-Instruktionen
COPY kopiert Dateien vom Build-Kontext (lokales Verzeichnis) ins Image
Minimalbeispiel
FROM alpine:latest
WORKDIR /app
COPY index.html .

Aufgabe: Projektverzeichnis mit eigener index.html anlegen und ein Dockerfile mit FROM/WORKDIR/COPY dafür schreiben.

RUN und Layer

Was RUN macht

Jede RUN-Zeile führt einen Befehl während des Builds aus und erzeugt dabei einen eigenen Layer, der dauerhaft Teil des Images wird.

FROM alpine:latest
RUN apk add --no-cache curl
Image bauen
  • docker build -t my-first-image .

Der Punkt am Ende gibt den Build-Kontext an (das Verzeichnis, in dem nach dem Dockerfile gesucht wird).

Entstandene Layer ansehen
  • docker history my-first-image

Aufgabe: Das eigene Dockerfile um eine RUN-Zeile ergänzen, mit docker build -t webtest . bauen und mit docker history prüfen, wie viele Layer entstanden sind.

ENV und EXPOSE

Umgebungsvariablen im Image festlegen
ENV APP_HOME=/app
ENV NODE_ENV=production

Spätere Instruktionen können mit $VARIABLE_NAME darauf zugreifen. Beim Start lassen sich Werte überschreiben:

Umgebungsvariable beim Start überschreiben
  • docker run -e APP_HOME=/newpath myimage
Ports dokumentieren
EXPOSE 80

EXPOSE veröffentlicht den Port NICHT tatsächlich, es ist reine Dokumentation. Freigegeben wird ein Port erst beim Start:

Port tatsächlich auf den Host mappen
  • docker run -p 8080:80 mynginx

CMD vs. ENTRYPOINT

CMD – austauschbarer Standardbefehl
FROM alpine
CMD ["echo", "Hello from CMD"]

docker run myimage führt den CMD aus, docker run myimage echo overridden ersetzt ihn komplett.

ENTRYPOINT – fester Befehl
FROM alpine
ENTRYPOINT ["echo"]
CMD ["Hello"]

Der ENTRYPOINT bleibt fest, CMD liefert nur das austauschbare Standard-Argument.

Faustregel
  • Nur CMD → wenn Nutzer den kompletten Befehl frei überschreiben können sollen
  • Nur ENTRYPOINT → wenn das Image sich wie ein eigenständiges Kommando verhalten soll
  • Beides → fester Befehl mit austauschbaren Argumenten

Aufgabe: Zwei Varianten desselben Mini-Images bauen (einmal nur CMD, einmal ENTRYPOINT+CMD) und live testen, was docker run image ARGUMENT jeweils tatsächlich ausführt.

ADD vs. COPY

Warum meistens COPY die richtige Wahl ist

COPY kopiert lokale Dateien 1:1 ins Image – vorhersehbar, tut genau eine Sache.

Was ADD zusätzlich kann
  • Lokale tar-Archive automatisch entpacken
  • Dateien direkt von einer URL herunterladen

Für URL-Downloads ist RUN curl oder RUN wget vorzuziehen – klarer und mit mehr Kontrolle als ADD.

Layer-Caching und Instruktionsreihenfolge

Wie das Caching funktioniert

Docker berechnet für jeden Layer einen Hash aus Inhalt und Hash des vorherigen Layers. Ändert sich eine Zeile, wird ab dieser Zeile alles neu gebaut – die Layer darüber bleiben aus dem Cache.

Empfohlene Reihenfolge (selten → häufig ändernd)
  1. FROM
  2. RUN apt/apk install (ändert sich selten)
  3. COPY requirements.txt / package.json
  4. RUN pip/npm install (hängt von obiger Datei ab)
  5. COPY restlicher Code (ändert sich am häufigsten)

Schlechte Reihenfolge: COPY . . ganz am Anfang – dann invalidiert jede Codeänderung den kompletten nachfolgenden Cache.

Aufgabe: Ein eigenes Projekt zweimal hintereinander OHNE Änderung bauen (Cache sichtbar), dann eine Zeile im Anwendungscode ändern und beobachten, welche Layer neu gebaut werden. Danach bewusst schlechte Reihenfolge testen (COPY vor RUN install) und den Unterschied vergleichen.

Image-Verkleinerung: Alpine und Multi-Stage-Builds

Warum kleine Images wichtig sind
  • Schnelleres Pull/Push
  • Weniger Angriffsfläche
  • Weniger Speicherbedarf
Alpine Linux

Rund 5 MB statt 70–80 MB bei Ubuntu/Debian. Nutzt musl libc + busybox statt glibc + GNU coreutils. Viele offizielle Images bieten eine -alpine-Variante.

Multi-Stage-Build
FROM golang:alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o myapp

FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]

Die Build-Stage enthält Compiler und Quellcode, die finale Stage nur das fertige Binary.

Aufgabe: Eine kleine eigene Anwendung (Python-Flask, Node.js oder statische HTML-Seite) von Grund auf mit eigenem Dockerfile bauen – FROM/WORKDIR/COPY/RUN/CMD, sinnvolle Layer-Reihenfolge, Alpine-Basis.

Images verwalten

Image-Metadaten als JSON ansehen
  • docker inspect alpine:latest
Einzelne Felder gezielt auslesen
Ein Image entfernen
  • docker rmi my-first-image
Nicht mehr verwendete Images aufräumen
  • docker image prune

Aufgabe: Ein nicht mehr benötigtes Test-Image aus einer früheren Übung gezielt entfernen und danach mit docker image prune aufräumen.

Images teilen

Bei Docker Hub anmelden
  • docker login
Image für den Upload mit dem eigenen Benutzernamen taggen
  • docker tag my-first-image johnsmith/my-first-image:1.0
Image hochladen
  • docker push johnsmith/my-first-image:1.0
Image auf einem anderen Rechner herunterladen und starten
  • docker pull johnsmith/my-first-image:1.0
  • docker run johnsmith/my-first-image:1.0

Aufgabe: Ein eigenes Image mit dem eigenen Docker-Hub-Konto taggen und pushen (oder alternativ gegen die spätere lokale Registry aus dem Security-/Registry-Teil).

Images offline sichern

Ein Image als Datei exportieren
  • docker save -o myimage.tar my-first-image:latest
Ein Image aus einer Datei importieren
  • docker load -i myimage.tar

Sinnvoll für Umgebungen ohne Registry-Zugriff (Firewall, air-gapped Systeme) oder zur Archivierung einer bestimmten Version.

Typische Anfängerfehler

Änderung am Code wird nicht übernommen

Das Image enthält noch den alten Stand – nach Codeänderungen muss neu gebaut werden: docker build -t myimage .

latest ist keine sichere Wahl

ubuntu:latest zeigt heute auf 22.04, in einem Monat womöglich auf 24.04 – für reproduzierbare Builds immer eine konkrete Version wie ubuntu:22.04 verwenden.

Riesige Images durch unnötige Dateien

Mit einer .dockerignore-Datei (analog zu .gitignore) lassen sich node_modules, .git, Logs und Secrets vom Build-Kontext ausschließen.

Container läuft standardmäßig als root

Sicherheitsrisiko – siehe Abschnitt "Container-Security" weiter unten.

Troubleshooting

Build bricht mit "Unable to locate package" ab

Meist fehlt ein vorheriges apt update/apk update vor der Installation.

"Unable to find image ... locally"

Das referenzierte Image existiert lokal nicht und konnte auch nicht von der Registry geladen werden – Image-Name/Tag prüfen oder zuerst selbst bauen.

Cache greift nicht mehr

Eine frühe Zeile im Dockerfile hat sich geändert – der Cache bricht ab dieser Stelle linear ab. Reihenfolge nach Änderungshäufigkeit prüfen (siehe oben).

Image läuft bei mir, aber nicht beim Kollegen

Häufigste Ursachen: unterschiedliche Auflösung von latest, fehlende COPY-Dateien (stattdessen lokale Bind Mounts genutzt), oder Umgebungsvariablen, die nur lokal gesetzt sind.

Docker Container-Handling

Kurzwiederholung

Ein Image ist eine statische, read-only Vorlage. Ein Container ist eine laufende Instanz dieser Vorlage mit einer eigenen beschreibbaren Schicht obendrauf. Ab hier geht es um den täglichen Umgang mit laufenden Containern.

Der Container-Lifecycle

Einen neuen Container aus einem Image erzeugen und starten
  • docker run <image>
Einen bestehenden, gestoppten Container fortsetzen
  • docker start <container>

run erzeugt jedes Mal einen NEUEN Container. start setzt einen vorhandenen fort – inklusive aller Änderungen in seiner beschreibbaren Schicht.

Einen laufenden Container stoppen
  • docker stop <container>
Einen Container neu starten
  • docker restart <container>
Einen gestoppten Container endgültig entfernen
  • docker rm <container>
Laufende bzw. alle Container anzeigen
  • docker ps
  • docker ps -a

Aufgabe: Einen Container starten, stoppen, wieder starten und schließlich entfernen – bei jedem Schritt docker ps -a prüfen.

Wichtige Flags beim Starten

-d detached – Container läuft im Hintergrund
-it interaktiv mit Pseudo-TTY – für Shells und interaktive Programme
--rm Container wird nach dem Beenden automatisch entfernt
--name vergibt einen festen, sprechenden Namen statt einer zufälligen ID
-p host:container Port-Mapping vom Host auf den Container
Container im Hintergrund mit festem Namen und Port-Mapping starten
  • docker run -d --name webserver -p 8080:80 nginx:alpine

In laufende Container hineinschauen

Logs eines Containers ansehen
  • docker logs <container>
Der Ausgabe live folgen
  • docker logs -f <container>
Einen zusätzlichen Befehl im laufenden Container ausführen
  • docker exec -it <container> /bin/sh
  • docker exec <container> ps aux

exec startet einen zusätzlichen Prozess IM bereits laufenden Container – der Hauptprozess läuft unbeeinflusst weiter.

Aufgabe: Einen nginx-Container starten, per exec eine Shell öffnen, dort eine Datei unter /usr/share/nginx/html ändern und die Änderung im Browser/mit curl vom Host aus prüfen.

Metadaten auslesen: inspect

Vollständige Metadaten eines Containers ansehen
  • docker inspect <container>

Liefert IP-Adresse, Mount-Punkte, Umgebungsvariablen, Netzwerkeinstellungen, Restart-Policy und mehr als JSON.

Ein einzelnes Feld gezielt auslesen

Netzwerke: bridge, host, none

bridge Standard. Eigene IP in einem privaten Docker-Netz, NAT zum Host
host teilt sich den Netzwerk-Stack direkt mit dem Host – kein NAT, keine Isolation
none keine Netzwerkanbindung

Eigene Netzwerke und Container-DNS

Warum ein eigenes Netzwerk nötig ist

Im Standard-bridge-Netzwerk können Container sich NICHT über ihren Namen erreichen, nur über IP. Benutzerdefinierte Netzwerke bringen automatisch Namensauflösung mit.

Eigenes Netzwerk anlegen
  • docker network create meinnetz
Zwei Container im selben Netzwerk starten
  • docker run -d --name db --network meinnetz postgres
  • docker run -d --name app --network meinnetz --env DB_HOST=db myapp

Innerhalb von meinnetz kann app den Hostnamen db direkt auflösen.

Netzwerke verwalten
  • docker network ls
  • docker network inspect meinnetz
  • docker network rm meinnetz

Aufgabe: Zwei eigene Container in einem selbst angelegten Netzwerk starten und aus dem einen Container heraus den anderen per Namen anpingen bzw. curlen.

Daten dauerhaft speichern: Volumes und Bind Mounts

Warum das wichtig ist

Ohne Vorkehrung gehen alle Daten eines Containers verloren, sobald er entfernt wird – sie liegen nur in seiner beschreibbaren Schicht.

Ein Volume anlegen und einem Container zuweisen
  • docker volume create meindaten
  • docker run -d --name db -v meindaten:/var/lib/postgresql/data postgres

Docker verwaltet den Speicherort selbst – Standardweg für persistente Anwendungsdaten wie Datenbanken.

Einen Host-Pfad direkt einhängen (Bind Mount)
  • docker run -d --name web -v /home/user/website:/usr/share/nginx/html nginx

Praktisch für Entwicklung (Codeänderungen sofort sichtbar), aber abhängig von der Verzeichnisstruktur des jeweiligen Hosts.

Volumes verwalten
  • docker volume ls
  • docker volume inspect meindaten
  • docker volume rm meindaten
  • docker volume prune

Aufgabe: Einen Datenbank-Container mit einem Volume starten, einen Datensatz anlegen, den Container mit docker rm -f entfernen, einen neuen Container mit demselben Volume starten und prüfen, dass der Datensatz noch vorhanden ist.

Ressourcenlimits

Speicher und CPU eines Containers begrenzen
  • docker run -d --memory=512m --cpus=1.5 myapp

--memory ist eine harte RAM-Obergrenze – bei Überschreitung beendet der Kernel-OOM-Killer den Prozess. --cpus begrenzt die maximale Anzahl an CPU-Kernen, auch als Bruchteil.

Neustart-Strategien

no Standard. Nie automatisch neu starten
on-failure nur bei Fehler-Exitcode
always immer, auch nach Host-Neustart
unless-stopped wie always, außer der Container wurde manuell gestoppt
Restart-Policy beim Start festlegen
  • docker run -d --restart unless-stopped nginx

Für produktiv laufende Dienste ist unless-stopped meist die sinnvollste Wahl.

Healthchecks

Warum ein laufender Prozess nicht reicht

Ein Prozess kann laufen, ohne dass die Anwendung dahinter tatsächlich funktioniert (z. B. hängender Webserver). Healthchecks prüfen aktiv den Zustand.

HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1

docker ps zeigt danach zusätzlich zu "Up" den Status "healthy" oder "unhealthy" an.

Aufgabe: Einem eigenen Dockerfile einen HEALTHCHECK hinzufügen, das Image bauen, den Container starten und in docker ps den Health-Status beobachten.

Troubleshooting

Container beendet sich sofort wieder

Meist läuft der Hauptprozess kurz und terminiert dann. Prüfen mit docker logs <container>.

"Bind for 0.0.0.0
80 failed: port is already allocated"

Der Host-Port ist bereits belegt. Prüfen mit ss -tulpn | grep :80 oder anderen Host-Port wählen.

Container erreicht anderen Container nicht per Namen

Beide müssen im selben, benutzerdefinierten Netzwerk laufen – im Standard-bridge-Netz funktioniert Namensauflösung nicht.

Daten sind nach docker rm weg

Ohne Volume oder Bind Mount ist das erwartetes Verhalten, kein Fehler.

Docker Compose

Warum Compose?

Sobald eine Anwendung aus mehreren Containern besteht (z. B. Webserver + Datenbank + Cache), wird das manuelle Verketten von docker run-Befehlen mit Netzwerken und Volumes schnell unübersichtlich. Docker Compose beschreibt den kompletten Stack in einer einzigen YAML-Datei und startet ihn mit einem Befehl.

YAML-Grundlagen

Grundsyntax

YAML nutzt Einrückung (Leerzeichen, keine Tabs) statt Klammern. Listen beginnen mit einem Bindestrich, Schlüssel-Wert-Paare werden mit Doppelpunkt getrennt.

schluessel: wert
liste:
  - eintrag1
  - eintrag2
verschachtelt:
  unterschluessel: wert

Grundstruktur einer compose.yml

Minimalbeispiel
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

networks:
  default:

volumes:
  webdata:

Die drei Hauptblöcke: services (die eigentlichen Container), networks und volumes (beide optional, werden bei Bedarf automatisch mit Standardwerten angelegt).

Ein erster Service

Stack im Hintergrund starten
  • docker compose up -d
Status aller Services anzeigen
  • docker compose ps
Stack stoppen und entfernen
  • docker compose down

up baut/pullt bei Bedarf und startet alle Services. down entfernt Container und Standard-Netzwerke, Volumes bleiben standardmäßig erhalten.

Aufgabe: Eine minimale compose.yml mit einem nginx-Service und Port-Mapping schreiben, starten und im Browser aufrufen.

build vs. image

Ein eigenes Dockerfile über Compose bauen und gleichzeitig taggen
services:
  app:
    build: .
    image: meinapp:1.0

Mit build baut Compose ein eigenes Dockerfile aus dem angegebenen Verzeichnis. Mit image allein wird stattdessen ein vorhandenes Image gepullt.

Mehrere Services verbinden

Zwei Services, die sich gegenseitig per Name erreichen
services:
  app:
    build: .
    environment:
      DB_HOST: db
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: geheim

Alle Services eines Compose-Projekts landen automatisch im selben, projektspezifischen Netzwerk.

Startreihenfolge: depends_on

services:
  app:
    build: .
    depends_on:
      - db
  db:
    image: postgres:16

depends_on wartet standardmäßig nur, bis der Container GESTARTET ist – nicht, bis der Dienst darin tatsächlich BEREIT ist. Für echtes Warten auf Betriebsbereitschaft: Kombination mit Healthchecks (siehe unten).

Aufgabe: Zwei Services (App + Datenbank) mit depends_on verbinden und beobachten, dass die App evtl. startet, bevor die Datenbank bereit ist.

Volumes in Compose

services:
  db:
    image: postgres:16
    volumes:
      - dbdata:/var/lib/postgresql/data

volumes:
  dbdata:

Umgebungsvariablen und .env-Dateien

Variablen aus einer .env-Datei einbinden
services:
  app:
    build: .
    env_file: .env
# .env
DB_PASSWORD=geheim
APP_PORT=8080

Werte lassen sich zusätzlich direkt in der compose.yml referenzieren: "${APP_PORT}:80". Das trennt Konfiguration von Code und verhindert, dass Zugangsdaten fest im Compose-File landen.

Aufgabe: Die Datenbank-Zugangsdaten aus der vorherigen Übung in eine .env-Datei auslagern und per env_file einbinden.

Healthchecks als Startbedingung

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy

Damit wartet app nicht nur, bis der db-Container läuft, sondern bis der Healthcheck tatsächlich "healthy" meldet.

Aufgabe: Den Stack aus der depends_on-Übung um einen Healthcheck für die Datenbank erweitern und prüfen, dass die App jetzt erst nach "healthy" startet.

Profiles

services:
  app:
    build: .
  debug-tools:
    image: busybox
    profiles: ["debug"]
Nur Services mit einem bestimmten Profil zusätzlich starten
  • docker compose --profile debug up

Services mit profiles starten NICHT bei einem normalen docker compose up.

Override-Files

Compose liest standardmäßig automatisch sowohl docker-compose.yml als auch docker-compose.override.yml und führt beide zusammen.

Explizit mehrere Compose-Dateien kombinieren
  • docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Skalieren mit --scale

Mehrere Instanzen eines Services starten
  • docker compose up -d --scale app=3

Voraussetzung: Der Service darf keinen festen Host-Port über ports belegen, sonst kommt es zum Portkonflikt.

Die wichtigsten Compose-Befehle im Überblick

Stack im Hintergrund starten
  • docker compose up -d
Stack stoppen und entfernen
  • docker compose down
Zusätzlich auch Volumes entfernen
  • docker compose down -v
Status aller Services
  • docker compose ps
Logs eines einzelnen Services live verfolgen
  • docker compose logs -f <service>
Shell in einem laufenden Service öffnen
  • docker compose exec <service> sh
Einzelnen Service neu starten
  • docker compose restart <service>

Troubleshooting

"service depends on undefined service"

Tippfehler im Service-Namen bei depends_on – Name muss exakt mit dem Schlüssel unter services: übereinstimmen.

Änderungen am Dockerfile werden nicht übernommen

Nach Dockerfile-Änderungen: docker compose up -d --build.

Service startet vor der Datenbank, obwohl depends_on gesetzt ist

Ohne Healthcheck-Bedingung wartet Compose nur auf den Containerstart, nicht auf Betriebsbereitschaft.

Aufgabe (Mini-Stack): Einen kompletten Stack aus Webanwendung und Datenbank mit eigenem Dockerfile, Netzwerk, Volume, .env-Datei und Healthcheck-basiertem depends_on von Grund auf selbst schreiben und starten.

Container-Security

Rootless Docker

Warum das relevant ist

Standardmäßig läuft der Docker-Daemon als root. Bricht jemand aus einem Container aus, hat er potenziell Root-Rechte auf dem Host. Rootless Docker führt den kompletten Daemon als normaler Benutzer aus.

Kurzüberblick zur Einrichtung
  • dockerd-rootless-setuptool.sh install

Details hängen von der jeweiligen Distribution ab – im Kurs wird der Ablauf live an einer Testmaschine gezeigt.

Als Non-Root-User laufen

Eigenen Benutzer im Dockerfile anlegen und nutzen
FROM ubuntu:22.04
RUN useradd -m -s /bin/bash myuser
USER myuser
WORKDIR /home/myuser

Manche Basis-Images bringen bereits einen passenden Nutzer mit (node für Node.js, www-data für PHP) – diese nach Möglichkeit verwenden statt einen eigenen anzulegen.

Aufgabe: Das eigene Dockerfile aus den vorherigen Übungen um einen Non-Root-User erweitern und mit docker exec <container> whoami bestätigen, dass der Container nicht mehr als root läuft.

Capabilities einschränken

Alle Capabilities entfernen und nur die nötigen zurückgeben
  • docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp

Reduziert die Angriffsfläche, falls ein Container kompromittiert wird – er bekommt nur die Kernel-Rechte, die er tatsächlich braucht.

Private Registry und Ausblick

Eigene private Registry betreiben

Eine lokale Registry als Container starten
  • docker run -d -p 5000:5000 --name registry registry:2
Ein Image für die lokale Registry taggen
  • docker tag myimage localhost:5000/myimage
In die lokale Registry pushen
  • docker push localhost:5000/myimage
Aus der lokalen Registry pullen
  • docker pull localhost:5000/myimage

Aufgabe: Eine eigene lokale Registry starten, ein selbst gebautes Image dorthin pushen und auf einer zweiten Kurs-VM wieder pullen.

Ausblick: Docker Swarm

Docker Swarm ist die in Docker eingebaute Orchestrierungslösung für mehrere Hosts – ein Compose-File lässt sich mit wenigen Anpassungen als "Stack" über mehrere Knoten verteilen. Sinnvoll, sobald ein einzelner Host für Last oder Ausfallsicherheit nicht mehr ausreicht.

Ausblick: Kubernetes

Kubernetes verfolgt dasselbe Grundproblem wie Swarm – Container-Orchestrierung über viele Hosts – ist aber deutlich umfangreicher und in der Praxis der De-facto-Standard für größere produktive Umgebungen. Compose-Kenntnisse lassen sich konzeptionell übertragen (Services, Volumes, Umgebungsvariablen), die konkrete Umsetzung unterscheidet sich aber deutlich.

Zusammenfassung

  • Ein Image ist eine statische Vorlage, ein Container eine laufende Instanz davon
  • Dockerfile-Instruktionen erzeugen Layer; deren Reihenfolge entscheidet über effektives Caching
  • Alpine und Multi-Stage-Builds halten Images klein
  • Benutzerdefinierte Netzwerke ermöglichen Namensauflösung zwischen Containern
  • Volumes sind der empfohlene Weg für persistente Daten, Bind Mounts eher für lokale Entwicklung
  • Docker Compose bündelt Mehr-Container-Anwendungen in einer YAML-Datei
  • Healthchecks erweitern depends_on von "Container läuft" zu "Dienst ist bereit"
  • Non-Root-User und eingeschränkte Capabilities reduzieren die Angriffsfläche produktiver Container
  • Private Registries und Orchestrierung (Swarm/Kubernetes) sind die nächsten Schritte, sobald ein einzelner Host nicht mehr reicht

Abschlussaufgabe: Einen eigenen Compose-Stack (mindestens zwei Services, Netzwerk, Volume, .env-Datei, Healthcheck-basiertes depends_on, Non-Root-User in mindestens einem eigenen Dockerfile) von Grund auf planen, umsetzen und im Plenum vorstellen.