tva
← Insights

Über hundert Docker-Container: Unsere monatliche Zustandsprüfung

Mehr als hundert Docker-Container im Produktivbetrieb sind nichts Ungewöhnliches, wenn über mehrere Jahre schrittweise Dienste hinzugekommen sind. Ein selbst betriebener Supabase-Stack umfasst allein dreizehn Container. Dazu kommen ein Web-Frontend, mehrere API-Dienste, Hintergrundprozesse, ein Monitoring-Stack und eine zentrale Logsammlung. Die Zahl steigt schneller, als die meisten Planungen vorsehen. Schwierig ist nicht die Bereitstellung dieser Container, sondern ihre Wartung.

Die meisten Dokumentationen erklären, wie Container gestartet werden. Nur wenige behandeln, was sechs Monate später passiert, wenn an einem Sonntag die Festplatte voll ist, ein Drittel der Container keine Neustartrichtlinie hat oder ein SSL-Zertifikat unbemerkt abgelaufen ist, weil die Monitoring-Warnung stummgeschaltet war. Dieser Beitrag dokumentiert unsere monatliche Routine für einen Produktivserver mit über hundert Containern.

Mit der Festplatte beginnen

In lange laufenden Docker-Umgebungen ist eine volle Festplatte die häufigste Ursache akuter Ausfälle. Docker sammelt Daten an, die mit üblichen Systemwerkzeugen nicht sichtbar werden. df -h zeigt die Dateisystembelegung, verrät aber nicht, dass Docker fünfzig Gigabyte an Schichten gestoppter Container, unreferenzierten Images und Build-Cache aus sechs Monaten fortlaufender Deployments vorhält.

Der richtige Einstieg ist docker system df. Der Befehl schlüsselt die Belegung nach Images, Containern, lokalen Volumes und Build-Cache auf. Das Ergebnis überrascht häufig. Wir haben Server gesehen, auf denen allein der Build-Cache größer war als die Schichten aller laufenden Container zusammen – unbemerkt angesammelt durch monatelange CI-Builds, deren Daten nie bereinigt wurden.

Entscheidend ist jedoch die Spalte für freigebbaren Speicherplatz. Bevor wir etwas ändern, erfassen wir den Ausgangswert: Wie viel Speicher lässt sich aktuell freigeben, und wie entwickelt sich dieser Wert von Monat zu Monat? Wächst der freigebbare Speicher, muss die Bereinigung gründlicher oder häufiger erfolgen.

Wir bereinigen in dieser Reihenfolge. Zuerst anonyme Volumes, auf die kein Container mehr verweist. Seit Docker 23.0 lässt docker volume prune ungenutzte benannte Volumes unangetastet, sofern Sie nicht --all hinzufügen. Prüfen Sie vor dem Hinzufügen dieser Option, dass kein nicht zugeordnetes Volume noch benötigte Daten enthält:

docker volume prune -f

Danach unreferenzierte Images – Schichten ohne Tag, auf die weder laufende noch gestoppte Container verweisen:

docker image prune -f

Zum Schluss, sofern wir bestätigt haben, dass ein vollständiger Neuaufbau innerhalb unseres Wiederherstellungszeitraums möglich ist, alle ungenutzten Images, die älter als sieben Tage sind:

docker image prune -a --filter "until=168h" -f

Das Flag -a entfernt alle ungenutzten Images, nicht nur solche ohne Referenz. Wir führen den Befehl erst aus, nachdem wir geprüft haben, dass sich alle Dienste innerhalb der akzeptablen Wiederherstellungszeit aus der Registry neu aufbauen lassen. Diese Prüfung erfolgt vor dem Befehl, nicht danach.

Neustartrichtlinien

Neustartrichtlinien bestimmen, was passiert, wenn ein Container unerwartet endet oder der Docker-Daemon nach einem Neustart des Hosts wieder startet. Die meisten Deployment-Leitfäden erwähnen das nur kurz. Eine falsch konfigurierte Richtlinie kann jedoch dazu führen, dass Dienste zwei Wochen lang unbemerkt gestoppt bleiben, weil die Monitoring-Warnung auf den falschen Endpunkt gerichtet war.

Docker bietet vier Neustartrichtlinien. no ist der Standard: Der Container startet unter keinen Umständen neu. always startet ihn nach jedem Stopp neu, unabhängig vom Exit-Code; nach einem manuellen docker stop läuft er erst wieder an, wenn der Daemon neu startet oder der Container erneut gestartet wird. unless-stopped verhält sich wie always, berücksichtigt aber ausdrückliche Stopps: Wenn Sie vor einem Neustart docker stop ausführen, bleibt der Container danach gestoppt. on-failure[:max-retries] startet nur bei Exit-Codes ungleich null neu; optional begrenzt eine maximale Zahl von Versuchen die Wiederholungen.

