Zum Inhalt springen
Alle Artikel
Docker ComposeDockerContainerDevOpsWebentwicklung

Docker Compose Praxis-Guide für Webentwickler

Docker Compose vereinfacht Entwicklung und Deployment. Lerne Netzwerke, Volumes und Best Practices mit praxisnahen Beispielen für Webprojekte.

7. März 20268 Min. Lesezeit

Wer heute Webanwendungen entwickelt, kommt an Docker kaum noch vorbei. Doch während ein einzelner Container schnell gestartet ist, wird es bei mehreren Services – Webserver, Datenbank, Cache, Queue – schnell unübersichtlich. Genau hier setzt Docker Compose an: Mit einer einzigen YAML-Datei definierst du deine gesamte Infrastruktur, startest sie mit einem Befehl und hast eine reproduzierbare Umgebung, die auf jedem Rechner identisch läuft.

Was ist Docker Compose?

Docker Compose ist ein Tool, das es erlaubt, Multi-Container-Anwendungen deklarativ in einer docker-compose.yml-Datei zu beschreiben. Statt für jeden Service einzelne docker run-Befehle mit dutzenden Flags zu tippen, definierst du alles zentral: Images, Ports, Volumes, Netzwerke und Umgebungsvariablen.

Seit Docker Desktop und neueren Docker-Engine-Versionen ist Compose als docker compose (ohne Bindestrich) direkt integriert. Die ältere Python-basierte Version docker-compose funktioniert weiterhin, wird aber nicht mehr aktiv weiterentwickelt.

# Version prüfen
docker compose version

Eine typische docker-compose.yml aufbauen

Nehmen wir ein realistisches Szenario: Eine Next.js-Anwendung mit einer PostgreSQL-Datenbank und Redis als Cache-Layer. So könnte die Compose-Datei aussehen:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://appuser:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Diese Datei beschreibt drei Services, die mit einem einzigen Befehl gestartet werden:

docker compose up -d

Das -d-Flag startet alles im Hintergrund. Mit docker compose logs -f kannst du die Logs aller Services live verfolgen.

Netzwerke verstehen

Ein oft unterschätztes Feature von Docker Compose ist das automatische Netzwerk-Management. Compose erstellt für jedes Projekt ein eigenes Bridge-Netzwerk. Alle Services innerhalb dieses Netzwerks können sich gegenseitig über ihren Service-Namen als Hostnamen erreichen.

Im Beispiel oben verbindet sich die App mit db:5432 – nicht mit localhost oder einer IP-Adresse. Docker löst den Namen db intern auf den entsprechenden Container auf. Das funktioniert automatisch und ohne zusätzliche Konfiguration.

Eigene Netzwerke definieren

Für komplexere Setups kannst du separate Netzwerke erstellen, um Services voneinander zu isolieren:

services:
  app:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend

  nginx:
    networks:
      - frontend

networks:
  frontend:
  backend:

In diesem Setup kann nginx die App erreichen, aber nicht direkt die Datenbank. Die App wiederum hat Zugriff auf beide Netzwerke. Das entspricht dem Principle of Least Privilege und erhöht die Sicherheit.

Volumes: Daten persistent speichern

Container sind von Natur aus ephemeral – werden sie gelöscht, sind die Daten weg. Für Datenbanken, Uploads oder Konfigurationsdateien brauchst du deshalb Volumes.

Docker Compose unterscheidet zwei Arten:

Named Volumes

Named Volumes werden von Docker verwaltet und überleben Container-Neustarts und sogar das Löschen von Containern:

volumes:
  pgdata:
    driver: local

Der Speicherort auf dem Host wird von Docker automatisch festgelegt (meist unter /var/lib/docker/volumes/).

Bind Mounts

Bind Mounts verknüpfen einen konkreten Host-Pfad mit dem Container. Das ist besonders in der Entwicklung nützlich, um Live-Reloading zu ermöglichen:

services:
  app:
    volumes:
      - ./src:/app/src
      - /app/node_modules

Der zweite Eintrag ohne Host-Pfad ist ein Anonymous Volume – er verhindert, dass das lokale (möglicherweise leere) node_modules-Verzeichnis das im Container installierte überschreibt. Ein klassischer Trick, der viel Debugging erspart.

Umgebungsvariablen richtig verwalten

