StartseiteRaspberry PiDocker Container automatisch mit Watchtower aktualisieren: Alles was du zu händischen & manuellen Updates von Docker-Containern/Images wissen solltest

Docker Container automatisch mit Watchtower aktualisieren: Alles was du zu händischen & manuellen Updates von Docker-Containern/Images wissen solltest

Veraltete Softwarekomponenten finden sich auch 2021 unter den Top 10 der Einfallstore für Angriffe. Wer Software betreibt, sollte sie regelmäßig auf den aktuellen Stand bringen. Vor allem für Anwendungen die im Internet erreichbar sind, ist das sehr wichtig, damit Sicherheitslücken schnell geschlossen werden. Das gilt grundsätzlich – unabhängig davon, ob man einen Raspberry Pi Zuhause oder einen Server in der Cloud verwendet. Doch gerade bei Docker gibt es keine automatischen Updates. Wie man Docker-Container aktualisiert und welche wichtigen Besonderheiten es dort zu beachten gibt, zeigt dieser Beitrag. Dafür werfen wir einen Blick auf die Grundlagen der Semantischen Versionierung und wie sie uns vor allem für automatische Aktualisierungen helfen kann.

Das Problem: Der Lebenszyklus von Docker

Wenn ihr einen Container zum ersten Mal startet und das Image nicht auf eurem Pi oder Server liegt, lädt Docker das Image herunter. Aber: Docker prüft danach nie wieder auf Aktualisierungen! Dies geschieht nur, wenn ihr händisch einen Pull durchführt. Das im Container laufende Programm wird daher schnell veralten und kann ein Sicherheitsrisiko darstellen.

Die Docker Images

Ein Image setzt sich aus der Registry zusammen, dem Name eines Image und dem Tag. Die Registry ist optional, standardmäßig wird vom Docker-Hub ausgegangen. Auch der Tag muss nicht zwingend angegeben werden. Der Standard lautet hier „latest“, diesen speziellen Tag schauen wir uns später noch ein. Ein einfaches Beispiel ist der Webserver Nginx, hier existiert ein gleichnamiges Image im Hub von Docker.

Was sind „Aliase“?

Alle verfügbaren Tags werden im Reiter „Tags“ aufgelistet. Übersichtlicher ist aber meist die Liste in der Beschreibung, die von vielen Images angeboten wird. Hier sieht man sofort, dass es Aliase gibt – also verschiedene Tags für das gleiche Image, die in einer Zeile dargestellt werden. Im markierten Bereich können wir wahlweise stable, 1.20 oder 1.20.2 nutzen und erhalten das gleiche Image. Name des Image und Tag trennt man per Doppelpunkt, etwa nginx:1.20.2 um Version 1.20.2 von Nginx zu erhalten.

Durch die Aliase kann man Aktualisierungen leichter einspielen: Entscheiden wir uns beispielsweise für den exakten Tag 1.20.2, müssen wir bei einer neueren Version (z.B. 1.20.3) den Tag von Hand ändern. 1.20.2 wird die neue Version nicht erhalten – denn ein großer Vorteil in der Entwicklung mit Docker ist ja die Reproduzierbarkeit: nginx:1.20.2 soll sich überall gleich Verhalten.

Wie kann ich mir die semantische Versionierung zunutze machen?

Vor allem kleinere Aktualisierungen möchte man in der Praxis aber oft gerne automatisch einspielen. Zumindest wenn man die Software nur nutzt, statt aktiv an ihr zu entwickeln. Hier bieten sich Tags an, die nur einen Teil der Version enthalten: 1.20 wird beispielsweise automatisch alle Aktualisierungen der letzten Versionsnummer enthalten. Derzeit 1.20.2, zukünftig aber auch 1.20.3, 1.20.4 und so weiter.

