Multi-Arch Docker-Images für Docker Hub erstellen

Schon seit langer Zeit unterstützt Docker verschiedene Prozessor-Architekturen, von welchen x86_64 und ARMv6/7 die prominentesten sein dürften. Doch darüber hinaus sind auch aarch64/arm64 (ARMv8+), s390x (IBM z Systems) und ppc64le (IBM POWER) möglich.

Images lassen sich so parallel für mehrere Architekturen erstellen und anschließend auf Docker Hub hochladen - vorausgesetzt man beachtet einige Zwischenschritte.

Dockerfile

Eine Möglichkeit zur Erstellung von Images ist das Dockerfile - eine einfache Textdatei, die auf Basis eines bereits vorhandenen Images ein neues Image erstellt. Das ist in der Regel die gängige Vorgehensweise - das manuelle Erstellen eines "nackten" Images, welches Betriebssystem und Applikation enthält, bedeutend aufwändiger ist.

Die Grundlage vieler aktueller Images ist die minimalistische, auf Sicherheit optimierte, Distribution Alpine Linux.

Hier als Beispiel eine Python-Webanwendung:

 1FROM alpine:3.11
 2MAINTAINER pinkepank@foo.bar
 3
 4# add base and community repositories
 5ADD repositories /etc/apk/repositories
 6RUN apk add --update python3 py3-pip@community
 7
 8# install dependencies
 9RUN pip3 install flask
10
11# create application directory
12RUN mkdir -p /opt/joke_api/joke_api
13ADD joke_api /opt/joke_api/joke_api
14ADD entrypoint.sh /opt/joke_api/entrypoint.sh
15
16# volume configuration
17VOLUME ["/opt/joke_api/instance"]
18
19# start application
20CMD "/opt/joke_api/entrypoint.sh"
21
22# listen on port 5000
23EXPOSE 5000

Die erste Zeile (FROM) gibt an, auf welchem Image das neue Image basiert, mit MAINTAINER wird ein Entwickler benannt. Mit ADD angeführte Zeilen fügen lokale Dateien zum Image hinzu, beispielsweise Applikations- oder Konfigurationsdateien. Mit RUN beginnende Zeilen beinhalten im Container auszuführende Befehle während der Image-Erstellung. Mit VOLUME versehene Verzeichnisse können außerhalb des Containers manipuliert werden - in der Regel wird dies für Applikationsdaten oder Datenbanken genutzt. Wird im späteren Container eine Applikation ausgeführt, die auch außerhalb erreichbar sein soll, so muss der entsprechende Port mit EXPOSE deklariert werden.

Das Erstellen des Images erfolgt nun mit dem docker build-Kommando. Der Tag-Name wurde hier auf joke_api festgelegt:

 1$ docker build -t joke_api .
 2Step 1/11 : FROM alpine:3.11
 3---> a187dde48cd2
 4Step 2/11 : MAINTAINER pinkepank@foo.bar
 5---> fede7d8c01e5
 6Step 3/11 : ADD repositories /etc/apk/repositories
 7...
 8Step 4/11 : RUN apk add --update python3 py3-pip@community
 9...
10Step 5/11 : RUN pip3 install flask
11Step 6/11 : RUN mkdir -p /opt/joke_api/joke_api
12Step 7/11 : ADD joke_api /opt/joke_api/joke_api
13Step 8/11 : ADD entrypoint.sh /opt/joke_api/entrypoint.sh
14Step 9/11 : VOLUME ["/opt/joke_api/instance"]
15Step 10/11 : CMD "/opt/joke_api/entrypoint.sh"
16Step 11/11 : EXPOSE 5000
17Successfully built a7177d5042c1

Bevor das Image entsprechend markiert und auf Docker Hub hochgeladen wird, muss - falls noch nicht geschehen - ein entsprechender Account erstellt und auf der Kommandozeile für den Login genutzt werden:

1$ docker login -u <username>

Als Vorbereitung muss nun im Docker Hub ein neues Repository für das Image erstellt werden. Docker unterscheidet hier zwischen öffentlichen und privaten Repositories. Während öffentliche Repositories unbegrenzt erstellt werden können, räumt Docker jedem Benutzer nur ein privates Repository ein.

Anschließend lässt sich das soeben erstellte Image mit einem Tag versehen und hochladen. Grundsätzlich sind hierfür die folgenden Kommandos notwendig:

1$ docker tag local-image:tag-name remote-repo:tag-name
2$ docker push remote-repo:tag-name