Hardcodierte Passwörter in der docker-compose.yml sind ein Sicherheitsrisiko, besonders wenn die Datei im Git-Repository liegt. Besser: Eine .env-Datei verwenden.

# .env
POSTGRES_USER=appuser
POSTGRES_PASSWORD=ein_sicheres_passwort_hier
POSTGRES_DB=myapp
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

Docker Compose liest die .env-Datei im selben Verzeichnis automatisch ein. Vergiss nicht, sie in die .gitignore aufzunehmen:

echo ".env" >> .gitignore

Healthchecks und depends_on

Ein häufiges Problem: Die App startet, bevor die Datenbank bereit ist, und stürzt ab. Das einfache depends_on garantiert nur die Startreihenfolge, nicht die tatsächliche Verfügbarkeit.

Mit Healthchecks löst du das Problem elegant:

services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  app:
    depends_on:
      db:
        condition: service_healthy

Jetzt wartet der App-Container tatsächlich, bis PostgreSQL Verbindungen akzeptiert. Der start_period gibt der Datenbank Zeit für die initiale Initialisierung, ohne dass fehlgeschlagene Checks als Fehler gezählt werden.

Best Practices für Produktion

1. Immer konkrete Image-Tags verwenden

# Schlecht – kann sich jederzeit ändern
image: postgres:latest

# Gut – reproduzierbar
image: postgres:16.3-alpine

2. Ressourcen-Limits setzen

Verhindere, dass ein einzelner Container den gesamten Server lahmlegt:

services:
  app:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

3. Restart-Policy definieren

Für Produktionsumgebungen sollte jeder Service eine Restart-Policy haben:

restart: unless-stopped

Damit startet Docker den Container automatisch nach einem Crash oder Server-Neustart – es sei denn, du hast ihn bewusst gestoppt.

4. Multi-Stage Builds nutzen

Dein Dockerfile sollte Multi-Stage Builds verwenden, um das finale Image klein zu halten:

# Build-Stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production-Stage
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

USER node
EXPOSE 3000
CMD ["npm", "start"]

Die USER node-Direktive ist wichtig: Container sollten niemals als root laufen.

5. .dockerignore nicht vergessen

Erstelle eine .dockerignore-Datei, um unnötige Dateien aus dem Build-Context auszuschließen:

node_modules
.git
.env
.next
Dockerfile
docker-compose.yml
README.md

Das beschleunigt den Build erheblich und verhindert, dass sensible Dateien im Image landen.

Nützliche Befehle im Alltag

Hier eine Übersicht der wichtigsten Docker-Compose-Befehle, die du regelmäßig brauchen wirst:

# Alle Services starten (im Hintergrund)
docker compose up -d

# Logs eines bestimmten Services verfolgen
docker compose logs -f app

# In einen laufenden Container springen
docker compose exec db psql -U appuser -d myapp

# Alle Services stoppen und Container entfernen
docker compose down

# Alles entfernen, inklusive Volumes (Achtung: Datenverlust!)
docker compose down -v

# Nur einen Service neu bauen und starten
docker compose up -d --build app

# Status aller Services anzeigen
docker compose ps

# Ressourcenverbrauch live überwachen
docker compose stats

Entwicklung vs. Produktion trennen

Für unterschiedliche Umgebungen kannst du Override-Dateien nutzen. Docker Compose merged automatisch docker-compose.yml mit docker-compose.override.yml:

# docker-compose.override.yml (Entwicklung)
services:
  app:
    build:
      context: .
    volumes:
      - ./src:/app/src
    environment:
      - NODE_ENV=development

Für Produktion verwendest du eine separate Datei:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

So bleibt die Basis-Konfiguration identisch, während umgebungsspezifische Anpassungen sauber getrennt sind.

Fazit

Docker Compose ist ein unverzichtbares Werkzeug für moderne Webentwicklung. Es eliminiert das berüchtigte "Works on my machine"-Problem, macht Onboarding neuer Teammitglieder trivial und bildet die Brücke zwischen lokaler Entwicklung und Produktions-Deployment. Mit den hier gezeigten Best Practices – Healthchecks, Ressourcen-Limits, Netzwerk-Isolation und sauberer Trennung von Umgebungen – bist du bestens aufgestellt, um deine Webprojekte professionell zu containerisieren.