Hier kann man sich die sogenannte semantische Versionierung zunutze machen, vorausgesetzt die Software nutzt diese: Bei 1.20.2 ist 1 die Hauptversion, sie wird bei größeren Änderungen erhöht, die evtl. nicht abwärtskompatibel sind. Die Nebenversionsnummer .20 steht für neue Funktionen, die bereits vorhandene nicht einschränken sollten. Dagegen ist die Revisionsnummer (.2) für korrigierte Fehler oder Sicherheitsaktualisierungen vorgesehen. Teils gibt es auch noch Buildnummern. Das ist eine fortlaufende Zahl, die bei jedem kompilieren erhöht wird und meist mit Minus von der restlichen Version getrennt. Sie ist aber eher für Test- und Entwicklerversionen.

Der Tag 1.20 erhält also alle Fehlerkorrekturen. Weniger restriktiv könnte man auch den Tag 1 nutzen, um zusätzlich neue Funktionen zu erhalten. Dies würde ich aber nur mit Bedacht empfehlen. Zumal man nicht sicher gehen kann, dass jede Versionsangabe den semantischen Regeln folgt.

Lebenszyklus: Wann und Wie wird ein Docker-Container aktualisiert?

Wichtig ist, den Lebenszyklus zu verstehen: Beim ersten Start eines Containers lädt Docker das per Tag angegebene Image einmalig aus dem Internet. Danach findet keine automatische Aktualisierung statt! Wenn ihr etwa den Tag nginx:1.20 nutzt, derzeit ist 1.20.2 die aktuellste Version und morgen erscheint 1.20.3, werdet ihr die neue Version also nie erhalten. Hierfür sind zwei Schritte nötig:

  1. Das jeweilige Image neu pullen, dadurch wird die aktuellste Version aus der Docker Registry geladen – z.B. docker pull nginx:1.20
  2. Den Container neu erstellen. Neu heruntergeladene Images werden auch nicht auf laufende Container angewendet! Neu erstellt werden kann ein Container z.B. mit docker compose up -d.

Wie kann ich meine per Docker bereitgestellten Anwendungen automatisch aktualisieren?

Beide Schritte müssen allerdings händisch ausgeführt werden. Gerade um Sicherheitslücken schnell zu schließen, möchte man zumindest Fehlerkorrekturen möglichst automatisiert einspielen. Man könnte dafür einen entsprechenden Tag verwenden (im Beispiel etwa nginx:1.20), per Skript automatisch nach neuen Images pullen und den Container neu starten. Mit dem Projekt Watchtower gibt es jedoch bereits eine fertige Lösung, die genau das macht. Ein Watchtower-Container überwacht eure laufenden Docker-Container, prüft regelmäßig auf aktualisierte Image und startet in diesem Falle nicht nur den betroffenen Container neu. Er kann auch Abhängigkeiten berücksichtigen, wenn z.B. der zu aktualisierende Nginx Webserver zu einer PHP-FPM Installation gehört. Oder Datenbanken für eine Anwendung im Spiel sind.

Dazu legt ihr in einem frei wählbaren Verzeichnis eine docker-compose.yml an:

services:
  watchtower:
    image: containrrr/watchtower:1.4.0
    container_name: watchtower
    mem_limit: 128MB
    restart: always
    # Prueft alle 4h nach neuen Images
    command: --interval 21600

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - "TZ=Europe/Berlin"

Startet den Container mit docker compose up -d im Hintergrund. In den Logs seht ihr, wie Watchtower konfiguriert ist und wann die nächste Prüfung durchgeführt wird:

watchtower  | time="2022-05-10T21:53:31+02:00" level=info msg="Watchtower 1.4.0"
watchtower  | time="2022-05-10T21:53:31+02:00" level=info msg="Using no notifications"
watchtower  | time="2022-05-10T21:53:31+02:00" level=info msg="Checking all containers (except explicitly disabled with label)"
watchtower  | time="2022-05-10T21:53:31+02:00" level=info msg="Scheduling first run: 2022-05-11 03:53:31 +0200 CEST"
watchtower  | time="2022-05-10T21:53:31+02:00" level=info msg="Note that the first check will be performed in 5 hours, 59 minutes, 59 seconds"

Wird ein neues Image erkannt, protokolliert Watchtower dies und startet die betroffenen Container entsprechend neu:

time="2022-02-25T21:36:27+01:00" level=info msg="Found new mariadb:latest image (539871f8c20e)"

