Ein Upgrade von Docker Engine auf einem laufenden Produktivserver
Ein Upgrade von Docker Engine auf einem Server mit mehr als hundert laufenden Containern lässt wenig Raum für Improvisation. Als auf unserem Hetzner-Produktivserver der Wechsel von Docker Engine 28.1 auf 28.2 und von Docker Compose 2.35 auf 2.36 anstand, gingen wir mit derselben Disziplin vor wie bei einer Datenbankmigration: eine schriftliche Vorbereitungscheckliste, ein getesteter Weg zum Zurücksetzen, eine genaue Ausführungsreihenfolge und eine Prüfung nach dem Upgrade, bevor wir das Wartungsfenster beendeten.
Im Folgenden beschreiben wir unser Vorgehen vollständig: die Gründe für jeden Schritt, die tatsächlich ausgeführten Befehle und die Punkte, auf die Sie achten sollten, wenn eine falsch konfigurierte Neustartrichtlinie bei einer großen Containerzahl eine Kettenreaktion mit aufwendiger Wiederherstellung auslösen kann.
Warum dieses Upgrade ein formelles Betriebshandbuch erforderte
Docker Engine 28.2 führte neue Bridge-Netzwerkoptionen und eine Daemon-Option für Direktrouting zu Containern ein und entfernte die Unterstützung für das Pullen veralteter Schema-1-Images. Im selben Wartungsfenster wechselte das Compose-Plugin von 2.35.1 auf 2.36.0. Das ist ein Minor-Release, doch jede Änderung am Startverhalten eines Stacks sollte getestet werden, insbesondere depends_on-Blöcke mit condition: service_healthy.
Auf einem Server, auf dem eine einzelne docker-compose.yml zwölf voneinander abhängige Dienste steuern kann – Datenbanken, Reverse Proxies, Anwendungscontainer und Hintergrundprozesse –, ist eine unbemerkte Verhaltensänderung bei der Abhängigkeitsauflösung genau die Art von Problem, die nicht sofort sichtbar wird. Sie zeigt sich um 03:00 Uhr, wenn ein Healthcheck nie ausgelöst wird und ein abhängiger Container in einer Endlosschleife steckt.
Die praktische Antwort ist Vorbereitung, nicht Vorsicht um ihrer selbst willen.
Vorbereitungscheckliste
Bevor wir ein einziges Paket anfassten, arbeiteten wir folgende Checkliste durch. Jeder Punkt hat einen Grund.
1. Alle laufenden Container und ihre Neustartrichtlinien erfassen.
docker ps --format "table {{.Names}} {{.Status}} {{.Image}}" | sort
docker inspect $(docker ps -q) --format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}' | sort
Das liefert einen Referenzzustand. Container mit restart: always versuchen nach dem Neustart des Docker-Daemons automatisch wieder zu starten. Das ist gewünscht, aber nur, wenn die zugrunde liegenden Compose-Dateien und Images in einem nachweislich funktionierenden Zustand sind. Ein Container, der beim Upgrade wiederholt abstürzt und neu startet, wird das auch danach tun. Besser, Sie wissen das vorher.
2. Alle Compose-Dateien validieren, bevor das Upgrade durchgeführt wird.
docker compose config --quiet 2>&1 | grep -i warning
Führen Sie dies in jedem Verzeichnis aus, das eine docker-compose.yml enthält. Dabei wird die aktuell installierte Compose-Version verwendet: Der Befehl zeigt vorhandene Warnungen, weist aber keine Kompatibilität mit der Zielversion nach. Testen Sie dafür die Zielversion in einer Staging-Umgebung. Aktuelle Compose-Versionen warnen vor dem veralteten Schlüssel version: am Anfang von Compose-Dateien (er wird ignoriert). Die ältere links:-Direktive wird nicht mehr benötigt, da sich Dienste in einem gemeinsamen Netzwerk über ihren Namen erreichen. Warnungen können in späteren Releases zu Fehlern werden; es lohnt sich, sie jetzt zu beheben.
3. Verfügbaren Speicherplatz prüfen.
df -h /var/lib/docker
docker system df
Das Upgrade lädt einen neuen containerd-Shim herunter und ersetzt die Binärdatei von Docker Engine. Die alte Binärdatei und ihre Abhängigkeiten werden nicht immer automatisch entfernt. Auf einem seit einem Jahr laufenden Server zeigt docker system df häufig mehrere Gigabyte freigebbaren Speicher in Image-Schichten. Bereinigen Sie diese vor dem Upgrade, nicht währenddessen.
docker system prune -f --volumes
Verwenden Sie --volumes nur, wenn Sie sicher sind, dass keine anonymen Volumes Daten enthalten, die Sie benötigen. Auf unserem Server sind alle persistenten Daten in benannten, explizit deklarierten Volumes gemountet – daher war dies sicher.
4. Die aktuellen Versionen von Docker Engine und Compose in eine Referenzdatei exportieren.
docker version > /root/docker-pre-upgrade.txt
docker compose version >> /root/docker-pre-upgrade.txt
docker ps -a >> /root/docker-pre-upgrade.txt
dpkg-query -W docker-ce docker-ce-cli containerd.io docker-compose-plugin >> /root/docker-pre-upgrade.txt
Das dauert zehn Sekunden und liefert eine eindeutige Rollback-Referenz, falls etwas schiefgeht und Sie identifizieren müssen, welche Container zum Upgrade-Zeitpunkt liefen.
5. Nachgelagerte Systeme benachrichtigen.
Auf unserem Server laufen ein selbst betriebener Supabase-Stack und mehrere n8n-Instanzen zur Workflow-Automatisierung. Jeder Webhook, der während eines Container-Neustarts ausgelöst wird, schlägt fehl. Wenn der Sender keinen erneuten Versuch unternimmt, kann das Ereignis unbemerkt verloren gehen. Wir richteten in unserem Werkzeug zur Verfügbarkeitsüberwachung ein Wartungsfenster ein und deaktivierten für dessen Dauer eingehende Webhooks in n8n.
Der Rollback-Plan
Ein Rollback-Plan ist nur nützlich, wenn vorab feststeht, welche Bedingung ihn auslöst. Unser Kriterium war einfach: Wenn ein zuvor laufender Container zehn Minuten nach Abschluss des Upgrades nicht läuft und sich mit docker compose up -d nicht wieder starten lässt, setzen wir die Binärdatei von Docker Engine zurück.
Auf einem Debian-basierten System wird Docker Engine durch Festlegen der vorherigen Paketversion zurückgesetzt. Wir notierten vor Beginn die genaue Versionszeichenfolge:
apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin
Ausgabe auf unserem Server vor dem Upgrade:
docker-ce:
Installed: 5:28.1.1-1~debian.12~bookworm
Candidate: 5:28.2.2-1~debian.12~bookworm
docker-compose-plugin:
Installed: 2.35.1-1~debian.12~bookworm
Candidate: 2.36.0-1~debian.12~bookworm
Der Rollback-Befehl, falls benötigt:
CONTAINERD_PREV=$(awk '$1 == "containerd.io" {print $2}' /root/docker-pre-upgrade.txt)
apt-get install -y --allow-downgrades docker-ce=5:28.1.1-1~debian.12~bookworm docker-ce-cli=5:28.1.1-1~debian.12~bookworm containerd.io="${CONTAINERD_PREV}" docker-compose-plugin=2.35.1-1~debian.12~bookworm
Der Befehl legt auch containerd.io auf die in Schritt 4 in der Referenzdatei gespeicherte Version fest und übergibt --allow-downgrades, da apt-get -y abbricht, statt Pakete ohne interaktive Rückfrage auf eine ältere Version zurückzusetzen.
Wir speicherten den Befehl auf dem Server in der Datei /root/docker-rollback.sh mit Ausführungsrechten. So ließ er sich bei Bedarf sofort ausführen, ohne ihn unter Zeitdruck eintippen zu müssen.
Der Upgrade-Prozess
Das eigentliche Upgrade ist unkompliziert, sobald die Vorbereitung abgeschlossen ist. Die Reihenfolge ist wichtig: zuerst den Paket-Index aktualisieren, dann überprüfen, was installiert werden soll, dann installieren.
Schritt 1: Den apt-Index nur für das Docker-Repository aktualisieren.
apt-get update -o Dir::Etc::sourcelist="sources.list.d/docker.list" -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0"
Damit wird nur das Docker-Repository aktualisiert, statt alle Quellen neu einzulesen. Eine unbeabsichtigte Paketaktualisierung aus einer anderen Quelle während des Docker-Wartungsfensters ist auf einem Produktivserver eine unnötige zusätzliche Variable.
Schritt 2: Die Kandidatenversionen bestätigen.
apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin
Prüfen Sie vor dem Fortfahren, ob die zur Installation vorgesehene Version Ihren Erwartungen entspricht. Ist sie neuer als die in der Staging-Umgebung getestete Version, unterbrechen Sie den Vorgang und bewerten Sie die Situation.
Schritt 3: Installieren.
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Dabei wird der Docker-Daemon gestoppt, die Binärdateien werden ersetzt und der Daemon wird neu gestartet. Container mit restart: always startet der Daemon anschließend wieder. Auf einem angemessen dimensionierten Server dauert die Unterbrechung typischerweise weniger als dreißig Sekunden. Die genaue Dauer hängt jedoch davon ab, wie lange containerd zur Initialisierung braucht.
Den Neustart in einem zweiten Terminal überwachen:
watch -n 2 'docker ps --format "table {{.Names}} {{.Status}}" | sort'
Schritt 4: Die installierten Versionen überprüfen.
docker version
docker compose version
Auf unserem Server bestätigte die Ausgabe:
Docker Engine: 28.2.2
Docker Compose: v2.36.0
Docker Compose wird unabhängig von Docker Engine versioniert: Die Version des Pakets docker-compose-plugin (hier 2.36.0) ist die Compose-Version und wird vom apt-Index erfasst.
Prüfung nach dem Upgrade
Zehn Minuten nach dem Upgrade führten wir einen strukturierten Prüfdurchlauf durch. Dieser ist erforderlich: Der Daemon-Neustart startet die Container, garantiert aber nicht, dass Container mit Healthchecks diese tatsächlich bestanden haben.
Überprüfen, dass alle Container im erwarteten Zustand sind.
docker ps -a --format "table {{.Names}} {{.Status}} {{.RunningFor}}" | sort
Jeder Container mit dem Status Restarting oder Exited muss sofort untersucht werden. Auf unserem Server zeigte ein Container – ein Supabase-Analysedienst – Restarting (1). Er war bereits vor dem Upgrade in größeren Abständen wiederholt abgestürzt. Das Upgrade war nicht die Ursache, doch die Prüfung danach machte das Problem deutlicher sichtbar. Genau solche bestehenden Probleme soll die Bestandsaufnahme vor dem Upgrade erfassen.
Prüfen, ob die Healthchecks erfolgreich sind.
docker inspect $(docker ps -q) --format '{{.Name}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' | grep -v "health=none" | sort
Container ohne explizite Healthchecks haben keinen Gesundheitsstatus. Je nach Docker-Version gibt ein ungeschütztes {{.State.Health.Status}} für sie <no value> aus oder bricht den ganzen Befehl mit einem Vorlagenfehler ab. Die Vorlage prüft deshalb zuerst .State.Health, und grep filtert die Container ohne Healthcheck heraus. Relevant sind diejenigen, von denen andere Container über condition: service_healthy abhängen.
Die Compose-Konfiguration aller Stacks validieren.
for dir in /opt/*/; do
if [ -f "${dir}docker-compose.yml" ]; then
echo "--- $dir ---"
docker compose -f "${dir}docker-compose.yml" config --quiet 2>&1
fi
done
Damit erkennen Sie Compose-Dateidirektiven, die die neue Compose-Version als Warnung oder Fehler behandelt. Auf unserem Server meldeten drei Stacks eine Warnung zum veralteten Schlüssel version. Diese Warnungen sind kosmetischer Natur, sollten aber im nächsten Wartungszyklus behoben werden.
Die Docker-Daemon-Logs auf Fehler überprüfen.
journalctl -u docker --since "1 hour ago" | grep -i -E "error|warn|fatal"
Ein sauberes Upgrade produziert keine Fehler im Daemon-Log. Warnungen über veraltete Konfigurationen sind erwähnenswert, erfordern aber keine sofortige Maßnahme.
Netzwerkkonnektivität zwischen Containern bestätigen.
docker network ls
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}'
Der zweite Befehl zeigt nur das Standardnetzwerk bridge. Wiederholen Sie ihn für jedes von docker network ls aufgeführte benutzerdefinierte Netzwerk, um zu bestätigen, dass den benutzerdefinierten Bridge-Netzwerken nach dem Daemon-Neustart weiterhin die erwarteten Container zugeordnet sind, und testen Sie mindestens eine tatsächliche Verbindung zwischen voneinander abhängigen Diensten.
Was wir beobachteten
Das Upgrade war in weniger als zwei Minuten abgeschlossen. Alle zuvor laufenden Container liefen auch danach. Bei Compose fiel uns zuerst das übersichtlichere Ausgabeformat von --dry-run auf: Die neue Version erzeugt strukturierte, farbige Ausgaben, mit denen sich vor der Ausführung leichter prüfen lässt, was docker compose up tatsächlich tun wird.
Die Healthcheck-Reihenfolge von depends_on verhielt sich auf unseren Stacks genauso wie unter Compose 2.35. Das war zu erwarten, weil wir die Compose-Dateien vorher validiert hatten. Die Bestätigung war dennoch beruhigend.
Eine wesentliche Erkenntnis: Compose erlaubt das Feld container_name nicht in Kombination mit scale, da Containernamen eindeutig sein müssen. Ein Stack verwendete bei uns noch beides – ein Überbleibsel aus der Zeit vor dem Umstieg auf reguläre Replikate. Bei docker compose up gab Compose für diesen Stack eine eindeutige Fehlermeldung aus. Die Lösung bestand darin, das überflüssige Feld container_name mit einer einzeiligen Änderung zu entfernen.
Weiterführende Beiträge
- React-Anwendungen im Produktivbetrieb bereitstellen: Vollständige Docker-Einrichtung mit Traefik Reverse Proxy
- Einen mandantenfähigen Entwicklungsstack mit Docker aufbauen
- Traefik Reverse Proxy: Der vollständige Self-Hosting-Leitfaden für HTTPS und SSL-Automatisierung
- n8n auf Hetzner Cloud selbst betreiben: Anleitung zur vollständigen Docker-Einrichtung