Universeller Container Updater

Veröffentlicht: Lesedauer: 5 Min.

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?

  1. Voraussetzungen: Das Skript sucht nach docker, nach docker compose (v2) oder docker-compose (v1). Fehlt ein Programm, gibt es eine Meldung.
  2. Compose-Datei: Entweder mit -f explizit 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üft docker compose config, ob die Datei überhaupt gültig ist.
  3. Backup: Compose-Datei, .env und der aktuelle Image-Stand jedes laufenden Containers werden gesichert. Mehr dazu weiter unten.
  4. Pull: docker compose pull holt neue Images. Schlägt pull fehl, beendet sich das Skript noch bevor ein Container angefasst wurde. Halb aktualisierte Projekte gibt es also nicht.
  5. 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-orphans ist nur dabei, wenn es mit -o angestoßen wird.
  6. Healthcheck: Das Skript wartet, bis die Container laufen. Falls ein healthcheck definiert ist wird healthy zurückgemeldet.
  7. Ergebnis: Pro Service steht am Ende aktualisiert oder unverändert. Geht etwas schief, kommt ein Hinweis zum Rollback. Mit -a erfolgt 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:

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

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?).

Letzte Anpassung: ↑ zum Anfang