TECHZ.
Infrastruktur 5 Min. Lesezeit

Docker Compose Updates sicher einspielen: Images, Rollback und Wartungsfenster 2026

Praxisleitfaden zu "Docker Compose Updates sicher einspielen: Images, Rollback und Wartungsfenster 2026": klare Umsetzungsschritte, typische Fehler und konkrete Empfehlungen für kleine Teams.

Ausgangslage: 14 Stacks, ein `pull`, ein Ausfall

Ein Kunde betreibt auf einem einzelnen Root-Server 14 Docker-Compose-Stacks - Reverse Proxy, drei interne Webanwendungen, eine PostgreSQL-Instanz, diverse Hilfsdienste. Update-Routine war ein Cronjob um 4 Uhr morgens: docker compose pull && docker compose up -d für jeden Stack, nacheinander, per Shell-Loop. Funktionierte seit zwei Jahren.

Dann zog ein Upstream-Image von nginx 1.25 auf 1.27, weil der Tag latest im Compose-File stand. Die neue Version änderte das Default-Verhalten für proxy_buffering minimal - genug, dass eine der Webanwendungen ab 4:03 Uhr HTTP 502 zurückgab. Bemerkt wurde es um 8:40 Uhr, als der erste Mitarbeiter sich nicht einloggen konnte. Vier Stunden und 37 Minuten Teilausfall, weil niemand wusste, welches Image vorher lief und wie man dorthin zurückkommt.

Das eigentliche Problem: `latest` ist keine Version

`docker compose pull` mit einem unversionierten Tag zieht bei jedem Lauf ein potenziell anderes Image. Das ist keine Aktualisierung, das ist ein Zufallsgenerator mit Firmenlogo. Der Fehler lag nicht im Cronjob, sondern in der Annahme, dass "Update" und "gleiche Version, neuer Build" das Gleiche sind. Das sind sie bei `latest` nicht.

Für den Leser mit eigenem Server: Wenn irgendwo in den Compose-Files noch image: nginx:latest, image: postgres:15 ohne Patch-Version oder image: myapp ohne Tag steht, läuft man in genau dieses Muster. Der erste Schritt ist nicht Automatisierung, sondern Disziplin bei der Versionsangabe.

Images pinnen: Tag reicht nicht, Digest schon

Ein Tag wie nginx:1.27.1 ist besser als latest, aber nicht unveränderlich. Registries lassen zu, dass ein Tag neu gepusht wird und auf ein anderes Image zeigt - selten böswillig, aber es passiert bei Rebuilds mit gleichem Versions-Tag. Der einzige wirklich stabile Anker ist der Digest.

services:
  proxy:
    image: nginx:1.27.1@sha256:2ab30d6ac53580a6db4a123c...
    restart: unless-stopped
  db:
    image: postgres:16.4@sha256:8f4d1d0e6c9b2a7f3c5e1d9a...
    restart: unless-stopped

Praktisch heißt das: Digest per docker inspect --format='{{index .RepoDigests 0}}' nginx:1.27.1 ermitteln, ins Compose-File eintragen, committen. Ein Update ist danach ein bewusster Commit, kein stiller Seiteneffekt eines Cronjobs. Der Nachteil: Man bekommt keine Sicherheitspatches mehr automatisch. Das ist Absicht - ungeplante Patches waren ja das Problem.

Wartungsfenster statt Cronjob um 4 Uhr

Seit dem Vorfall läuft für diesen Kunden kein automatischer `pull` mehr. Updates passieren einmal wöchentlich, dienstags zwischen 7:00 und 7:30 Uhr, außerhalb der Kernarbeitszeit (8:00-17:00), aber mit jemandem erreichbar, falls etwas kippt. Der Cronjob prüft nur noch, welche Digests veraltet sind, und schickt eine Mail - er aktualisiert nichts.

#!/usr/bin/env bash
# check-updates.sh - meldet nur, aktualisiert nicht
set -euo pipefail

for svc in proxy db app1 app2; do
  current=$(docker compose images -q "$svc")
  latest=$(docker manifest inspect "$(docker compose config --images "$svc")" \
    | jq -r '.config.digest' 2>/dev/null || echo "unbekannt")
  if [ "$current" != "$latest" ]; then
    echo "Update verfügbar: $svc ($current -> $latest)"
  fi
done

Das Skript ist bewusst simpel gehalten und ersetzt kein Monitoring - es ist ein Hinweisgeber, kein Entscheider. Die Entscheidung, ob und wann ein Update läuft, bleibt bei mir.

Rollback: das alte Image bleibt liegen

