Universeller Container Updater
Inhalt
Wer ein paar Dutzend Container betreut, kennt das: Ordner wechseln, pull, up -d, Daumen drücken. Beim zehnten Projekt am Stück ist die Begeisterung weg. Und wenn dann ein Update schiefgeht, sucht man auch noch das alte Image zusammen. Deshalb habe ich mir ein Skript gebaut, das diese Arbeit übernimmt. Es funktioniert für alles, was per docker-compose.yml läuft.
Bei mir liegt das Skript zentral auf dem Server. In jedem Projektordner steht daneben, direkt bei der docker-compose.yml, nur ein Softlink darauf. Das Skript kümmert sich immer um das Projekt, in dessen Ordner ich es starte.
Download: docker-update.sh
Was passiert beim Aufruf?
- Voraussetzungen: Das Skript sucht nach
docker, nachdocker compose(v2) oderdocker-compose(v1). Fehlt ein Programm, gibt es eine Meldung. - Compose-Datei: Entweder mit
-fexplizit angegeben, oder die erste, die im Arbeitsverzeichnis gefunden wird. Die Suchreihenfolge:docker-compose.yaml,docker-compose.yml,compose.yaml,compose.yml. Bevor irgendetwas passiert, prüftdocker compose config, ob die Datei überhaupt gültig ist. - Backup: Compose-Datei,
.envund der aktuelle Image-Stand jedes laufenden Containers werden gesichert. Mehr dazu weiter unten. - Pull:
docker compose pullholt neue Images. Schlägtpullfehl, beendet sich das Skript noch bevor ein Container angefasst wurde. Halb aktualisierte Projekte gibt es also nicht. - Neustart:
docker compose up -d. Compose baut nur die Container neu, bei denen sich Image oder Konfiguration geändert haben. Der Rest läuft einfach weiter.--remove-orphansist nur dabei, wenn es mit-oangestoßen wird. - Healthcheck: Das Skript wartet, bis die Container laufen. Falls ein
healthcheckdefiniert ist wirdhealthyzurückgemeldet. - Ergebnis: Pro Service steht am Ende
aktualisiertoderunverändert. Geht etwas schief, kommt ein Hinweis zum Rollback. Mit-aerfolgt der Rollback automatisch.
Installation
Das Skript wird an einen zentralen Ort gespeichert und ausführbar gemacht:
# z.B.
sudo mkdir -p /opt/scripts
sudo cp docker-update.sh /opt/scripts/
sudo chmod +x /opt/scripts/docker-update.sh
Danach wird in jedem Projektordner einen Softlink erstellt:
ln -s /opt/scripts/docker-update.sh /opt/myapp/docker-update.sh
Das Skript wird immer aus dem Projektordner heraus gestartet:
cd /opt/myapp
./docker-update.sh
Parameter
Skript-Parameter
| Option | Bedeutung |
|---|---|
-f FILE |
Pfad zur Compose-Datei |
-d DIR |
Arbeitsverzeichnis (Standard: aktuelles) |
-n |
Dry-run, zeigt nur an, was passieren würde |
-o |
--remove-orphans beim Neustart verwenden (Standard: aus) |
-a |
Bei fehlgeschlagenem Healthcheck automatisch Rollback durchführen |
-w SEK |
Maximale Wartezeit für den Healthcheck (Standard: 60) |
-k ANZAHL |
Anzahl aufzubewahrender Backups (Standard: 5) |
-r |
Rollback auf das letzte Backup (statt Update) |
-t STEMPEL |
Rollback auf ein bestimmtes Backup, Format JJJJMMTT-HHMMSS (nur mit -r) |
-l |
Vorhandene Backups auflisten |
-h |
Hilfe |
SERVICE ... |
Optional: Namen von Compose-Services statt aller Services des Projekts |
Am Ende der Zeile stehen die Service-Namen aus der entsprechenden compose-Datei und keine Image-Namen. Soll web und db aktualisiert werden, schreibt man also web db und nicht nginx postgres:16.
Beispiele
./docker-update.sh # alle Services des Projekts, mit Backup + Healthcheck
./docker-update.sh -n # Trockenlauf: zeigt nur, was passieren würde
./docker-update.sh web db # nur die Services "web" und "db"
./docker-update.sh -a # mit automatischem Rollback bei Fehler
./docker-update.sh -l # Backups auflisten
./docker-update.sh -r # Rollback auf den Stand vor dem letzten Update
./docker-update.sh -r -t 20261025-110052 # Rollback auf ein bestimmtes Backup
Bei einem neuen Projekt wird das Skript immer zuerst mit -n gestartet. Dies zeigt sofort, ob das Skript die richtige Compose-Datei und die richtigen Services erwischt.
Backup und Rollback
Vor jedem Update legt das Skript im Projektordner ein Verzeichnis .update-backups/JJJJMMTT-HHMMSS/ an. Darin landen:
- die Compose-Datei und die
.envim damaligen Stand, - die aufgelöste Konfiguration (
compose.resolved.yml), - eine Liste
state.tsvmit Service, Image-Referenz und Image-ID jedes laufenden Containers.
Das alte Image bekommt außerdem einen Tag nach dem Muster <repository>:rollback-<zeitstempel>. Das ist der eigentliche Trick: Ohne Tag wäre das alte Image nach dem Pull ein namenloses <none> und beim nächsten docker image prune weg. Mit Tag bleibt es da.
Bei einem Rollback (-r) wird die ursprüngliche Image-Referenz wieder auf das gesicherte Image gelegt. Die betroffenen Services starten dann mit up -d --no-deps --pull never neu, es wird also nichts nachgeladen. Danach läuft der Healthcheck noch einmal.
Hat sich bei einem Lauf kein Image geändert, wirft das Skript das Backup gleich wieder weg. Ältere Backups über das Limit -k hinaus räumt es samt Rollback-Tags selbst auf.
Was das Skript nicht kann
- Volumes und Datenbankinhalte sichert es nicht. Ein Rollback bringt das Image zurück, aber kein Schema-Upgrade rückgängig. Bei Postgres, MariaDB und Verwandten vor jedem größeren Update einen Dump ziehen.
- Images, die per Digest fixiert sind (
image: foo@sha256:...), ändern sich ohnehin nie. Der Rollback überspringt sie. - Das alte Image muss noch lokal liegen. Wer
docker image prune -avon Hand über die Rollback-Tags jagt, hat sich die Rückfahrkarte selbst zerrissen. - Die Backups enthalten die
.envund damit womöglich Zugangsdaten..update-backupsgehört nicht ins Git, am besten in die.gitignore, und mitchmod 700abgesichert. - Es geht nur mit Compose. Container, die per
docker rungestartet wurden, kennt das Skript nicht. Eine frühere Version hatte dafür einen Zweig mitdocker restart, aber der startete den Container mit dem alten Image neu und hat damit nie etwas aktualisiert. Er ist deshalb raus.
Automatisch per Cron
Wer die Updates nachts laufen lassen will, nimmt am besten -a dazu. Dann liegt morgens nicht ein kaputter Dienst herum, sondern der alte, funktionierende:
0 3 * * 0 cd /opt/myapp && ./docker-update.sh -a >> /var/log/docker-update-myapp.log 2>&1
Zwei gleichzeitige Läufe im selben Projekt verhindert das Skript über eine Sperre (flock). Die Farbcodes landen als Steuerzeichen im Log, mit less -R ist es trotzdem gut lesbar.
Hinweise
Ist der Container gestoppt, dann meldet sich das Skript ebenfalls mit einer Meldung
[WARN] Keine laufenden Container im Projekt – Rollback-Stand ist leer (Erstinstallation?).