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.
Inhaltsverzeichnis
- 1. Warum Docker Compose für Tests der richtige Ansatz ist
- 2. Grundstruktur einer Test-Compose-Datei
- 3. Healthchecks und depends_on: Services wirklich bereit warten
- 4. Netzwerkisolation: Testumgebungen sauber trennen
- 5. Datenbankinitialisierung und Fixtures reproduzierbar einrichten
- 6. End-to-End-Tests mit Playwright und Cypress im Container
- 7. Docker Compose in GitHub Actions und GitLab CI einbinden
- 8. Sauberes Teardown: Volumes, Logs und Artefakte sichern
- 9. Teststrategie-Vergleich: mit und ohne Docker Compose
- 10. Zusammenfassung
- 11. FAQ
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?
condition: service_healthy mit Healthcheck in depends_on löst das.2Wie verhindere ich Port-Konflikte in parallelen CI-Builds?
--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?
--wait beim CLI-Befehl wartet, bis alle Healthchecks grün sind, bevor er zurückgibt.5Wie initialisiere ich die Testdatenbank?
/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?
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.