Für zustandslose Webdienste und API-Worker verwenden wir unless-stopped. Wenn wir einen Container in einem Wartungsfenster bewusst stoppen, soll er nach dem nächsten Neustart gestoppt bleiben, statt unerwartet wieder anzulaufen. always würde ihn beim nächsten Neustart des Daemons wieder starten, obwohl wir ihn bewusst gestoppt haben.

Für Datenbankmigrationscontainer und einmalige Initialisierungsaufgaben ist no die richtige Richtlinie. Eine fehlgeschlagene Migration soll nicht in einer Schleife laufen. on-failure:3 eignet sich für Container, die kurzzeitig auf eine möglicherweise vorübergehend nicht verfügbare Abhängigkeit warten sollen, etwa einen externen Warteschlangen-Consumer, der einen Broker erreichen muss. Sie sollen jedoch nicht unbegrenzt erneut starten.

Unsere monatliche Prüfung verwendet einen einzigen Befehl für alle Container:

docker inspect --format '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' $(docker ps -aq)

Jeder Container mit der Richtlinie no, der nicht absichtlich als einmalige Aufgabe vorgesehen ist, wird geprüft. Meist wurde ein Dienst während eines Vorfalls mit docker run gestartet und anschließend nie mit einer passenden Neustartrichtlinie in die Compose-Konfiguration aufgenommen.

Logrotation

Der standardmäßige Docker-Logging-Treiber ist json-file. Er setzt standardmäßig keine Größenbegrenzung. Ein Container mit einem mäßigen Strom von Logzeilen kann über mehrere Monate Hunderte Gigabyte erzeugen. Das ist kein theoretisches Problem, sondern eine häufige Ursache voller Festplatten auf Produktivservern, die ohne gezielte Logverwaltung eingerichtet wurden.

Die Lösung ist eine globale Richtlinie in /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}

Damit sind die Logs jedes Containers auf fünf Dateien mit jeweils hundert Megabyte begrenzt – maximal fünfhundert Megabyte pro Container. Nach dieser Änderung muss der Docker-Daemon neu gestartet werden. Entscheidend ist außerdem, dass die Container neu erstellt werden müssen. Ein bloßer Neustart aktiviert die neuen Logeinstellungen nicht.

Die Einstellung in daemon.json gilt nur für Container, die nach der Änderung erstellt werden. Bestehende Container behalten ihre ursprüngliche Logkonfiguration unbegrenzt bei. Das ist der häufigste Fehler, den wir sehen: Die Richtlinie wird gesetzt, der Daemon neu gestartet, und das Team nimmt an, dass nun alle Container die Vorgaben erfüllen. Das tun sie nicht. Unsere monatliche Prüfung kontrolliert die Logkonfiguration jedes Containers:

docker inspect --format '{{.Name}} {{.HostConfig.LogConfig}}' $(docker ps -q)

Container ohne ausdrückliche Größenbegrenzung werden im nächsten Wartungsfenster mit der aktualisierten Konfiguration neu erstellt. Die Reihenfolge ist wichtig: Zustandsbehaftete Dienste müssen ihre Datenvolumes behalten, und abhängige Dienste müssen in der richtigen Reihenfolge starten.

Ablauf von SSL-Zertifikaten

SSL-Zertifikate laufen ab. Automatisiertes Monitoring erkennt die meisten Fälle. Es kann aber auch falsch konfiguriert sein, zu Warnmüdigkeit führen oder zusammen mit dem überwachten Dienst unbemerkt ausfallen. Unsere monatliche Routine enthält deshalb eine manuelle Prüfung, die von automatisierten Systemen unabhängig ist.

Für jede öffentlich erreichbare Domain prüfen wir das Zertifikat direkt:

echo | openssl s_client -connect domain.com:443 -servername domain.com 2>/dev/null   | openssl x509 -noout -enddate

Das gibt das Datum notAfter aus. Jedes Zertifikat, das innerhalb von dreißig Tagen abläuft, kommt sofort in die Warteschlange zur Erneuerung – unabhängig von der Anzeige im Monitoring-Dashboard. Die manuelle Prüfung dient als zusätzliche Absicherung.

Bei selbst verwalteter Zertifikatsinfrastruktur, die wir für mehrere interne Dienste betreiben, prüfen wir Zwischenzertifikate getrennt von Endzertifikaten. Ein abgelaufenes Zwischenzertifikat lässt die Prüfung der gesamten Kette scheitern, selbst wenn das Endzertifikat noch gültig ist. Dieser Fehler ist weniger offensichtlich als ein abgelaufenes Endzertifikat: Browser und Clients melden möglicherweise verwirrende Fehler statt der eindeutigen Meldung „certificate expired“, die die meisten Entwickler erwarten.

