Docker für Entwickler: Images, Container und Compose
Du hast ein Projekt geklont, die Setup-Anleitung ausgeführt und bist an eine Wand gestoßen: eine fehlende Systembibliothek, eine Datenbank, die nicht startet, oder eine Node-Versions-Inkompatibilität. Docker existiert, um diese Art von Problemen zu beseitigen. Statt Dienste direkt auf deiner Maschine zu installieren, beschreibst du sie einmal und führst sie in isolierten Containern aus, die sich überall gleich verhalten.
Dieser Leitfaden erklärt die drei Ideen, die du wirklich brauchst: Images, Container und Compose. Er bleibt praktisch, mit Befehlen, die du beim Lesen ausführen kannst.
Images vs. Container: das mentale Modell
Ein Image ist eine schreibgeschützte Vorlage. Es enthält einen Filesystem-Snapshot plus Metadaten wie den Standardbefehl und exponierte Ports. Ein Container ist eine laufende (oder gestoppte) Instanz dieses Images mit einer dünnen beschreibbaren Schicht darüber.
Wenn du aus der objektorientierten Programmierung kommst, ist die Analogie treffend: Ein Image ist eine Klasse, ein Container ist ein Objekt. Du kannst viele Container aus einem Image starten, und Änderungen innerhalb eines Containers verändern das Image nicht.
| Konzept | Was es ist | Typischer Befehl |
|---|---|---|
| Image | Unveränderliche Vorlage | docker build, docker pull |
| Container | Laufende Instanz | docker run, docker ps |
| Volume | Persistenter Speicher | docker volume create |
| Compose | Multi-Container-Definition | docker compose up |
Dein erstes Image mit einem Dockerfile
Ein Dockerfile ist ein Rezept. Jede Anweisung erzeugt eine Schicht, und Docker cached Schichten, sodass Rebuilds schnell sind, wenn sich nichts geändert hat. Hier ist ein kleines Node.js-Beispiel:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Zwei Gewohnheiten sind hier wichtig. Erstens: Kopiere Abhängigkeits-Manifeste vor dem Rest des Quellcodes, damit die npm ci-Schicht gecached wird, wenn sich nur der Anwendungscode ändert. Zweitens: Bevorzuge ein schlankes Basis-Image wie alpine, um Größe und Angriffsfläche zu reduzieren.
Baue und starte es:
docker build -t myapp:1.0 .
docker run --rm -p 3000:3000 myapp:1.0Das Flag -p 3000:3000 mappt Host-Port 3000 auf Container-Port 3000. --rm löscht den Container, wenn er beendet wird, was deine Maschine während der Entwicklung sauber hält.
Alltägliche Container-Befehle
Diese decken die meiste tägliche Arbeit ab:
docker ps— laufende Container auflisten; füge-ahinzu, um gestoppte einzuschließen.docker logs -f <container>— Anwendungsausgabe streamen.docker exec -it <container> sh— eine Shell innerhalb eines laufenden Containers öffnen.docker stop <container>— ordnungsgemäßes Herunterfahren.docker images— lokale Images und ihre Größen auflisten.docker system prune— ungenutzte Container, Netzwerke und dangling Images entfernen.
Wenn ein Container sofort beendet wird, lies zuerst die Logs. Die häufigsten Ursachen sind ein falsches CMD, eine fehlende Umgebungsvariable oder der Prozess, der sich im Container an 127.0.0.1 statt an 0.0.0.0 bindet.
Warum Compose existiert
Eine echte Anwendung braucht normalerweise mehr als einen Prozess: eine API, eine Datenbank, vielleicht einen Cache. Drei docker run-Befehle mit langen Flag-Listen auszuführen ist fehleranfällig. Docker Compose lässt dich alle Services in einer YAML-Datei deklarieren und sie zusammen starten.
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:Beachte, dass die API sich mit db verbindet, nicht mit localhost. Compose erstellt ein privates Netzwerk, in dem jeder Service über seinen Service-Namen erreichbar ist. Dies ist die häufigste Verwirrungsquelle für Neueinsteiger.
Starte alles mit docker compose up oder docker compose up -d, um im Hintergrund auszuführen. Baue nach Code-Änderungen mit docker compose up --build neu. Fahre mit docker compose down herunter und füge -v hinzu, wenn du auch die benannten Volumes löschen möchtest.
Volumes, Bind Mounts und Live Reload
Container sind ephemer; Daten darin verschwinden, wenn sie entfernt werden. Zwei Mechanismen lösen das:
- Benannte Volumes — von Docker verwaltet, ideal für Datenbankdateien. Sie bleiben über Container-Neustarts hinweg erhalten und werden im Abschnitt
volumes:deklariert. - Bind Mounts — mappen ein Host-Verzeichnis in den Container, ideal für Quellcode während der Entwicklung.
services:
api:
build: .
volumes:
- ./src:/app/srcMit einem Bind Mount werden Bearbeitungen von Dateien auf deinem Host im Container widergespiegelt. Kombiniere dies mit einem File Watcher (nodemon, uvicorn --reload, air) und du erhältst Live Reload ohne das Image neu zu bauen.
Ein praktischer Entwicklungs-Workflow
- Schreibe ein Dockerfile für deine App und halte es minimal.
- Füge eine
docker-compose.ymlmit der App plus ihren Abhängigkeiten hinzu. - Verwende Bind Mounts für Quellcode und benannte Volumes für Daten.
- Halte Secrets aus der Datei heraus; verwende eine
.env-Datei und referenziere Variablen mit${VAR}. - Füge eine
.dockerignorehinzu, dienode_modules,.gitund Build-Output enthält, um Builds zu beschleunigen. - Führe
docker compose up --buildaus und iteriere.
Wenn dein Service Logs produziert, die du später inspizieren musst, denke daran, dass Container-Logs nur Text sind. Tools, die Server-Logs parsen, wie ein nginx Log Analyzer, passen gut zu Containern, weil du Logs herauspipe-en oder kopieren kannst, ohne etwas auf dem Host zu installieren.
Häufige Fallstricke
- Fette Images: Verwende Multi-Stage-Builds, um in einer Stage zu kompilieren und nur Artefakte in ein schlankes Runtime-Image zu kopieren.
- Alles neu bauen: Ordne Dockerfile-Anweisungen von am seltensten zu am häufigsten geänderten.
- Hardcodierte Hosts: Verwende innerhalb von Compose Service-Namen; außerhalb Umgebungsvariablen.
- Cleanup ignorieren: Dangling Images und gestoppte Container sammeln sich an. Führe regelmäßig
docker system pruneaus. - Als root ausführen: Füge in Produktions-Images einen Nicht-Root-
USERhinzu.
FAQ
Was ist der Unterschied zwischen einem Image und einem Container?
Ein Image ist eine unveränderliche Vorlage, die ein Filesystem und Metadaten enthält. Ein Container ist eine laufende Instanz dieses Images mit einer zusätzlichen beschreibbaren Schicht. Du kannst viele Container aus einem einzigen Image starten.
Brauche ich Docker Compose für einen einzelnen Service?
Nein. Ein einzelner docker run-Befehl reicht für einen Container. Compose wird wertvoll, wenn du mehrere Services, geteilte Netzwerke oder Umgebungsvariablen hast, die du an einem Ort deklarieren möchtest.
Wie kommunizieren Container miteinander?
Compose platziert alle Services in einem gemeinsamen Netzwerk, in dem jeder Service über seinen Service-Namen erreichbar ist. Verwende diesen Namen als Hostname, zum Beispiel db:5432, statt localhost.
Docker belohnt eine kleine Anfangsinvestition. Sobald Images, Container und Compose klick machen, wird das Onboarding eines Teamkollegen oder der Wechsel der Maschine von einem Setup-Ritual zu einem einzigen Befehl.