Docker Kurs: Unterschied zwischen den Versionen
| (5 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
| − | + | '''Docker – Von Images über Handling bis Compose''' | |
| − | + | __TOC__ | |
| − | + | = 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 <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)
- 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.
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
- docker inspect alpine:latest --format='Vorlage:.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 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
- docker inspect --format='Vorlage:.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 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.