Zu den Tags lässt sich sagen, dass sich die folgenden Regeln etabliert haben:

  • latest - letzte Version, auch nicht stabile Releases möglich
  • edge, devel - Entwicklungsversionen
  • stable - für den Produktionseinsatz freigegebene Version
  • 3.11, 3.1, 3 - Haupt- und Nebenversionen
  • alpine, bionic - (abweichende) Basis-Images
  • amd64, armv6 - (abweichende) Prozessor-Architekturen

Natürlich lassen sich diese Fragmente auch kombinieren - so wären z. B. devel-bionic und alpine-latest denkbare Tags. Auch wenn es nicht ratsam ist, mit dem latest-Tag versehene Images in der Produktion einzusetzen, ist es ratsam, die letzte Version des eigenes Images mit diesem Tag zu versehen. Nur so ist sichergestellt, dass Benutzer die neueste Version erhalten, wenn sie das folgende Kommando eingeben:

1$ docker pull username/repository

In einem Projekt mit aktiver Entwicklung, macht es durchaus Sinn für Haupt- und Nebenversionen entsprechende Tags zu verwenden und produktionstaugliche Versionen entsprechend zu markieren. Images lassen sich auch mit mehreren Tags versehen. So kann ein Image beispielsweise sowohl latest als auch stable sein.

Zurück zum Beispiel - in meinem Fall waren die folgenden Kommandos nötig, um den lokalen Container hochzuladen:

1$ docker tag joke_api stdevel/joke-api:latest
2$ docker push stdevel/joke-api:latest

Nach erfolgtem Upload ist der Container sofort online und kann auf der Docker Hub-Webseite eingesehen werden.

Docker Hub Automated Builds

Damit man nicht nach jeder Code-Änderung manuell das Image aktualisieren und hochladen muss, bietet Docker mit Automated Builds eine eigene kleine CI-/CD-Pipeline. Diese kann nach erkannter Code-Änderung einen entsprechenden Build auslösen, sofern der Quellcode auf GitHub oder BitBucket gehostet wird. Hierfür ist eine Verknüpfung der entsprechenden Accounts notwendig - die dazugehörigen Einstellungen finden sich im Profil-Bereich auf Docker Hub.

In den Repository-Einstellungen findet sich unter Builds > Configure Automated Builds ein Dialog, in welchem unter anderem Build-Regeln festgelegt werden können. So können beispielsweise einzelnen Branches ausgewählt und Docker-Tags definiert werden - standardmäßig werden hier die master-Branch und der latest-Tag definiert:

Hub Automated Builds

Nach einem Klick auf Save and Build werden die Einstellungen gespeichert und der erste Build ausgelöst.

Nach einigen Minuten fällt auf, dass das vorher hochgeladene Image überschrieben wurde - auch wenn es eine andere Architektur hatte.

Manifest

Hier kommt das Manifest ins Spiel - es ist dafür zuständig, mehrere Images pro Architektur für einen Tag zu hinterlegen. Der Vorteil hierbei ist, dass beispielsweise sowohl x86_64- als auch ARM-Benutzer den gleichen docker pull-Befehl absetzen könnten, ohne sich vor dem Download über das konkrete Image informieren zu müssen.

Hierfür ist eine Docker-Installation mit entsprechenden Kommandozeilen-Werkzeuge notwendig - mir ist schleierhaft, warum diese Funktion nicht über die Web-Oberfläche zur Verfügung stehen. Die Konfigurationsdatei des Werkzeugs findet sich unter ~/.docker/config.json und muss noch um die folgende Einstellung erweitert werden:

1{
2...
3  "experimental": "enabled"
4}

Anschließend wird für einen spezifischen Tag ein neues Manifest erstellt und veröffentlicht:

1$ docker manifest create stdevel/joke-api:latest stdevel/joke-api:amd64 stdevel/joke-api:arm
2$ docker manifest push --purge stdevel/joke-api:latest

Wenn nun wieder entsprechende Builds und Uploads ausgelöst werden, werden keine Images mehr überschrieben:

Docker-Image mit mehreren Architekturen

Ausblick

Ich nutze Docker Hub Automated Builds für automatisierte x86_64-Builds mit einem dedizierten Tag (amd64) - andere Plattformen werden (derzeit) nicht unterstützt. Sollte ich hier etwas übersehen haben, freue ich mich über einen Tipp. 🙂

Darüber hinausgehende Builds für die ARM-Architektur erstelle ich lokal und lade diese per Skript teilautomatisiert hoch. Eine elegantere Lösung wäre es, beide Images über eine lokale CI-/CD-Pipeline erstellen zu lassen - beispielsweise mit GitLab.

Übersetzungen: