Docker Compose · Integrationstests · E2E · CI/CD
Docker Compose für Integrations- und End-to-End-Tests
isoliert, reproduzierbar und CI-tauglich

Integrations- und End-to-End-Tests scheitern nicht an der Testlogik, sondern an der Umgebung: falsche Datenbankversion, nicht gestartete Services, Race Conditions beim Hochfahren. Docker Compose löst genau das – mit deklarierten Abhängigkeiten, Healthchecks und Netzwerkisolation, die jede Testausführung reproduzierbar macht.

15 Min. Lesezeit Healthchecks · depends_on · Netzwerke · Teardown Docker Compose v2 · GitHub Actions · GitLab CI

1. Warum Docker Compose für Tests der richtige Ansatz ist

Integrationstests und End-to-End-Tests stellen andere Anforderungen an die Infrastruktur als Unit-Tests. Während Unit-Tests vollständig in-process laufen und keine externe Infrastruktur benötigen, setzen Docker Compose-gestützte Integrationstests voraus, dass Datenbanken, Message-Broker, Cache-Server und externe Dienste tatsächlich erreichbar und im korrekten Zustand sind. Der häufigste Fehler: man verlässt sich darauf, dass auf dem Entwicklerrechner die richtige MySQL-Version läuft, die in der CI-Umgebung gar nicht vorhanden ist.

Docker Compose löst dieses Problem durch deklarative Umgebungsdefinitionen. Die gesamte Testumgebung wird in einer einzigen Datei beschrieben: Welche Services werden gestartet? In welcher Version? Mit welchen Umgebungsvariablen? Welche Services müssen vor dem eigentlichen Test verfügbar sein? Das Ergebnis ist eine Testumgebung, die auf jedem Entwicklerrechner und in jeder CI-Pipeline identisch hochfährt – unabhängig davon, was sonst auf dem System installiert ist. Besonders für Teams, die parallel an mehreren Projekten arbeiten, ist die Netzwerk- und Volumen-Isolation von Docker Compose entscheidend, um Konflikte zwischen verschiedenen Testläufen zu vermeiden.

Der zweite entscheidende Vorteil ist die Reproduzierbarkeit im Fehlerfall. Schlägt ein Integrationstest fehl, kann der Entwickler die exakt gleiche Umgebung lokal hochfahren, in der der Test in der CI-Pipeline gescheitert ist – mit denselben Daten, denselben Versionen, denselben Netzwerkbedingungen. Dieses Merkmal reduziert den Debugging-Aufwand bei intermittierenden Fehlern erheblich.

2. Grundstruktur einer Test-Compose-Datei

Eine Docker Compose-Datei für Integrationstests unterscheidet sich strukturell von einer Entwicklungsumgebung: Sie enthält keine Volumes für Live-Code-Reload, keine dauerhaften Datenvolumes und keine Port-Weiterleitungen auf den Host, die mit anderen Testläufen kollidieren könnten. Stattdessen definiert sie ein geschlossenes Netzwerk, in dem alle Services ausschließlich über interne DNS-Namen kommunizieren.

Die Benennung des Projekts über die Umgebungsvariable COMPOSE_PROJECT_NAME oder den --project-name-Parameter ist in CI-Umgebungen unverzichtbar. Sie verhindert, dass parallele Builds auf demselben CI-Worker dieselben Netzwerk- und Container-Namen verwenden und sich gegenseitig stören. Für jeden Test-Run in der Pipeline sollte ein eindeutiger Projektname verwendet werden – zum Beispiel durch Anhängen der Build-ID oder eines kurzen Hash-Werts.


# docker-compose.test.yml — Integration test environment
# All services communicate only via internal Docker network

services:
  # Application under test
  app:
    build:
      context: .
      target: test          # Use dedicated test build stage
    environment:
      APP_ENV: testing
      DB_HOST: db
      DB_PORT: 3306
      DB_NAME: testdb
      DB_USER: testuser
      DB_PASSWORD: testpass
      REDIS_HOST: redis
      REDIS_PORT: 6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - test-network

  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: testdb
      MYSQL_USER: testuser
      MYSQL_PASSWORD: testpass
      MYSQL_ROOT_PASSWORD: rootpass
    volumes:
      - ./tests/fixtures/init.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
      - ./tests/fixtures/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql:ro
    # No host port mapping — prevents conflicts with local MySQL
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "testuser", "-ptestpass"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 20s
    networks:
      - test-network

  redis:
    image: redis:7.4-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 3s
      retries: 10
    networks:
      - test-network

