Wie kommt dieselbe Anwendung auf einen anderen Rechner?
Auf deinem Laptop läuft die Anwendung. Auf dem Server fehlt eine Bibliothek, eine andere Version verhält sich anders und der Startbefehl ist nirgends dokumentiert. Containerisierung packt das Programm und seine benötigte Benutzerumgebung in ein wiederverwendbares Image. Docker ist eine verbreitete Plattform, um solche Images zu bauen, zu verteilen und als Container auszuführen. Der Host stellt weiterhin Kernel, Netzwerk und Speicher bereit.
Ein Linux-Container ist ein isolierter Prozess oder eine Gruppe von Prozessen mit einer eigenen Sicht auf Teile des Systems. Namespaces trennen beispielsweise Prozessnummern und Netzwerkschnittstellen; cgroups ordnen Ressourcen zu und können sie begrenzen. Ein Container besitzt dabei keinen eigenen Linux-Kernel. Auf macOS und Windows laufen gewöhnliche Linux-Container deshalb in einer Linux-VM. Die Grundlagen dazu stehen im Artikel Virtualisierung, VMs und Proxmox.
Die Docker Engine umfasst unter anderem den Dienst, der Images, Container, Netzwerke und Volumes verwaltet. Die Docker CLI ist das Kommandozeilenprogramm, das Aufträge an diesen Dienst schickt. Der Dienst kann lokal oder auf einem anderen Rechner laufen. Ein korrekt eingegebener Befehl kann deshalb den falschen Host verändern: Prüfe vor einem Labor den ausgewählten Docker-Kontext mit docker context show.
Image ist die Vorlage, Container die laufende Instanz.
Ein Image enthält Dateisystemschichten und Metadaten wie Startbefehl und Standardbenutzer. Die Schichten, englisch Layers, lassen sich zwischen Images wiederverwenden. Beim Build erzeugen dateiverändernde Dockerfile-Anweisungen neue Schichten; passende Zwischenergebnisse können aus dem Build-Cache kommen. Das spart Arbeit, verlangt aber einen bewussten Umgang mit Versionsständen. Eine gelöschte Datei kann in einer früheren Schicht weiterhin enthalten sein. Zugangsdaten gehören deshalb niemals in das Dockerfile oder den Build-Kontext.
Ein Container entsteht aus einem Image und bekommt eine eigene beschreibbare Schicht sowie Laufzeitkonfiguration. Zwei Container desselben Images teilen nicht automatisch ihre Änderungen. Stoppen beendet den Prozess, erhält aber den vorhandenen Container. Erneutes Starten setzt dessen Dateisystem fort. Entfernen und Neuerstellen verwirft seine beschreibbare Schicht. Dauerhafte Anwendungsdaten müssen außerhalb dieser Schicht liegen.
Eine Registry ist ein Dienst zum Speichern und Verteilen von Images, etwa Docker Hub oder eine eigene Registry. Ein Tag wie python:3.13-alpine bezeichnet eine bewegliche Referenz. Ein Digest wie sha256:… bezeichnet einen konkreten Inhalt. Für ein reproduzierbares Release prüfst du Herkunft und Updates und hältst den getesteten Digest fest. „latest“ bedeutet weder geprüft noch automatisch sicher. Auch die hier bewusst lesbaren Versionstags sind keine unveränderliche Lieferkette.
Der übliche Lebenszyklus lautet: Image bauen oder herunterladen, Container erstellen, Prozess starten, beobachten, stoppen und bei einer neuen Imageversion neu erstellen. docker ps zeigt laufende Container, docker ps -a zusätzlich gestoppte. docker images zeigt lokale Images. Diese lesenden Befehle helfen, Vorlage und Instanz auseinanderzuhalten.
Netzwerk, Ports und Daten haben getrennte Aufgaben.
Ein benutzerdefiniertes Bridge-Netz verbindet Container auf demselben Docker-Host. Compose richtet für dieses Labor ein solches Projektnetz ein. Darin können die Services einander über ihre Namen erreichen. app:8000 meint den Service app und dessen Containerport 8000. Die IP-Adresse kann sich bei einer Neuerstellung ändern; der Servicename bleibt die passende Konfigurationsreferenz. Das baut auf Namensauflösung und IP-Routing auf, ist aber kein eigenes physisches VLAN.
Port-Mapping veröffentlicht einen Containerport über den Host. 127.0.0.1:8080:8080 bedeutet Hostadresse, Hostport und Containerport. Unser Browser verwendet den Hostport. Der Proxy spricht intern den Containerport der Anwendung an. Ohne Veröffentlichung bleibt die Anwendung für andere Container im gemeinsamen Netz erreichbar. Das Weglassen eines Hostports ist daher keine vollständige Isolation zwischen Services. localhost in einem Container meint grundsätzlich diesen Container, nicht den Nachbarservice.
Ein Named Volume wird von Docker verwaltet und hat einen vom Container unabhängigen Lebenszyklus. Es eignet sich hier für den Besuchszähler. Ein Bind Mount bindet dagegen einen konkreten Hostpfad ein, hier eine von dir kontrollierte Proxykonfiguration. :ro macht diesen Mount aus Sicht des Containers schreibgeschützt. Große oder sensible Hostverzeichnisse gehören nicht pauschal in einen Container.
Ein Volume überlebt eine gewöhnliche Neuerstellung und docker compose down. Es schützt nicht vor einer defekten Festplatte oder einer Anwendung, die ihre eigenen Daten löscht. Für wichtige Daten brauchst du eine anwendungskonsistente Sicherung und einen geprüften Wiederherstellungsweg. Das Löschen eines Volumes entfernt seine Daten; entsprechende Löschoptionen gehören nicht in einen routinemäßigen Stop-Befehl.
Ein lokales Labor mit Proxy und Besuchszähler.
Das Beispiel nutzt einen Python-Dienst und einen vorgeschalteten NGINX-Proxy. Der Python-Dienst zählt Aufrufe von / in einer Datei; /health prüft seine HTTP-Antwort, ohne den Zähler zu verändern. Nur der Proxy bekommt einen veröffentlichten Port, gebunden an die Loopbackadresse. Das Labor enthält keine Anmeldung, keine echten Nutzerdaten und kein öffentliches TLS-Setup. Es ist keine Vorlage für einen öffentlich erreichbaren Produktivdienst.
Voraussetzung sind eine lokal eingerichtete Docker Engine für Linux-Container und das moderne Compose-Plugin mit docker compose. Verwende einen aktuellen unterstützten Stand und prüfe docker compose version. Die historische eigenständige Schreibweise docker-compose ist hier nicht gemeint. Die aktuelle Compose Specification ersetzt die früheren Dateiformatnummern. Ein oberstes version:-Feld ist obsolet und fehlt bewusst.
Lege die folgenden vier Dateien in einem neuen eigenen Laborverzeichnis ab. Die zusätzliche .dockerignore darunter begrenzt den Build-Kontext auf die benötigten Dateien. Alle Dateinamen und relativen Pfade beziehen sich auf dieses Verzeichnis.
compose.yaml
name: bitsicht-compose-lab
services:
app:
build: .
environment:
APP_TITLE: BitSicht Containerlabor
volumes:
- visits:/data
healthcheck:
test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
restart: "no"
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
web:
image: nginxinc/nginx-unprivileged:stable-alpine
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
app:
condition: service_healthy
restart: "no"
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
volumes:
visits:
name legt den Projektnamen fest. services enthält die beiden Dienste. build: . baut das App-Image aus dem aktuellen Verzeichnis; image beim Proxy verweist auf das vom NGINX-Projekt gepflegte Image für Betrieb ohne Root-Benutzer. environment setzt den harmlosen Anzeigetitel. Das Named Volume visits wird unter /data eingebunden und auf oberster Ebene deklariert. Compose versieht dessen tatsächlichen Namen normalerweise mit dem Projektnamen.
healthcheck.test führt die angegebene Argumentliste direkt im App-Container aus. Python ruft dessen eigenen HTTP-Endpunkt auf. Ein Fehler ergibt einen erfolglosen Test. interval ist der Prüfungsabstand, timeout die maximale Testdauer und retries die Zahl aufeinanderfolgender Fehlversuche für den Zustand „unhealthy“. start_period gewährt eine Startphase, in der Fehler noch nicht auf diese Schwelle angerechnet werden. Der HTTP-Aufruf hat zusätzlich ein eigenes Timeout.
depends_on mit condition: service_healthy lässt Compose vor dem Start des Proxys auf den erfolgreichen App-Healthcheck warten. Die Kurzform würde nur die Startreihenfolge festlegen. Auch diese Bedingung repariert keine später abgebrochene Verbindung und startet bei einer später ungesunden App nicht automatisch den gesamten Verbund neu. Die Anwendung muss Laufzeitfehler behandeln können.
restart: "no" deaktiviert automatische Neustarts im Labor. Andere Richtlinien sind beispielsweise on-failure und unless-stopped; ihre Wahl hängt vom gewünschten Betriebsverhalten ab. Eine Restart Policy reagiert auf Prozessende, nicht allein auf „unhealthy“. cap_drop: [ALL] entfernt zusätzliche Linux-Capabilities. no-new-privileges verhindert zusätzliche Privilegien durch einen Programmaustausch. Diese Einstellungen ergänzen den Betrieb ohne Root; sie ersetzen keine Prüfung des Images.
Beim Proxy veröffentlicht ports ausschließlich 127.0.0.1:8080. Sein Bind Mount liest default.conf vom Host. Beide Services hängen am automatisch erzeugten Projektnetz; eine zusätzliche networks-Sektion wäre dafür nicht nötig.
Dockerfile
FROM python:3.13-alpine
WORKDIR /app
COPY app.py ./app.py
RUN mkdir /data && chown 10001:10001 /data
USER 10001:10001
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
CMD ["python", "app.py"]
FROM wählt die Python-Basis. WORKDIR setzt das Arbeitsverzeichnis. COPY übernimmt nur den Anwendungscode. RUN erstellt beim Build das Datenverzeichnis mit Eigentümer 10001; beim ersten Einbinden eines leeren Named Volumes übernimmt Docker den vorbereiteten Verzeichnisinhalt einschließlich der relevanten Eigentumsrechte. USER setzt den späteren Prozess auf diese numerische UID und GID. ENV sorgt für unmittelbare Python-Logausgabe und verhindert Bytecode-Dateien. CMD legt den Standardstart als Argumentliste fest. Es wird keine zusätzliche Shell benötigt.
app.py
"""Single-process learning server; never use as a public production service."""
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
COUNTER = Path("/data/visits.txt")
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
payload = b"ok\n"
elif self.path == "/":
count = int(COUNTER.read_text()) if COUNTER.exists() else 0
count += 1
temporary = COUNTER.with_suffix(".tmp")
temporary.write_text(str(count))
temporary.replace(COUNTER)
payload = (json.dumps({"title": os.environ.get("APP_TITLE", "Labor"),
"visits": count}, ensure_ascii=False) + "\n").encode()
else:
self.send_error(404)
return
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8" if self.path == "/" else "text/plain")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
Der Server bindet im Container an alle Schnittstellen auf Port 8000, damit der Proxy ihn erreicht. Die Hostveröffentlichung bleibt auf dem Proxy. Python verwendet ausschließlich die Standardbibliothek. HTTPServer verarbeitet hier nacheinander; die temporäre Datei wird anschließend durch Umbenennen zum neuen Zählerstand. Das erklärt Persistenz, ist aber keine Datenbank und bietet keine zugesicherte Haltbarkeit bei Stromausfall. Die Fehlerpfade einer produktiven Anwendung wären umfangreicher.
default.conf und .dockerignore
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://app:8000;
}
}
listen bestimmt den internen Proxyport. server_name benennt diesen Serverblock; es ist keine Zugriffskontrolle. location / erfasst die Anfragen und proxy_pass leitet sie zum internen Servicenamen weiter.
*
!app.py
!Dockerfile
Der Stern schließt zunächst alles aus; die beiden Ausnahmen erlauben Anwendung und Dockerfile. So geraten zusätzliche Laborunterlagen nicht versehentlich in den Build-Kontext.
Konfiguration prüfen, starten und gezielt beobachten.
Die folgenden Befehle sind für dein eigenes lokales Labor gedacht. Der Strukturcheck erzeugt keine Container. Das Starten lädt Images, baut das App-Image und erzeugt Netzwerk, Volume und Container. Prüfe vorher den Kontext und führe alle Compose-Befehle im Laborverzeichnis aus.
docker context show
docker compose version
docker compose config --quiet
docker compose up --build -d
docker compose ps
docker compose logs --tail=50 app web
curl http://127.0.0.1:8080/
docker compose stop
docker compose down
config --quiet validiert die Compose-Struktur ohne die aufgelöste Konfiguration auszugeben. up erstellt und startet den Verbund, --build baut vor dem Start und -d lässt die Container im Hintergrund laufen. ps zeigt Status und Portzuordnung. logs --tail=50 begrenzt die Ausgabe pro Service auf die letzten 50 Zeilen; mit -f ließe sich die Ausgabe bis zum Abbruch mit Strg+C verfolgen. stop hält die Container an; down entfernt anschließend Container und Projektnetz. Das Named Volume bleibt erhalten.
Simulierte Beispielantwort nach dem ersten Aufruf mit neuem Volume:
{"title": "BitSicht Containerlabor", "visits": 1}
Beim nächsten Aufruf steigt die Zahl. Nach down und erneutem up -d sollte sie bei demselben Projekt und Volume weitersteigen. Ein anderer Projektname kann ein anderes Volume ergeben. Beim Schreiben dieses Artikels wurden keine Container gestartet; diese erwartete Laufzeitwirkung ist keine behauptete Ausführungsprüfung.
Bei Fehlern lies zuerst ps und die Logs. „Port already allocated“ spricht für einen belegten Hostport. „Permission denied“ unter /data verlangt eine Prüfung der Volume-Eigentumsrechte. Ein laufender Proxy mit HTTP 502 kann die App nicht erfolgreich erreichen; der Status „running“ allein beweist keine funktionierende Anfragekette. Compose-Konfiguration, Namen und tatsächlichen Endpunkt prüfst du getrennt.
Updates und Geheimnisse brauchen einen Betriebsplan.
Für den Proxy lädt docker compose pull web den aktuellen Inhalt seines Tags. docker compose build --pull app aktualisiert beim Neubau auch die referenzierte Basis. docker compose up -d --force-recreate erstellt hier beide Services bewusst neu. --force-recreate erneuert auch den unveränderten Proxy: NGINX löst den Namen app in dieser einfachen Konfiguration beim Start auf. Wird nur die App mit einer anderen Container-IP neu erstellt, könnte ein weiterlaufender Proxy noch die alte Adresse verwenden. Das Named Volume bleibt bei der Neuerstellung erhalten; laufende Verbindungen können abbrechen. Ein reines restart wechselt kein Image. In einer echten Anwendung folgen darauf Funktionsprüfung, Datenkompatibilitätsprüfung und ein vorbereiteter Rückweg. Alte Imageversionen helfen nur dann, wenn Datenmigrationen ebenfalls rückwärtskompatibel sind.
Umgebungsvariablen eignen sich für gewöhnliche Konfiguration. Sie sind kein sicherer Geheimnisspeicher: Metadaten, Diagnoseausgaben oder unbedachte Logs können sie offenlegen. Auch eine lokale .env-Datei ist nicht automatisch geschützt. Compose kann Secrets als Dateien gezielt an Services bereitstellen. Quelle, Dateirechte, Sicherung, Zugriff und Rotation bleiben zu organisieren; ein gewöhnliches lokales Compose-Secret ist kein automatischer verschlüsselter Tresor. Dieses Labor benötigt keine Secrets.
Zugriff auf den Docker-Dienst ist besonders mächtig. Der Docker-Socket und bei einem gewöhnlichen Root-Daemon die Mitgliedschaft in der Docker-Gruppe können weitreichende Host-Rechte ermöglichen. Montiere den Socket nicht als beiläufige Komfortfunktion. Privilegierte Container, Host-Networking und große beschreibbare Host-Mounts sind keine sicheren Standardlösungen. Verwende geprüfte Images, möglichst geringe Rechte und nur benötigte Ports. Rootless Docker ist eine weitere Betriebsoption mit eigenen Voraussetzungen und Grenzen.
Für lokale KI-Modelle kann Compose Modellserver und Oberfläche zusammen beschreiben. Modelllizenzen, Speicherbedarf, GPU-Zugriff und Schutz der Prompts müssen trotzdem geplant werden. Die Containerverpackung macht einen Dienst weder automatisch privat noch skalierbar. Der nächste Lernschritt Kubernetes erklärt, wie eine Plattform Anwendungen über mehrere Nodes verteilt und ihren Sollzustand überwacht.
Prüfe die Grenzen der Konfiguration.
Aufgabe 1: Der Proxy läuft, die App wird später „unhealthy“. Startet Compose beide neu?
Nein. service_healthy ist eine Bereitschaftsbedingung beim Start der abhängigen Instanz. Unser restart: "no" startet auch beendete Prozesse nicht automatisch neu. Selbst eine gewöhnliche Restart Policy reagiert nicht allein auf einen fehlgeschlagenen Healthcheck. Prüfe App-Logs und Gesundheitsursache; eine Wiederholungsschleife ohne Diagnose würde den Fehler nur verdecken.
Aufgabe 2: Nach „down“ und „up“ steht der Zähler wieder auf 1. Welche Annahme prüfst du?
Prüfe, ob dasselbe Projekt und dasselbe Named Volume verwendet werden. Ein anderer Projektname oder ein gelöschtes Volume erzeugt einen neuen Datenbestand. Prüfe außerdem den Mount auf /data und die Dateirechte. Das Löschen des Containers allein erklärt den Verlust hier nicht, weil der Zähler ausdrücklich im Volume liegt. Ein Backup bleibt nötig, auch wenn der Persistenztest gelingt.
Quellen und fachlicher Stand.
Fachlich aktualisiert am . Grundlage sind die aktuelle Compose Specification und stabile Docker-Engine-Konzepte; konkrete lokale Softwarestände müssen vor einem eigenen Lauf geprüft werden. Der Beispielcode ist eine eigene didaktische Umsetzung.