Container haben das Update-Problem nicht gelöst, sondern verschoben. Statt Paketen aktualisiert man Images und weil ein Image das ganze Betriebssystem des Dienstes enthält, bleibt eine alte Bibliothek darin so lange liegen, bis jemand das Image neu baut oder zieht.
:latest ist keine Version#
image: nextcloud:latest bedeutet nicht „die neueste stabile Fassung“. Es bedeutet: irgendein Stand, den der Herausgeber zuletzt so getaggt hat. Auf zwei Maschinen, die eine Woche auseinander gezogen haben, laufen damit zwei verschiedene Programme, bei identischer Compose-Datei.
Die Abstufung, von unbrauchbar bis reproduzierbar:
image: postgres:latest # unbrauchbar im Betrieb
image: postgres:17 # bewegt sich innerhalb von 17.x
image: postgres:17.4 # fest genug für die meisten Fälle
image: postgres:17.4-alpine@sha256:9b1e... # exakt dieses Image, für immer
Meine Praxis: Nebenversion festnageln (17.4), Digest nur dort, wo es wirklich exakt sein muss. Der Digest ist unbestechlich, macht aber jede Aktualisierung zu einer Änderung an zwei Stellen.
Ein Tag kann neu gesetzt werden, derselbe Tag zeigt morgen auf ein anderes Image. Ein Digest ist der Hash des Manifests und ändert sich nie. Genau deshalb ist er in einer Lieferkette das einzige belastbare Merkmal: docker image inspect postgres:17.4 --format '{{index .RepoDigests 0}}'.
Ein Update-Ablauf mit Rückweg#
Der Punkt an einem Ablauf ist nicht, dass nichts schiefgeht, sondern dass man weiß, wie man zurückkommt.
cd /opt/appstack
# 1. Wissen, wo man herkommt. Diese Zeile ist der Rückweg.
docker compose config --images
docker compose images --format 'table {{.Service}}\t{{.Repository}}\t{{.Tag}}\t{{.ID}}'
# 2. Datenbestand sichern, bevor irgendetwas gezogen wird
docker compose exec -T db pg_dump -U app app | zstd -9 > /var/backups/app-vor-update.sql.zst
# 3. Neue Images holen, ohne sie zu starten
docker compose pull
# 4. Was ändert sich? Changelog des Projekts lesen — besonders auf
# Migrationen, geänderte Umgebungsvariablen und Breaking Changes.
# 5. Umschalten
docker compose up -d
# 6. Hinsehen. Nicht wegklicken.
docker compose ps
docker compose logs -f --tail=100 app
Der Rückweg, falls Schritt 6 unschön aussieht:
# Tag in der compose.yaml auf die alte Version zurücksetzen
docker compose up -d
# Falls die neue Version das Schema migriert hat: Dump zurückspielen
zstd -d < /var/backups/app-vor-update.sql.zst | docker compose exec -T db psql -U app app
Ein Zurücksetzen des Images ist einfach. Ein Zurücksetzen einer Datenbankmigration ist es nicht. Viele Anwendungen migrieren beim Start automatisch und ohne Nachfrage; das alte Programm kann danach mit dem neuen Schema nichts mehr anfangen. Der Dump von Schritt 2 ist deshalb kein Formalismus, sondern die einzige echte Rückfahrkarte.
Was auf jeden Fall in eine Vorabprüfung gehört#
Bevor man ein Major-Update fährt, drei Punkte im Changelog suchen:
- Migrationen, ändert sich das Datenbankschema, und ist der Schritt umkehrbar?
- Entfernte Konfiguration, sind Umgebungsvariablen weggefallen oder umbenannt?
- Übersprungene Versionen, verlangt das Projekt, von 15 über 16 nach 17 zu gehen? Bei Datenbanken ist das der Regelfall, nicht die Ausnahme.
Punkt 3 ist der, der Abende kostet. Postgres springt nicht von 15 direkt auf 17, ohne dass man pg_upgrade oder einen Dump-und-Restore-Weg geht.
Automatische Updates, mit Augenmaß#
Werkzeuge wie Watchtower oder Diun können Updates ziehen bzw. melden. Die Frage ist nicht „automatisch oder nicht“, sondern welche Updates automatisch laufen dürfen.
| Art | Automatisch? | Begründung |
|---|---|---|
Patch-Updates (17.4.1 → 17.4.2) | ja | Fehlerkorrekturen, kein Schemabruch |
Nebenversion (17.4 → 17.5) | mit Healthcheck und Benachrichtigung | meist harmlos, aber nicht immer |
Hauptversion (17 → 18) | nie | Migrationen, geänderte Standards |
| Datenbanken | nie automatisch | siehe oben |
Wer benachrichtigt werden statt automatisch aktualisieren will, fährt mit einem reinen Melder besser:
services:
diun:
image: crazymax/diun:4.29
restart: unless-stopped
environment:
DIUN_WATCH_SCHEDULE: "0 6 * * *"
DIUN_PROVIDERS_DOCKER: "true"
DIUN_NOTIF_TELEGRAM_TOKEN: ${TG_TOKEN}
DIUN_NOTIF_TELEGRAM_CHATIDS: ${TG_CHAT}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- diun-daten:/data
Jeder Container mit Zugriff auf /var/run/docker.sock kann einen privilegierten Container starten und damit den Host übernehmen, :ro ändert daran nichts, weil der Socket eine API ist und keine Datei mit Rechten. Solche Container gehören auf ein Minimum begrenzt, und wenn, dann hinter einen Socket-Proxy, der nur Leseoperationen durchlässt.
Auf bekannte Lücken prüfen#
Ein Image mitzuführen heißt, dessen Betriebssystem mitzupflegen. Ein Scanner zeigt, wie alt der Unterbau tatsächlich ist:
# Trivy — prüft Paketstände im Image gegen bekannte Schwachstellen
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image --severity HIGH,CRITICAL postgres:17.4-alpine
Die Ausgabe ist beim ersten Mal ernüchternd, und das ist normal. Wichtig ist die Einordnung: Eine Lücke in einer Bibliothek, die der Dienst gar nicht aufruft, ist etwas anderes als eine im Webserver, der nach außen hört. Der Scanner liefert die Liste, die Bewertung bleibt Handarbeit.
Zwei Punkte, die den Aufwand dauerhaft senken:
- Schlanke Basis-Images (
-alpine,-slim,distroless) enthalten weniger Pakete und damit weniger Angriffsfläche. - Selbst gebaute Images regelmäßig neu bauen, auch ohne Codeänderung. Ein
docker build --pull --no-cachezieht die aktuellen Systempakete nach.
Wenn ein Update wirklich schiefgeht#
# Was ist passiert? Logs seit dem Start des Containers
docker compose logs --since 15m app
# Läuft der Prozess, antwortet er nur nicht?
docker compose exec app sh -c 'ps aux; wget -qO- localhost:3000/healthz'
# Was hat sich am Image geändert?
docker image history app:1.9.0 --no-trunc | head -20
Und der wichtigste Reflex: nicht weiter nach vorne flüchten. Der Versuch, mit dem nächsthöheren Tag das Problem zu überspringen, führt fast immer weiter weg von einem funktionierenden Zustand. Zurück auf die alte Version, dann in Ruhe schauen.
Ein Rhythmus, der funktioniert#
| Wann | Was |
|---|---|
| täglich, automatisch | Meldung über verfügbare Updates |
| wöchentlich, 15 Minuten | Patch-Updates einspielen, Logs überfliegen |
| monatlich, 1 Stunde | Nebenversionen, Scannerlauf, alte Images aufräumen |
| bei Bedarf, geplant | Hauptversionen, mit Zeitfenster und Dump |
Der wöchentliche Termin ist der, der den Unterschied macht. Wer alle sechs Monate aktualisiert, hat kein Update mehr vor sich, sondern eine Migration.
Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Proxmox- Hosts, einen Backup-Server und Kundendienste in der EU. Fehler in einer Anleitung sind Fehler im eigenen Betrieb. Korrekturen bitte über Discord.