Damit habt ihr für viele Fälle eine Lösung, die sich automatisch um kleinere Aktualisierungen kümmert. Was ihr allerdings regelmäßig per Hand prüfen solltet, sind Updates der Nebenversion oder – je nach Tag – zumindest die Hauptversion.

Sonderfall #1: „latest“ Tag

Wenn man keinen Tag angibt (z.B. nur nginx statt nginx:1.20), setzt Docker standardmäßig „latest“. Dieser Tag steht oft für die aktuellste, stabile Version. Allerdings nur per Konvention – man kann daher nicht davon ausgehen, dass er für alle Images existiert! Generell würde ich den „latest“ Tag meiden: Da er keinerlei Rücksicht auf mögliche Inkompatibilitäten nimmt, kann es zu Problemen kommen. Das gilt ebenfalls für Tags, die zwar anders heißen, aber einen ähnlichen Zweck erfüllen – etwa „stable“ im Falle von Nginx.

Ich würde mindestens eine Hauptversion als Tag angeben, sofern möglich. Besser sind Haupt- und Nebenversion. Wobei dies nur eine Faustformel ist. Man sollte sich die Art der Versionierung des jeweiligen Programms anschauen und sich entsprechend entscheiden. Manche Projekte halten sich nicht an die semantische Versionierung, sodass z.B. auch Nebenversionen möglicherweise inkompatibel mit vorherigen Versionen sind.

In seltenen Fällen wird leider gar keine oder nur eine sehr rudimentäre Auswahl an Tags angeboten. In diesem Falle kann man Watchtower anweisen, nur auf neue Images zu prüfen, ohne automatische Aktualisierungen durchzuführen:

com.centurylinklabs.watchtower.monitor-only=true

Damit man von den Aktualisierungen erfährt, müssen Benachrichtigungen eingerichtet werden. Ansonsten bleibt nur das unbequemere regelmäßige Prüfen bzw. Filtern der Protokolle.

Sonderfall #2: Selbst gebaute Images

Der per Watchtower gezeigte Weg funktioniert nur, wenn der Container direkt mit einem Image aus dem Docker-Hub gestartet wird. Selbst gebaute Images per Dockerfile finden keine Berücksichtigung. Hierfür müsste das Basis-Image gepullt und das selbst erstellte Image neu gebaut werden. Die Vernküpfung zum Dockerfile fehlt Watchtower, weswegen er das selbst gebaute Image fälschlicherweise im Docker-Hub sucht und dort natürlich nicht findet:

time="2022-05-10T03:39:59+02:00" level=warning msg="Reason: registry responded to head request with \"401 Unauthorized\", auth: \"Bearer realm=\\\"https://auth.docker.io/token\\\",service=\\\"registry.docker.io\\\",scope=\\\"repository:library/u-img_u-img_php:pull\\\",error=\\\"insufficient_scope\\\"\"" container=/u-img_php image="u-img_u-img_php:latest"

Hier muss man die regelmäßige Neu-Erstellung des eigenen Images automatisieren. Im technisch besten Falle kommt eine CI/CD Pipeline zum Einsatz, die z.B. als nächtliche Aufgabe (Nightly Build) automatisch das Image neu baut. Einsteiger können sich aber auch mit zwei Zeilen in einem Skript behelfen (jeweils im Verzeichnis ausgeführt, in dem das Dockerfile liegt):

docker pull $(egrep ^FROM Dockerfile | awk '{print $2}')
docker compose up -d

Der erste Befehl ließt Image + Tag aus dem Dockerfile aus und pullt das Image. Falls es hier Neuerungen gibt, erscheint Downloaded newer image for xx in der Ausgabe, ansonsten Image is up to date for xx. Docker Compose ist mittlerweile so intelligent, dass up nur Container neu startet, bei denen sich etwas in der Konfiguration – wie unter anderem das Image – geändert hat.

Zusätzlich würde ich Watchtower über folgendes Label für einen Container der selbst gebaute Images nutzt komplett deaktivieren. Funktionell ist das zwar nicht nötig. Aber es vermeidet die oben gezeigten „falsche Fehler“, wenn Watchtower vergeblich versucht, das Image in der Docker-Registry zu finden:

