und Wait-For-Patterns richtig lösen
Der häufigste Fehler bei Multi-Service-Docker-Stacks: eine Anwendung startet und versucht, sich mit der Datenbank zu verbinden, bevor diese bereit ist. depends_on ohne condition löst nur die Startreihenfolge der Container — nicht die Bereitschaft des Services dahinter. Dieser Artikel zeigt alle Patterns, die das Problem zuverlässig lösen: von Health-Check-Abhängigkeiten über wait-for-it bis zu robuster applikationsseitiger Retry-Logik.
Inhaltsverzeichnis
- 1. Das Race-Condition-Problem beim Container-Start
- 2. depends_on: Container-Reihenfolge vs. Service-Bereitschaft
- 3. Health-Check-basierte Abhängigkeiten mit condition
- 4. wait-for-it und dockerize: externe Wartetools
- 5. Init-Container-Pattern: Voraussetzungen trennen
- 6. Applikationsseitige Retry-Logik: der zuverlässigste Ansatz
- 7. Datenbankbereitschaft sicher erkennen
- 8. Startup-Probleme debuggen und diagnostizieren
- 9. Wait-For-Patterns im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Race-Condition-Problem beim Container-Start
Eine Race Condition beim Container-Start ist der Zustand, in dem ein Service versucht, auf eine Abhängigkeit zuzugreifen, die noch nicht bereit ist. Das häufigste Szenario: Eine PHP-Anwendung startet und versucht beim ersten Request, eine MySQL-Verbindung aufzubauen. MySQL startet langsamer, weil es auf seinen ersten Start hin Initialisierungsskripte ausführt, die Datenbank-Dateien anlegt und auf freien Port wartet. Die PHP-Anwendung erhält einen Connection-Refused-Fehler und schlägt fehl – nicht weil MySQL grundsätzlich nicht erreichbar ist, sondern weil MySQL in diesem Moment noch nicht bereit war.
Das Problem ist nicht auf Datenbankverbindungen beschränkt. Redis muss bereit sein bevor ein Cache-Warmer ihn befüllt. Ein Message-Queue-Service muss bereit sein bevor Worker Nachrichten konsumieren. Ein API-Gateway muss bereit sein bevor Downstream-Services Traffic weiterleiten. In jedem dieser Fälle gibt es eine zeitliche Abhängigkeit zwischen Services, die Docker Compose abbilden muss. Das naive depends_on ohne weitere Konfiguration löst nur die Frage, wann ein Container gestartet wird – nicht wann der Service dahinter tatsächlich anfragen verarbeiten kann. Die Lücke zwischen Container-Start und Service-Bereitschaft ist der Kern des Startup-Reihenfolge-Problems.
Dieses Problem wird häufig unterschätzt, weil es in der Entwicklung nicht auftritt: Auf einem lokalen Rechner mit warmen Caches startet MySQL schnell genug, dass die Race Condition nicht zutage tritt. In CI-Umgebungen mit frischen Volumes, in Staging-Umgebungen mit größerem Datenbankdatenbestand oder nach einem ungeplanten Neustart unter Last wird die Race Condition dann sichtbar. Die korrekte Lösung ist nicht schnelleres Starten der Services, sondern robuste Wait-For-Patterns, die auf tatsächliche Service-Bereitschaft warten.
2. depends_on: Container-Reihenfolge vs. Service-Bereitschaft
Das einfache depends_on in Docker Compose gibt an, in welcher Reihenfolge Container gestartet werden sollen. depends_on: [db, redis] bedeutet: Starte web erst nachdem die Container db und redis gestartet wurden. Das sagt nichts darüber aus, ob MySQL nach dem Start sofort Verbindungen akzeptiert oder ob Redis fertig initialisiert ist. Ein MySQL-Container ist nach wenigen Millisekunden "gestartet" – der MySQL-Prozess ist möglicherweise erst nach 10–30 Sekunden bereit, Verbindungen anzunehmen. Diese Lücke ist der Startup-Race-Condition-Zeitfenster.
Compose v2 erweitert depends_on um das condition-Feld, das drei Werte kennt: service_started (das alte Verhalten: Container läuft), service_healthy (wartet bis Health-Check healthy ist) und service_completed_successfully (wartet auf Exit-Code 0 – nützlich für Init-Container). Mit condition: service_healthy wird das Startup-Reihenfolge-Problem auf die Ebene des Health-Checks delegiert: Der Health-Check muss korrekt konfiguriert sein und tatsächlich die Bereitschaft des Services prüfen, nicht nur ob der Prozess läuft. Das verschiebt die Komplexität vom depends_on-Block in den HEALTHCHECK-Block – wo sie hingehört.
# docker-compose.yml — depends_on with health-check conditions
# Compose v2 format: condition-based startup ordering
services:
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: app
# Health check: only healthy when MySQL accepts real connections
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-psecret"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s # Grace period for initial MySQL setup
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
web:
image: registry.example.com/app:latest
depends_on:
db:
condition: service_healthy # Wait until MySQL health check passes
restart: true # Restart web if db container restarts
redis:
condition: service_healthy
environment:
DB_HOST: db
REDIS_HOST: redis
migrations:
image: registry.example.com/app:latest
command: ["php", "bin/console", "doctrine:migrations:migrate", "--no-interaction"]
depends_on:
db:
condition: service_healthy
restart: "no" # Run once and exit
web-after-migrations:
image: registry.example.com/app:latest
depends_on:
migrations:
condition: service_completed_successfully # Wait for exit code 0
redis:
condition: service_healthy
3. Health-Check-basierte Abhängigkeiten mit condition
Der Health-Check ist der Schlüsselmechanismus für condition: service_healthy. Ein schlecht konfigurierter Health-Check macht die condition nutzlos – er meldet healthy, bevor der Service wirklich bereit ist. Für MySQL ist der minimale, zuverlässige Health-Check: mysqladmin ping, das eine Verbindung aufbaut und eine Antwort erwartet. Ein besserer Health-Check führt zusätzlich eine SQL-Query aus: mysql -u root -psecret -e "SELECT 1". Das stellt sicher, dass nicht nur der Prozess läuft, sondern auch die Datenbank initialisiert wurde und Queries akzeptiert.
Das start_period-Feld im Health-Check ist für die Startup-Reihenfolge besonders wichtig: Es definiert einen Anfangs-Zeitraum, in dem Health-Check-Fehler nicht als Fehler gewertet werden und den Container nicht in den "unhealthy"-Status setzen. Für MySQL mit Datenbank-Initialisierung sind 30 Sekunden ein realistischer Wert. Nach Ende der start_period zählen Fehlschläge gegen die retries-Grenze. Dieses Timing-Setup muss auf den tatsächlichen Startvorgang der Abhängigkeit abgestimmt sein – zu kurze start_period führt zu falschen Unhealthy-Signalen, zu lange verzögert das Erkennen echter Fehler.
Ein wichtiger Punkt für depends_on mit condition: service_healthy: Wenn der abhängige Service nicht in den healthy-Zustand kommt (weil z.B. MySQL nicht korrekt konfiguriert wurde), bleibt der wartende Service ewig im Startzustand. Das ist tatsächlich das gewünschte Verhalten für die Startup-Reihenfolge – besser wartend als mit fehlerhafter Datenbankverbindung startend –, erfordert aber ein funktionierendes Monitoring, das stuck-Services erkennt und alarmiert.
4. wait-for-it und dockerize: externe Wartetools
wait-for-it ist ein Shell-Script, das auf TCP-Port-Verfügbarkeit wartet und dann einen Befehl ausführt. Es wird als Entrypoint-Wrapper genutzt: ENTRYPOINT ["wait-for-it", "db:3306", "--timeout=60", "--", "php-fpm"]. Sobald Port 3306 auf dem db-Host erreichbar ist, wird PHP-FPM gestartet. Dieses Pattern hat einen wichtigen Nachteil: TCP-Port-Verfügbarkeit ist kein Beweis für Service-Bereitschaft. MySQL akzeptiert TCP-Verbindungen auf Port 3306, bevor die Initialisierung abgeschlossen ist, und sendet dann einen "too many connections"- oder "system table missing"-Fehler. wait-for-it würde trotzdem fortfahren und PHP-FPM starten, das dann mit einer nicht vollständig initialisierten Datenbank konfrontiert wird.
dockerize ist ein umfangreicheres Tool, das zusätzlich HTTP-Endpunkte prüfen, Template-Rendering ausführen und mehrere Abhängigkeiten gleichzeitig überwachen kann. Für Anwendungen mit HTTP-basierten Health-Endpoints ist dockerize -wait http://db:8080/health -timeout 60s zuverlässiger als TCP-Port-Prüfung. Beide Tools lösen das Wait-For-Pattern auf Container-Entrypoint-Ebene, ohne dass die Anwendung selbst Retry-Logik implementieren muss. Sie sind nützlich für Services, die keine eigene Retry-Logik haben (Legacy-Anwendungen, Drittanbieter-Images) oder für schnelle Setups in Entwicklungsumgebungen.
# wait-for-db.sh — Robust wait-for pattern with actual connection test
# More reliable than wait-for-it (checks real connectivity, not just TCP port)
#!/bin/sh
set -e
HOST="${DB_HOST:-db}"
PORT="${DB_PORT:-3306}"
USER="${DB_USER:-root}"
PASSWORD="${DB_PASSWORD:-secret}"
MAX_ATTEMPTS="${WAIT_TIMEOUT:-60}"
INTERVAL=2
echo "[WAIT] Waiting for MySQL at ${HOST}:${PORT}..."
attempt=0
until mysqladmin ping -h "${HOST}" -P "${PORT}" -u "${USER}" -p"${PASSWORD}" \
--connect-timeout=5 --silent 2>/dev/null; do
attempt=$((attempt + 1))
if [ "${attempt}" -ge "${MAX_ATTEMPTS}" ]; then
echo "[ERROR] MySQL not ready after $((attempt * INTERVAL))s — aborting"
exit 1
fi
echo "[WAIT] Attempt ${attempt}/${MAX_ATTEMPTS} — retrying in ${INTERVAL}s..."
sleep "${INTERVAL}"
done
echo "[OK] MySQL is ready — starting application"
exec "$@"
5. Init-Container-Pattern: Voraussetzungen trennen
Das Init-Container-Pattern trennt einmalige Setup-Aufgaben von der Hauptanwendung in separate Container, die vollständig abgeschlossen sein müssen bevor der Haupt-Service startet. Typische Init-Container: Datenbankmigrationen ausführen, Konfigurationsdateien aus Vault-Secrets generieren, Dateirechte setzen oder Schemata validieren. In Docker Compose wird das durch condition: service_completed_successfully implementiert: Der Haupt-Service wartet nicht nur auf den Start, sondern auf den erfolgreichen Abschluss (Exit-Code 0) des Init-Containers.
Das Init-Container-Pattern löst ein Problem, das weder depends_on noch wait-for-it allein lösen können: Es stellt sicher, dass idempotente Voraussetzungen genau einmal und vollständig ausgeführt wurden, bevor der Service startet. Ein Migrationscontainer, der die Datenbank auf den aktuellen Schemastand bringt, muss garantiert abgeschlossen sein, bevor PHP-FPM seinen ersten Request annimmt. Mit restart: "no" am Migrationscontainer wird verhindert, dass er bei einem Neustart des Compose-Stacks erneut läuft – der nächste Stack-Start erkennt via Compose-Exit-Code, dass der Init-Container bereits erfolgreich war.
6. Applikationsseitige Retry-Logik: der zuverlässigste Ansatz
Die zuverlässigste Lösung für das Startup-Reihenfolge-Problem ist applikationsseitige Retry-Logik: Die Anwendung selbst probiert die Datenbankverbindung beim Start wiederholt, bevor sie aufgibt. Das entspricht dem, wie professionelle Anwendungen in verteilten Systemen mit transienten Fehlern umgehen. Eine PHP-Anwendung, die beim Start eine PDO-Exception erhält, sollte diese nicht sofort als fatalen Fehler behandeln, sondern mit exponential Backoff erneut versuchen – bis zu einem konfigurierbaren Timeout. Dieses Verhalten macht die Anwendung resilient gegen Restart-Szenarien, temporäre Datenbankausfälle und Deployment-Race-Conditions gleichermaßen.
Für PHP gilt: PDO-Verbindungen werden in vielen Frameworks beim ersten Datenbankzugriff aufgebaut, nicht beim Start. Das bedeutet, die Startup-Race-Condition tritt oft erst beim ersten Request auf, nicht beim Container-Start. Ein expliziter Verbindungstest beim Startup mit Retry-Logik ist deshalb ein wertvoller Schritt im Entrypoint-Skript. Das Muster: Beim Container-Start eine Test-Connection aufbauen, bei Fehler schlafen und erneut versuchen, nach einem Timeout mit aussagekräftiger Fehlermeldung abbrechen. So scheitert der Container sauber und Kubernetes oder Compose kann ihn neu starten – mit frischer Wartezeit für die Datenbank.
# entrypoint.sh — Application entrypoint with retry logic for all dependencies
#!/bin/sh
set -e
# Retry function: exponential backoff with max attempts
wait_for_service() {
local name="$1"
local check_cmd="$2"
local max_attempts="${3:-30}"
local attempt=0
local wait_time=2
echo "[WAIT] Checking ${name}..."
until eval "${check_cmd}" 2>/dev/null; do
attempt=$((attempt + 1))
if [ "${attempt}" -ge "${max_attempts}" ]; then
echo "[ERROR] ${name} not available after ${attempt} attempts — failing"
exit 1
fi
echo "[WAIT] ${name} not ready (attempt ${attempt}/${max_attempts}), retrying in ${wait_time}s..."
sleep "${wait_time}"
# Exponential backoff: double wait time up to 30s
wait_time=$(( wait_time < 30 ? wait_time * 2 : 30 ))
done
echo "[OK] ${name} is ready"
}
# Check MySQL: real connection test, not just TCP port
wait_for_service "MySQL" \
"mysqladmin ping -h ${DB_HOST} -u ${DB_USER} -p${DB_PASSWORD} --silent" \
30
# Check Redis: PING command
wait_for_service "Redis" \
"redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT:-6379} ping | grep -q PONG" \
15
# Optional: check Elasticsearch
# wait_for_service "Elasticsearch" \
# "curl -sf http://${ES_HOST}:9200/_cluster/health | grep -q '\"status\":\"green\"'"
echo "[OK] All dependencies ready — starting application"
exec "$@"
7. Datenbankbereitschaft sicher erkennen
MySQL und PostgreSQL haben unterschiedliche Signale für "bereit". Bei MySQL ist mysqladmin ping der minimale Test, aber er schlägt bei einer noch laufenden Initialisierung eventuell falsch positiv an. Ein sichererer Test für MySQL: eine SELECT-Abfrage auf eine Systemtabelle ausführen, die nur verfügbar ist, wenn die Initialisierung abgeschlossen ist. Für PostgreSQL ist pg_isready -U user -d dbname der empfohlene Readiness-Check – er prüft nicht nur Verbindbarkeit, sondern auch, ob die angegebene Datenbank existiert und Verbindungen akzeptiert. Diese spezifischen Datenbankbereitschafts-Checks sind zuverlässiger als generische TCP-Port-Checks.
Ein kritischer Aspekt wird oft übersehen: Datenbankbereitschaft beim Start ist nicht identisch mit anhaltender Verfügbarkeit. Nach einem erfolgreichen Health-Check kann eine Datenbank überlastet werden, einen Crash erleiden oder aus dem Netzwerk verschwinden. Applikationsseitige Retry-Logik für Datenbankfehler ist deshalb auch nach dem erfolgreichen Start notwendig. Die Startup-Reihenfolge garantiert einen sauberen Start; Resilience-Patterns in der Anwendung garantieren korrekte Funktion über den Start hinaus. Beide sind notwendig, keiner ersetzt den anderen.
8. Startup-Probleme debuggen und diagnostizieren
Startup-Race-Conditions sind schwer zu debuggen, weil sie zeitabhängig sind und unter Lastbedingungen anders auftreten als in ruhigen Entwicklungsumgebungen. Der erste Schritt: docker compose logs --follow zeigt alle Service-Logs mit Zeitstempeln. Die Reihenfolge der Log-Nachrichten zeigt, welcher Service wann gestartet hat und wann er Fehler produziert hat. Das Muster "Application connecting to DB failed" kurz nach "MySQL starting" ist das klassische Race-Condition-Signal.
Das zweite Diagnosewerkzeug: docker inspect <container-id> zeigt den Health-Check-Status und die Ergebnisse der letzten Health-Check-Ausführungen. Bei einem stuck-Container im "starting"-State wegen eines failing health checks zeigt docker inspect --format='{{json .State.Health}}' die letzten Fehler und Zeitstempel. Das hilft dabei zu unterscheiden, ob der Health-Check grundsätzlich falsch konfiguriert ist (falscher Befehl, falscher User) oder ob der Service schlicht noch nicht fertig initialisiert war (zu kurze start_period). Das Logging aus dem Entrypoint-Wartes-Script mit Zeitstempeln ist ebenfalls wertvoll: Es zeigt, wie lange gewartet wurde und ob die Datenbank letztlich erreichbar wurde.
9. Wait-For-Patterns im direkten Vergleich
Jedes Wait-For-Pattern hat seinen Platz je nach Anforderungsprofil. Die richtige Wahl hängt von der Kontrolle über die Anwendung, der Umgebung und der Zuverlässigkeitsanforderung ab.
| Pattern | Prüft | Zuverlässigkeit | Eignet sich für |
|---|---|---|---|
| depends_on (einfach) | Container läuft | Gering | Services ohne Startup-Latenz |
| depends_on + Health-Check | Service wirklich bereit | Hoch | Standard für Datenbankabhängigkeiten |
| wait-for-it | TCP-Port offen | Mittel | Legacy-Images ohne Health-Check |
| Custom Entrypoint + Retry | Echte Konnektivität | Hoch | Eigene Images mit voller Kontrolle |
| Init-Container | Setup-Aufgabe abgeschlossen | Sehr hoch | Migrationen und Einmalsetup |
Für neue Projekte ist die Empfehlung: depends_on mit condition: service_healthy als Basis-Pattern, ergänzt durch applikationsseitige Retry-Logik für Resilience nach dem Start. Init-Container für Datenbankmigrationen. Das sind drei Pattern, die sich gegenseitig ergänzen und gemeinsam das Startup-Reihenfolge-Problem vollständig lösen – ohne externes Tooling wie wait-for-it, das ein weiteres Binary im Image erfordert.
Mironsoft
Docker-Infrastruktur, Multi-Service-Orchestrierung und Startup-Resilienz
Flaky Starts und Race Conditions ein für alle Mal lösen?
Wir analysieren eure Docker-Compose-Konfigurationen, implementieren Health-Check-basierte depends_on-Abhängigkeiten, Init-Container für Migrationen und robuste Retry-Logik für zuverlässige Multi-Service-Starts in Entwicklung und Produktion.
Compose-Review
Startup-Reihenfolge analysieren und depends_on mit korrekten Health-Checks konfigurieren
Init-Container-Setup
Datenbankmigrationen und Einmalsetup als dedizierte Init-Container isolieren
Retry-Logik
Applikationsseitige Retry-Logik mit exponential Backoff für alle Abhängigkeiten
10. Zusammenfassung
Das Container-Startup-Reihenfolge-Problem in Docker ist kein Randthema – es ist eine der häufigsten Ursachen für flaky Startups in Multi-Service-Stacks. Die Lösung ist mehrschichtig: depends_on mit condition: service_healthy koordiniert die Startreihenfolge auf Compose-Ebene und wartet auf tatsächliche Service-Bereitschaft. Health-Checks, die echte Konnektivität statt nur Port-Verfügbarkeit prüfen, sind die Grundlage dieses Mechanisms. Init-Container isolieren Einmal-Aufgaben wie Datenbankmigrationen. Applikationsseitige Retry-Logik sorgt für Resilienz über den initialen Start hinaus.
Das wichtigste Lernziel: Wait-For-Patterns sind keine Notlösung, sondern ein notwendiger Bestandteil jeder produktionsreifen Container-Konfiguration. Die Wahl des richtigen Patterns hängt von der Kontrolle über die Images ab: Health-Check + depends_on für eigene Images, Entrypoint-Wartes-Scripts für Drittanbieter-Images, applikationsseitige Retry-Logik als robuste Grundlage für alle Services. Diese drei Ansätze kombiniert ergeben einen Multi-Service-Stack, der zuverlässig startet – unabhängig von der Reihenfolge, in der Services hochfahren.
Container Startup-Reihenfolge — Das Wichtigste auf einen Blick
depends_on + Health-Check
condition: service_healthy wartet auf echten Health-Check-Erfolg, nicht nur Container-Start. start_period für Initialisierungszeit konfigurieren.
Init-Container
condition: service_completed_successfully für Migrationen. restart: "no" verhindert Neuausführung. Einmal und vollständig abgeschlossen vor Haupt-Service.
Retry-Logik
Applikationsseitig mit exponential Backoff. Macht Services resilient gegen transiente Fehler nach dem Start — nicht nur beim initialen Start.
Datenbankbereitschaft
mysqladmin ping oder pg_isready statt TCP-Port-Check. Echte Verbindung prüfen — Port offen bedeutet nicht, dass DB Queries akzeptiert.