Docker Kurs
Docker – Von Images über Handling bis Compose
Docker Images
Container vs. virtuelle Maschine
- Was ist ein Container?
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 ist das wichtig?
- 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
Übung: Zeichnen Sie den Unterschied Host → Hypervisor → VM vs. Host → Docker-Engine → Container auf. Keine Rechnerarbeit, reine Verständnisübung.
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 Zeitpunkt. Das Image läuft danach überall identisch, wo Docker installiert ist.
Docker installieren und verifizieren
- Installation (Debian/Ubuntu)
- sudo apt update
- sudo apt install docker.io
- sudo systemctl enable --now docker
- sudo usermod -aG docker $USER
Nach dem usermod ist ein Neu-Login (oder newgrp docker) nötig, damit die Gruppenmitgliedschaft aktiv wird.
- Installation prüfen
- docker --version
- docker run hello-world
$ docker run 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.
Übung: Docker installieren, hello-world ausführen und die Ausgabe bestätigen.
Erste Images pullen und auflisten
- Images herunterladen
- docker pull alpine
- docker pull alpine:3.18
Ohne Tag wird automatisch :latest angenommen. latest ist eine reine Konvention – keine Garantie für "neueste stabile Version".
- Lokale Images ansehen
- docker images
- docker image ls
Beide Befehle liefern dieselbe Tabelle: REPOSITORY, TAG, IMAGE ID, CREATED, SIZE.
Übung: alpine, alpine:3.18 und nginx:alpine pullen, Größen mit docker images vergleichen.
Das Dockerfile: FROM, WORKDIR, COPY
- Was ist ein Dockerfile?
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; wird bei Bedarf automatisch angelegt |
| COPY | kopiert Dateien vom Build-Kontext (lokales Verzeichnis) ins Image |
FROM alpine:latest
WORKDIR /app
COPY index.html .
Übung: Projektverzeichnis mit eigener index.html anlegen, Dockerfile mit FROM/WORKDIR/COPY schreiben.
RUN und Layer
- RUN – Befehle während des Builds ausführen
- Jede RUN-Zeile erzeugt einen eigenen Layer
- Ergebnisse werden dauerhaft Teil des Images
FROM alpine:latest
RUN apk add --no-cache curl
- Bauen und prüfen
- docker build -t my-first-image .
- docker images
- docker history my-first-image
Der Punkt am Ende von docker build gibt den Build-Kontext an (das Verzeichnis, in dem nach dem Dockerfile gesucht wird).
Übung: Dockerfile um eine RUN-Zeile ergänzen, mit docker build -t webtest . bauen, mit docker history die entstandenen Layer prüfen.
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"]
docker run myimage → echo Hello. docker run myimage Goodbye → echo Goodbye. Der ENTRYPOINT bleibt fest, CMD liefert nur das 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
Übung: Zwei Varianten desselben Mini-Images bauen (einmal nur CMD, einmal ENTRYPOINT+CMD), live testen, was docker run image ARGUMENT jeweils ausführt.
Layer-Caching und Instruktionsreihenfolge
- Wie 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.
- Gute 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: COPY . . ganz am Anfang – dann invalidiert jede Codeänderung den kompletten nachfolgenden Cache.
Übung: 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 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 (python:alpine, node:alpine, nginx:alpine).
- Multi-Stage-Builds
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.
Übungsprojekt: eigenes Dockerfile
Eine kleine eigene Anwendung wählen (Python-Flask, Node.js oder eine statische HTML-Seite), dafür von Grund auf ein eigenes Dockerfile schreiben (FROM/WORKDIR/COPY/RUN/CMD, sinnvolle Layer-Reihenfolge, Alpine-Basis) und erfolgreich bauen.
Docker Container-Handling
Kurzwiederholung: Image vs. Container
Ein Image ist eine statische, read-only Vorlage. Ein Container ist eine laufende Instanz dieser Vorlage mit einer eigenen beschreibbaren Schicht obendrauf. In diesem Teil geht es um den täglichen Umgang mit laufenden Containern: starten, stoppen, debuggen, vernetzen, Daten sichern.
Der Container-Lifecycle
- Grundbefehle
- docker run <image> – erzeugt einen neuen Container und startet ihn
- docker start <container> – startet einen bereits existierenden, gestoppten Container
- docker stop <container> – stoppt einen laufenden Container (sendet SIGTERM, nach Timeout SIGKILL)
- docker restart <container> – stoppt und startet neu
- docker rm <container> – entfernt einen gestoppten Container endgültig
- docker ps – zeigt laufende Container
- docker ps -a – zeigt auch gestoppte Container
Ein wichtiger Unterschied zu docker run: run erzeugt jedes Mal einen NEUEN Container aus dem Image. start setzt einen vorhandenen Container fort – inklusive aller Änderungen in seiner beschreibbaren Schicht.
Wichtige Flags beim Starten
| -d | detached – Container läuft im Hintergrund, Terminal bleibt frei |
| -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 |
docker run -d --name webserver -p 8080:80 nginx:alpine
In laufende Container hineinschauen
- Logs ansehen
- docker logs <container>
- docker logs -f <container> – folgt der Ausgabe live (wie tail -f)
- Befehle in einem 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.
Metadaten auslesen: inspect
- docker inspect <container>
Liefert ein vollständiges JSON-Objekt: IP-Adresse, Mount-Punkte, Umgebungsvariablen, Netzwerkeinstellungen, Restart-Policy und mehr.
docker inspect --format='{{.NetworkSettings.IPAddress}}' webserver
Netzwerke: bridge, host, none
| bridge | Standard. Container bekommt eine eigene IP in einem privaten Docker-Netz, NAT zum Host |
| host | Container teilt sich den Netzwerk-Stack direkt mit dem Host – kein NAT, keine Isolation |
| none | Container hat gar keine Netzwerkanbindung |
Eigene Netzwerke & Container-DNS
Das Standard-bridge-Netzwerk hat einen entscheidenden Nachteil: Container können sich darin NICHT über ihren Namen erreichen, nur über IP. Eigene, benutzerdefinierte Netzwerke lösen das.
- Eigenes Netzwerk anlegen und nutzen
- docker network create meinnetz
- docker run -d --name db --network meinnetz postgres
- docker run -d --name app --network meinnetz --env DB_HOST=db myapp
Innerhalb von meinnetz kann der Container app den Hostnamen db direkt auflösen – Docker betreibt dafür einen eingebauten DNS-Server für jedes benutzerdefinierte Netzwerk.
- Netzwerke verwalten
- docker network ls
- docker network inspect meinnetz
- docker network rm meinnetz
Daten dauerhaft speichern: Volumes vs. Bind Mounts
Ohne besondere Vorkehrung gehen alle Daten eines Containers verloren, sobald er entfernt wird.
- Volumes (von Docker verwaltet)
- docker volume create meindaten
- docker run -d --name db -v meindaten:/var/lib/postgresql/data postgres
Docker verwaltet den Speicherort selbst. Empfohlener Standardweg für persistente Anwendungsdaten wie Datenbanken.
- Bind Mounts (Host-Pfad direkt einhängen)
- 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 – entfernt alle nicht mehr referenzierten Volumes
Ressourcenlimits
docker run -d --memory=512m --cpus=1.5 myapp
- --memory – harte Obergrenze für RAM; bei Überschreitung wird der Container-Prozess vom Kernel-OOM-Killer beendet
- --cpus – maximale Anzahl an CPU-Kernen (auch als Bruchteil, z. B. 0.5)
Neustart-Strategien: Restart Policies
| no | Standard. Container wird nie automatisch neu gestartet |
| on-failure | nur bei einem Fehler-Exitcode (ungleich 0) |
| always | immer, egal wie der Container beendet wurde – auch nach Host-Neustart |
| unless-stopped | wie always, außer der Container wurde explizit manuell gestoppt |
docker run -d --restart unless-stopped nginx
Healthchecks
Ein laufender Prozess bedeutet nicht zwangsläufig, dass die Anwendung auch tatsächlich funktioniert. Healthchecks prüfen aktiv den Zustand.
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1
docker ps zeigt danach neben "Up" zusätzlich den Status "healthy" oder "unhealthy" an.
Troubleshooting häufiger Handling-Fehler
- 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 einen anderen Host-Port wählen.
- Container kann anderen Container nicht per Namen erreichen
Beide Container müssen im selben, benutzerdefinierten Netzwerk laufen (siehe 2.7).
- Daten sind nach docker rm weg
Ohne Volume oder Bind Mount ist das erwartetes Verhalten (siehe 2.8).
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 für Compose
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
Die Grundstruktur einer compose.yml
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
docker compose up -d
docker compose ps
docker compose down
up baut/pullt bei Bedarf und startet alle Services. down stoppt und entfernt Container und Standard-Netzwerke (Volumes bleiben standardmäßig erhalten).
build vs. image
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
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 und können sich gegenseitig über den Service-Namen erreichen.
Startreihenfolge: depends_on
services:
app:
build: .
depends_on:
- db
db:
image: postgres:16
Wichtig: 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 3.10).
Volumes in Compose
services:
db:
image: postgres:16
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
Umgebungsvariablen & .env-Dateien
services:
app:
build: .
env_file: .env
# .env
DB_PASSWORD=geheim
APP_PORT=8080
Werte aus der .env-Datei 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.
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.
Profiles
services:
app:
build: .
debug-tools:
image: busybox
profiles: ["debug"]
Services mit profiles starten NICHT bei einem normalen docker compose up. Nur explizit mit docker compose --profile debug up.
Override-Files
Compose liest standardmäßig automatisch sowohl docker-compose.yml als auch docker-compose.override.yml und führt beide zusammen.
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Skalieren mit --scale
docker compose up -d --scale app=3
Voraussetzung: Der Service darf keinen festen Host-Port über ports belegen (sonst Portkonflikt).
Die wichtigsten Compose-Befehle im Überblick
- docker compose up -d – Stack im Hintergrund starten
- docker compose down – Stack stoppen und entfernen
- docker compose down -v – zusätzlich auch Volumes entfernen
- docker compose ps – Status aller Services
- docker compose logs -f <service> – Logs eines einzelnen Services live verfolgen
- docker compose exec <service> sh – Shell in einem laufenden Service öffnen
- docker compose restart <service> – einzelnen Service neu starten
Troubleshooting häufiger Compose-Fehler
- "service depends on undefined service"
Tippfehler im Service-Namen bei depends_on.
- Ä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 (siehe 3.10) wartet Compose nur auf den Containerstart, nicht auf Betriebsbereitschaft.
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"
Als nächste Schritte bieten sich an: Container-Security (rootless Docker, Capabilities), private Registries für eigene Images sowie ein Ausblick auf Orchestrierung mit Docker Swarm oder Kubernetes, sobald ein einzelner Host nicht mehr ausreicht.