networks:
  test-network:
    driver: bridge
    # Unique name prevents collision between parallel CI runs
    name: "test-${COMPOSE_PROJECT_NAME:-default}"

3. Healthchecks und depends_on: Services wirklich bereit warten

Das häufigste Problem bei Docker Compose-Testumgebungen ist die Annahme, dass ein Container „läuft" bedeutet, dass der darin enthaltene Service auch bereit ist. Ein MySQL-Container startet innerhalb von Sekunden – aber der MySQL-Server braucht deutlich länger, um Initialisierungsskripte auszuführen, den InnoDB-Puffer aufzuwärmen und tatsächlich Verbindungen anzunehmen. Wer depends_on ohne condition: service_healthy verwendet, riskiert Race Conditions, bei denen die Anwendung versucht, auf die Datenbank zuzugreifen, bevor diese bereit ist.

Das korrekte Muster in Docker Compose v2 ist die Kombination aus einem präzisen healthcheck im Service und condition: service_healthy in der depends_on-Konfiguration des abhängigen Services. Der start_period-Parameter ist dabei besonders wichtig: Er definiert eine Zeitspanne nach dem Start, in der Healthcheck-Fehler nicht als Misserfolge gewertet werden. Das erlaubt es langsam startenden Services wie MySQL oder Elasticsearch, ihre Initialisierung abzuschließen, ohne dass der Container fälschlicherweise als ungesund markiert wird. In der Praxis hat sich ein start_period von 20–30 Sekunden für Datenbankcontainer bewährt.

4. Netzwerkisolation: Testumgebungen sauber trennen

In einer Docker Compose-Testumgebung ist Netzwerkisolation keine optionale Optimierung, sondern eine Grundvoraussetzung für stabile Tests. Ohne explizite Netzwerkkonfiguration verbindet Docker Compose alle Services eines Projekts in einem Standard-Bridge-Netzwerk – was für einzelne Testläufe ausreichend ist, aber bei parallelen Builds auf demselben Host zu unerwarteten Interaktionen führen kann.

Für robuste CI-Pipelines empfiehlt sich die explizite Benennung von Netzwerken mit einem eindeutigen Präfix pro Build. Die Variable ${CI_JOB_ID} in GitLab CI oder ${GITHUB_RUN_ID} in GitHub Actions sind dafür geeignete Kandidaten. Zusätzlich sollten in der Testumgebung keine Ports auf dem Host-System exponiert werden – interne Service-Kommunikation läuft ausschließlich über den Container-DNS-Namen. Das vermeidet Port-Konflikte zwischen parallelen Testläufen und verhindert, dass externe Prozesse auf dem CI-Worker unbeabsichtigt auf die Testdienste zugreifen.


# Running integration tests with unique project name per CI build
# This prevents network and container name collisions on shared CI workers

# GitHub Actions example
PROJECT_NAME="test-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
docker compose \
  --file docker-compose.test.yml \
  --project-name "$PROJECT_NAME" \
  up --detach --wait

# Run the actual integration test suite
docker compose \
  --file docker-compose.test.yml \
  --project-name "$PROJECT_NAME" \
  exec app php vendor/bin/phpunit --testsuite integration --log-junit /tmp/results.xml

# Copy test results from container to host for CI reporting
docker compose \
  --file docker-compose.test.yml \
  --project-name "$PROJECT_NAME" \
  cp app:/tmp/results.xml ./test-results/integration.xml

# Always clean up — even if tests fail
docker compose \
  --file docker-compose.test.yml \
  --project-name "$PROJECT_NAME" \
  down --volumes --remove-orphans

5. Datenbankinitialisierung und Fixtures reproduzierbar einrichten

Reproduzierbare Integrationstests erfordern eine definierte Datenbankausgangslage. Das MySQL- und PostgreSQL-Image unterstützen ein Init-Verzeichnis (/docker-entrypoint-initdb.d/), in dem SQL-Skripte beim ersten Start automatisch ausgeführt werden. Für Docker Compose-Testumgebungen bedeutet das: Schema und Testdaten werden als read-only Volumes eingebunden und beim ersten Container-Start automatisch eingespielt.