Wir pflegen ein Shell-Skript, das eine Domainliste durchläuft, das Ablaufdatum mit openssl ausliest und bei weniger als dreißig Tagen eine Warnung sowie bei weniger als sieben Tagen einen kritischen Alarm ausgibt. Das Skript läuft als Cronjob. Bei der monatlichen Prüfung führen wir es zusätzlich manuell aus, um zu bestätigen, dass die Cron-Ausgabe korrekt war. Cronjobs fallen häufiger unbemerkt aus, als viele erwarten.

Container-Ressourcenlimits

Ohne Speicherlimits kann ein fehlerhafter Container den Arbeitsspeicher des Hosts aufbrauchen und den OOM-Killer des Kernels dazu bringen, unbeteiligte Prozesse zu beenden. Ohne CPU-Limits kann ein außer Kontrolle geratener Prozess benachbarten Containern lange genug Rechenzeit entziehen, um eine Ausfallkaskade auszulösen. Beides sind keine seltenen Sonderfälle.

Bei der monatlichen Prüfung kontrollieren wir die Ressourcenlimits aller laufenden Container:

docker stats --no-stream --format "table {{.Name}}	{{.CPUPerc}}	{{.MemUsage}}"

Die Speicherspalte zeigt Verbrauch und Limit gemeinsam an. Bei einem Container ohne Speicherlimit wird der gesamte Arbeitsspeicher des Hosts als Limit angezeigt; 0B / 0B erscheint nur bei Containern, die nicht laufen. Wir prüfen jeden und legen ein geeignetes Limit fest. Für zustandslose HTTP-Dienste ist das Zwei- bis Vierfache des beobachteten Arbeitsspeicherbedarfs ein vernünftiger Ausgangspunkt. Es geht nicht um höchste Präzision, sondern darum, dass unbegrenztes Wachstum keine anderen Dienste auf demselben Host lahmlegt.

Wir betrachten dabei auch die CPU-Auslastung. Ein Container, der dauerhaft nahe hundert Prozent CPU liegt (in docker stats entsprechen 100 % einem vollständig ausgelasteten Kern; auf einem Mehrkernhost sind daher höhere Werte möglich), deutet auf einen außer Kontrolle geratenen Prozess oder zu wenig Ressourcen für seine Arbeitslast hin. Beide Fälle sollten vor dem nächsten Monat untersucht werden.

Aktualität der Images prüfen

Basis-Images erhalten Sicherheitsupdates in unregelmäßigen Abständen. Ein Container mit einem vor sechs Monaten aktuellen Image kann inzwischen eine nginx- oder PostgreSQL-Version mit bekannten Schwachstellen verwenden. Wir laden nicht jeden Monat automatisch alle Images neu und stellen sämtliche Container erneut bereit – das erzeugt mehr Risiko, als es reduziert. Wir vergleichen jedoch die laufenden mit den aktuellen Versionen.

Praktisch heißt das: Bei jedem Dienst mit festgelegter Image-Version vergleichen wir diese einmal im Monat mit dem Änderungsprotokoll des Projekts. Bei Diensten mit veränderlichen Tags wie latest oder 16-alpine laden wir das Image und vergleichen seinen Digest, um Änderungen festzustellen. Wenn es Änderungen gibt, prüfen wir sie vor dem Deployment.

Wichtiger ist allerdings, von veränderlichen Tags wegzukommen. Ein Dienst, der unbemerkt mit einer inkompatiblen Änderung neu bereitgestellt wurde, weil sein latest-Tag auf eine neue Hauptversion zeigte, lässt sich schwerer diagnostizieren als ein Dienst mit einem bekanntermaßen alten Image. Legen Sie Versionen fest und aktualisieren Sie sie bewusst.

Die Disziplin

Insgesamt deckt die monatliche Zustandsprüfung sechs Bereiche ab: Speicherbelegung und Bereinigung, Neustartrichtlinien, Logrotation, Ablauf von SSL-Zertifikaten, Ressourcenlimits und Aktualität der Images. Auf einem gut dokumentierten Server benötigen diese Aufgaben insgesamt nicht mehr als neunzig Minuten. Der Wert liegt nicht in den einzelnen Prüfungen, sondern darin, sie regelmäßig vor einem Ausfall durchzuführen.

Produktivsysteme verschlechtern sich schrittweise. Daten sammeln sich an, Logs wachsen, Zertifikate altern und Images bleiben zurück. Keiner dieser Vorgänge erzeugt einen Alarm, bevor ein Grenzwert überschritten wird. Die monatliche Prüfung macht aus reaktiver Wartung einen vorhersehbaren Aufwand und verändert damit die Arbeitsweise im Betrieb grundlegend.


Weiterführende Beiträge

Weiterführende Beiträge