com.centurylinklabs.watchtower.enable=false

Außerdem hat Docker vor einiger Zeit Limitierungen für Nutzer eingeführt, die kein kostenpflichtiges Abo besitzen. Vor allem mit einer größeren Anzahl an Container sollte es daher auch im eigenen Sinne sein, unnötige Anfragen möglichst zu vermeiden.

Hinweis: Beachtet bei einer automatischen Ausführung per Cron, dass dort i.d.R. kein vollständiger $PATH zur Verfügung steht! Prüft daher mit which docker, wo eure Binary liegt (meist /usr/bin/docker) und gebt den vollständigen Pfad im Skript an, etwa so:

/usr/bin/docker compose up -d

Letzte aktualisierte Container anzeigen (MOTD beim Login)

Je nachdem welche und wie viele Container man nutzt, führt Watchtower in einigen Tagen bis Wochen die ersten Aktualisierungen durch. In den Protokollen des Containers wird dies mit Found new XYZ image vermerkt:

docker logs watchtower --tail all 2>&1 | grep "Found new"

Praktisch finde ich, wenn man beim Einloggen auf dem Server die zuletzt aktualisierten Images aufgelistet bekommt. Das kann mit der Message of the Day umgesetzt werden: Sie führt bestimmte Shellskripte nach der Anmeldung aus und zeigt deren Ausgabe an. Sie liegen unter Ubuntu im Ordner /etc/update-motd.d und werden nach dem Name sortiert ausgeführt. Daher gibt es ein zweistelliges Präfix, um die Reihenfolge unabhängig vom alphabetischen Name definieren zu können. Im Beispiel lege ich 30-docker-container-updates an:

sudo vim /etc/update-motd.d/30-docker-container-updates

Hierbei handelt es sich um ein normales Shell-Skript, in diesem Fall Bash. Es filtert wie oben gezeigt die neu heruntergeladenen Images und entfernt unnötige Inhalte, sodass uns nur Zeitstempel und Image angezeigt werden:

#!/bin/bash
printf "Letzte Container-Updates\n"
docker logs watchtower --tail all 2>&1 | grep "Found new" |
        gawk 'match($0, /time="([^"]+)" level=info msg="Found new (.*) image \((.*)\)/, a) {print "[" a[1] "] " Image a[2] " (" a[3] ")"}' |
        tail -10
printf "\n"

Skript ausführbar machen:

/etc/update-motd.d/30-docker-container-updates

Ergebnis bei der nächsten Anmeldung:

Letzte Container-Updates
[2022-05-18T03:53:37+02:00] nginx:1.21-alpine (b1c3acb28882)
[2022-05-24T01:08:10+02:00] mariadb:10.7 (fb8f6c175bee)
[2022-05-25T00:13:47+02:00] traefik:v2.6 (22c6901de2be)
[2022-06-07T06:18:10+02:00] mariadb:10.7 (7e5b7dee917c)

Fazit

Gerade weil Docker keinerlei automatische Aktualisierungen vorsieht, ist es um so wichtiger, dass man diese Prozesse kennt und entsprechend selbst für Aktualisierungen sorgt. Watchtower kann einem hier viel Arbeit abnehmen, in dem ein Großteil (oder sogar alle, je nach verwendeter Software) aller Images zusammen mit den dazugehörigen Containern automatisiert aktualisiert werden.

Wer vorsichtiger agieren möchte, kann auch auf automatische Aktualisierungen verzichten und sich auf verschiedenen Wegen informieren lassen.

Man darf aber nicht vergessen: Watchtower ist ein Hilfsmittel. Es ersetzt nicht, dass ihr euch mit neuen Nebenversionen (oder zumindest Hauptversionen) sowie deren Änderungen auseinander setzt – und im Falle von nicht abwärtskompatiblen Veränderungen eure Konfiguration entsprechend anpasst. Bei vielen Programmen ist das aber eher selten, sodass sich trotzdem viel Arbeit einsparen lässt. Auf der anderen Seite werden Aktualisierungen schnell eingespielt, was sicherheitstechnisch definitiv sinnvoll ist.

Über DMW007

Schreibe einen Kommentar