Da Docker Compose-Testumgebungen typischerweise anonyme Volumes verwenden und beim Teardown mit docker compose down --volumes vollständig bereinigt werden, beginnt jeder Testlauf mit einem leeren Datenbankzustand. Das ist für Integrationstests der entscheidende Vorteil gegenüber geteilten Testdatenbanken: Kein Testlauf hinterlässt Daten, die den nächsten beeinflussen. Für Tests, die unterschiedliche Datenbankzustände benötigen, empfiehlt sich das Muster mit separaten Compose-Overrides, die verschiedene Fixture-Sets einbinden.

6. End-to-End-Tests mit Playwright und Cypress im Container

End-to-End-Tests mit Browser-Automatisierung sind der anspruchsvollste Anwendungsfall für Docker Compose-Testumgebungen. Playwright und Cypress bieten offizielle Docker-Images, die einen vollständigen Browser-Stack ohne grafische Ausgabe enthalten. In einer Docker Compose-Konfiguration läuft der E2E-Test-Runner als eigener Service, der die zu testende Anwendung über den internen DNS-Namen erreicht – ohne Netzwerkzugriff auf den Host.

Das kritische Detail bei Browser-Tests in Containern: Die zu testende Anwendung muss vollständig bereit sein, bevor der Browser den ersten Request sendet. Hier sind mehrstufige Healthchecks notwendig – der Datenbank-Healthcheck stellt sicher, dass MySQL bereit ist, ein separater HTTP-Healthcheck an der Anwendung stellt sicher, dass die App selbst initialisiert ist und Requests beantwortet. Erst wenn beide Bedingungen erfüllt sind, startet der E2E-Container. Das Selenium Grid als separater Docker Compose-Service mit einem oder mehreren Browser-Nodes ermöglicht parallele E2E-Tests über mehrere Browser-Instanzen.


# E2E test service using Playwright official container
# Waits for app to be fully ready before running tests

services:
  e2e:
    image: mcr.microsoft.com/playwright:v1.44.0-jammy
    working_dir: /tests
    volumes:
      - ./e2e:/tests:ro                         # Mount test files read-only
      - ./test-results/e2e:/tests/results       # Write results to host
    environment:
      BASE_URL: http://app:8080                  # Internal Docker DNS name
      CI: "true"
    command: >
      npx playwright test
        --reporter=html
        --output=/tests/results
    depends_on:
      app:
        condition: service_healthy              # App must pass HTTP healthcheck
    networks:
      - test-network

  app:
    build: .
    environment:
      APP_ENV: testing
      DB_HOST: db
    healthcheck:
      # HTTP healthcheck — app is ready when it responds 200
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 5s
      timeout: 5s
      retries: 12
      start_period: 30s
    depends_on:
      db:
        condition: service_healthy
    networks:
      - test-network

7. Docker Compose in GitHub Actions und GitLab CI einbinden

Die Integration von Docker Compose in CI-Pipelines erfordert besondere Sorgfalt beim Umgang mit parallelen Builds und der Bereinigung von Ressourcen. In GitHub Actions läuft jeder Job in einer eigenen VM-Instanz, was Netzwerk- und Port-Konflikte zwischen Jobs auf unterschiedlichen Runnern ausschließt. Auf Self-Hosted Runnern oder in GitLab CI mit geteilten Workern hingegen müssen eindeutige Projektnamen und Netzwerknamen sichergestellt werden.

Das zuverlässigste Muster für CI: Den Teardown-Befehl in einem separaten Step mit if: always() (GitHub Actions) oder als separater Job mit when: always (GitLab CI) ausführen. Das stellt sicher, dass Container und Volumes auch dann bereinigt werden, wenn der Test-Step fehlschlägt oder abgebrochen wird. Ohne dieses Muster akkumulieren fehlgeschlagene CI-Builds Container-Überreste auf dem Worker, die Speicher und Netzwerk-Ressourcen belegen und zu schwer debugbaren Folgeproblemen führen.

8. Sauberes Teardown: Volumes, Logs und Artefakte sichern

Ein professioneller Docker Compose-Testworkflow kopiert Artefakte aus den Containern, bevor er diese stoppt und entfernt. Test-Reports, Coverage-Daten und – im Fehlerfall besonders wertvoll – Anwendungslogs aus dem Container sollten vor dem docker compose down auf den Host oder in CI-Artefaktspeicher gesichert werden. In GitHub Actions übernimmt der upload-artifact-Action-Step diese Aufgabe, in GitLab CI die artifacts-Konfiguration im Job.