Docker löscht Images nicht automatisch beim Update, solange man nicht `--rmi` oder `docker system prune` verwendet. Das nutze ich als Rollback-Mechanismus: Vor jedem Update wird der aktuell laufende Digest in eine Datei geschrieben, danach kann ich mit wenigen Befehlen zurück.

docker compose images -q proxy > /var/backups/compose/proxy.digest.$(date +%F)

# Rollback:
old=$(cat /var/backups/compose/proxy.digest.2026-08-11)
docker compose stop proxy
sed -i "s|image: nginx.*|image: ${old}|" docker-compose.yml
docker compose up -d proxy

Wichtig ist, dass das Compose-File selbst in Git liegt. Der Rollback ist damit in Wahrheit ein `git revert` auf den letzten funktionierenden Commit plus `docker compose up -d`. Ohne Versionierung des Compose-Files bleibt Rollback eine manuelle Bastelei mit `sed` und Zetteln - genau das, was ich mit dem Befehl oben zwar zeige, aber nicht als Dauerlösung empfehle.

Healthchecks statt "sieht doch gestartet aus"

`docker compose up -d` meldet Erfolg, sobald der Container läuft - nicht, sobald der Dienst funktioniert. Nach dem nginx-Vorfall bekamen alle Kern-Services einen Healthcheck im Compose-File, und das Update-Skript wartet aktiv darauf, bevor es zum nächsten Stack geht.

services:
  app1:
    image: registry.example.internal/app1:2026.08.1@sha256:9c2f...
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 20s

In der Praxis bedeutet das: Der Loop im Update-Skript prüft nach jedem `up -d` mit docker compose ps --format json, ob der Status `healthy` ist, mit maximal 60 Sekunden Wartezeit. Bleibt er `unhealthy`, stoppt der Loop und wechselt nicht zum nächsten Stack. Seit Einführung im Februar 2026 ist kein Update mehr unbemerkt in einen kaputten Zustand gelaufen - bei bisher 26 Update-Läufen allerdings noch keine große Stichprobe.

Der Sonderfall: Datenbank-Images und Migrationen

Postgres- oder MySQL-Updates lassen sich nicht wie zustandslose Webanwendungen zurückrollen. Ein Major-Update wie Postgres 15 auf 16 schreibt das Datenverzeichnis um; danach startet die alte Version nicht mehr gegen die neuen Daten. Für diesen Fall gibt es keinen sauberen Digest-Rollback, nur ein Backup vor dem Update und einen Restore danach, wenn etwas schiefgeht. Ich fahre Datenbank-Major-Updates deshalb separat und nie im selben Fenster wie Anwendungs-Updates - sonst lässt sich später nicht mehr sagen, welche der beiden Änderungen die Ursache war.

Was nicht funktioniert hat

Ein Ansatz, den ich wieder verworfen habe: Blue-Green-Betrieb für jeden einzelnen Compose-Stack mit zwei parallelen Projektnamen und Umschaltung über den Reverse Proxy. Auf dem Papier sauber, in der Praxis für 14 Stacks auf einem einzelnen Server zu viel Overhead - doppelter RAM-Bedarf während der Umschaltung, und bei der Postgres-Instanz funktioniert das Prinzip ohnehin nicht wegen des gemeinsamen Datenverzeichnisses. Für einen einzelnen Server mit überschaubarer Anzahl Stacks ist Digest-Pinning plus Wartungsfenster der pragmatischere Kompromiss - auf Kosten von etwas mehr manueller Vorbereitung bei jedem Update.

Genauso wenig hat automatisches Rollback bei fehlgeschlagenem Healthcheck funktioniert: Ein Skript, das bei `unhealthy` selbstständig auf das alte Image zurückschaltet, hat einmal einen Fehlalarm - ein zu kurzer `start_period` - als echten Fehler interpretiert und mitten am Vormittag zurückgerollt, während gerade niemand hinsah. Seitdem stoppt das Skript nur noch und wartet auf eine manuelle Entscheidung.

Nächste Schritte

Wer das für den eigenen Server umsetzen will, fängt am besten dort an, wo der Schaden am größten wäre: Compose-Files aus `/etc` oder `/opt` in ein Git-Repository holen, jeden `latest`-Tag durch eine feste Version mit Digest ersetzen, und für die zwei oder drei kritischsten Services einen Healthcheck ergänzen. Der Cronjob um 4 Uhr morgens darf danach ruhig weiterlaufen - er sollte nur nicht mehr die Entscheidung treffen, welches Image gerade produktiv ist.

Wissen, was fehlt — die Infrastruktur-Analyse

Ein Tag vor Ort, ab 1.999 €: Ich nehme Ihre Server auf und dokumentiere die Nachbau-Lücke. Am Ende steht ein Risikobericht mit Roadmap — mehr zur Infrastruktur-Analyse .