Docker für Entwickler: Images, Container und Compose

DevOps2026-10-12TryQuickToolBox

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.

KonzeptWas es istTypischer Befehl
ImageUnveränderliche Vorlagedocker build, docker pull
ContainerLaufende Instanzdocker run, docker ps
VolumePersistenter Speicherdocker volume create
ComposeMulti-Container-Definitiondocker 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.0

Das 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:

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:

  1. Benannte Volumes — von Docker verwaltet, ideal für Datenbankdateien. Sie bleiben über Container-Neustarts hinweg erhalten und werden im Abschnitt volumes: deklariert.
  2. Bind Mounts — mappen ein Host-Verzeichnis in den Container, ideal für Quellcode während der Entwicklung.
services:
  api:
    build: .
    volumes:
      - ./src:/app/src

Mit 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

  1. Schreibe ein Dockerfile für deine App und halte es minimal.
  2. Füge eine docker-compose.yml mit der App plus ihren Abhängigkeiten hinzu.
  3. Verwende Bind Mounts für Quellcode und benannte Volumes für Daten.
  4. Halte Secrets aus der Datei heraus; verwende eine .env-Datei und referenziere Variablen mit ${VAR}.
  5. Füge eine .dockerignore hinzu, die node_modules, .git und Build-Output enthält, um Builds zu beschleunigen.
  6. Führe docker compose up --build aus 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

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.