Der Befehl docker compose logs --no-color --timestamps gibt die Logs aller Services in strukturiertem Format aus und kann direkt in eine Datei umgeleitet werden. Dieser Schritt sollte immer vor dem Teardown stehen – und bei fehlgeschlagenen Tests automatisch ausgeführt werden. Die Docker Compose-Option --volumes beim down-Befehl entfernt alle anonymen Volumes und stellt sicher, dass kein Datenbankzustand zwischen Testläufen persistiert wird. Named Volumes müssen explizit in der Volumes-Sektion definiert und separat mit docker volume rm entfernt werden, wenn sie nicht mit down --volumes gelöscht werden sollen.


#!/usr/bin/env bash
# ci-test.sh — Complete integration test lifecycle with proper teardown
set -euo pipefail

PROJECT_NAME="test-${CI_JOB_ID:-local}"
COMPOSE_FILE="docker-compose.test.yml"

cleanup() {
  echo "--- Collecting logs before teardown ---"
  # Save all service logs as CI artifact
  docker compose \
    --file "$COMPOSE_FILE" \
    --project-name "$PROJECT_NAME" \
    logs --no-color --timestamps > test-results/docker-compose.log 2>&1 || true

  echo "--- Tearing down test environment ---"
  docker compose \
    --file "$COMPOSE_FILE" \
    --project-name "$PROJECT_NAME" \
    down --volumes --remove-orphans --timeout 30
}

# Always run cleanup, even if tests fail
trap cleanup EXIT

echo "--- Starting test environment ---"
docker compose \
  --file "$COMPOSE_FILE" \
  --project-name "$PROJECT_NAME" \
  up --detach --wait --wait-timeout 120

echo "--- Running integration tests ---"
docker compose \
  --file "$COMPOSE_FILE" \
  --project-name "$PROJECT_NAME" \
  exec -T app php vendor/bin/phpunit \
    --testsuite integration \
    --log-junit /tmp/junit.xml \
    --coverage-clover /tmp/coverage.xml

# Copy test artifacts before cleanup runs
docker compose \
  --file "$COMPOSE_FILE" \
  --project-name "$PROJECT_NAME" \
  cp app:/tmp/junit.xml test-results/integration-junit.xml

echo "--- Tests completed successfully ---"

9. Teststrategie-Vergleich: mit und ohne Docker Compose

Die Entscheidung für oder gegen Docker Compose-gestützte Integrationstests hat konkrete Auswirkungen auf Testgeschwindigkeit, Wartungsaufwand und Reproduzierbarkeit. Ein fairer Vergleich berücksichtigt nicht nur den initialen Setup-Aufwand, sondern auch die langfristigen Kosten durch flaky Tests und umgebungsbedingte Fehler.

Kriterium Ohne Docker Compose Mit Docker Compose Vorteil
Reproduzierbarkeit Abhängig von lokaler Installation Identisch auf jedem System Docker Compose
Parallelausführung Port-Konflikte möglich Netzwerkisolation pro Build Docker Compose
Startzeit Sofort (Dienst läuft) 30–90 Sekunden Warmup Ohne Docker Compose
Datenzustand Manuelles Reset nötig Frisches Volume pro Run Docker Compose
Debugging Direkt auf dem System docker exec / Logs verfügbar Vergleichbar

Der entscheidende Punkt im Vergleich: Der Mehraufwand durch die Startzeit von Docker Compose-Testumgebungen amortisiert sich bereits bei wenigen flaky Test-Bugs, deren Ursache auf Umgebungsunterschiede zurückzuführen ist. Ein Fehler, der nur in der CI-Umgebung auftritt und lokal nicht reproduzierbar ist, kostet typischerweise mehr Debugging-Zeit als alle Startverzögerungen zusammen.

Mironsoft

Docker Compose, CI/CD-Infrastruktur und Testautomatisierung

Stabile Integrationstests, die in jeder Umgebung laufen?

Wir konzipieren Docker Compose-Testumgebungen mit vollständigen Healthchecks, Netzwerkisolation und automatisiertem Teardown – damit eure Integrations- und E2E-Tests in CI-Pipelines zuverlässig und reproduzierbar laufen.

Testumgebungs-Setup

Docker Compose für Integrationstests mit Healthchecks und Fixture-Initialisierung

CI-Integration

GitHub Actions und GitLab CI mit parallelen Builds und sauberem Teardown

E2E-Automatisierung

Playwright und Cypress in Docker-Containern für browserübergreifende Tests

10. Zusammenfassung

Docker Compose ist das richtige Werkzeug für Integrations- und End-to-End-Tests, weil es die Lücke zwischen "läuft auf meinem Rechner" und "läuft in der CI-Pipeline" schließt. Die Kombination aus deklarierten Abhängigkeiten, präzisen Healthchecks und Netzwerkisolation macht Testumgebungen reproducierbar und parallelfähig. Healthchecks mit condition: service_healthy eliminieren Race Conditions beim Start. Anonyme Volumes und automatisches Teardown stellen sicher, dass kein Testlauf Zustand für den nächsten hinterlässt.

Die wichtigsten Prinzipien in der Zusammenfassung: Eindeutige Projektnamen pro CI-Build verhindern Konflikte auf geteilten CI-Workern. Logs und Artefakte immer vor dem Teardown sichern. End-to-End-Tests in offiziellen Browser-Containern laufen lassen und den App-Healthcheck für den Browser-Container als Gate nutzen. Der initiale Mehraufwand für den Setup zahlt sich durch stabile, reproduzierbare Tests aus, die das Team in der Entwicklung wirklich voranbringen statt aufzuhalten.

Docker Compose für Tests — Das Wichtigste auf einen Blick

Healthchecks

condition: service_healthy in depends_on und start_period für langsam startende Services – verhindert Race Conditions beim Hochfahren.

Netzwerkisolation

Eindeutiger Projektname pro CI-Build, keine Host-Port-Mappings, interne DNS-Kommunikation – verhindert Konflikte bei parallelen Builds.

Sauberes Teardown

down --volumes --remove-orphans immer in trap/always-Block – Logs vor dem Teardown sichern, nie danach.

E2E-Tests

Playwright/Cypress in offiziellen Docker-Images, App-HTTP-Healthcheck als depends_on-Bedingung, Ergebnisse als Host-Volume einbinden.

11. FAQ: Docker Compose für Integrations- und End-to-End-Tests

1Warum schlägt mein Test fehl, obwohl der Container läuft?
Laufender Container ≠ Service bereit. MySQL braucht Zeit für Initialisierung. condition: service_healthy mit Healthcheck in depends_on löst das.
2Wie verhindere ich Port-Konflikte in parallelen CI-Builds?
Keine Host-Ports exposen. Eindeutiger --project-name pro Build mit Build-ID. Interne DNS-Kommunikation über Container-Namen.
3Wie starte ich nur bestimmte Services?
docker compose up service1 service2 startet nur die genannten Services und deren depends_on-Abhängigkeiten. Profile für Test-Gruppen nutzen.
4Unterschied zwischen --wait und depends_on?
depends_on ist deklarativ in der Compose-Datei. --wait beim CLI-Befehl wartet, bis alle Healthchecks grün sind, bevor er zurückgibt.
5Wie initialisiere ich die Testdatenbank?
SQL-Dateien als ro-Volume in /docker-entrypoint-initdb.d/. MySQL/Postgres führen sie beim ersten Start automatisch aus.
6Wie kopiere ich Ergebnisse aus dem Container?
docker compose cp service:/pfad ./host kopiert Dateien aus Container. Alternativ benanntes Volume für Ergebnisse einbinden.
7Playwright in Docker — besondere Anforderungen?
Offizielles mcr.microsoft.com/playwright-Image enthält alle Browser-Deps. Headless, kein GPU nötig. App-Healthcheck als depends_on-Gate definieren.
8Container bei Testfehler sicher aufräumen?
trap cleanup EXIT im Shell-Skript. if: always() in GitHub Actions. when: always in GitLab CI für den Teardown-Job.
9Wie debugge ich einen fehlgeschlagenen Integrationstest?
docker compose logs --follow während des Tests. docker compose exec app bash für interaktiven Zugriff. Festen Projektnamen setzen und down weglassen zum Inspizieren.
10Wie schnell startet die Testumgebung?
Mit gecachten Images 30–90 Sekunden. MySQL ist Flaschenhals. Start-Overhead durch Service-Wiederverwendung zwischen Test-Suiten reduzieren.