Nach vielen vielen gebauten und gestarteten Containern möchte ich ein paar praktische Tipps zum Image-Bau geben, die einem das Leben erleichtern. Entstehen soll ein ganzheitlicher Blick von der lokalen Entwicklungsumgebung, über das Bauen und Testen in der Continuous Delivery Pipeline, bis zum Betrieb in Produktion.

Beware of the Layers

Eine der ersten Regeln, die man sich beim Bau von Images vor Augen führen sollte, ist das Kombinieren von Befehlen, um keine unnötigen Layer zu erzeugen. Dateien, die in einem Layer erzeugt wurden, können in einem anderen Layer nicht mehr entfernt werden, um Speicherplatz zu reduzieren.

# Do
RUN apt-get update && \
    apt-get install -y \
    package-bar \
    && \
    rm -rf /var/lib/apt/lists/*

# Don't
RUN apt-get update
RUN apt-get install package-bar
RUN rm -rf /var/lib/apt/lists/*

Zudem spielen Layer eine wichtige Rolle, wenn es um die Build-Geschwindigkeit geht, da hier der Build Cache für Beschleunigung sorgt.

Ein Dockerfile dem Befehl „docker container commit“ vorziehen

Man muss kein Dockerfile als Basis für seine Applikation nutzen. Stattdessen kann man auch einfach einen Basis-Container mit der Linux-Distribution meiner Wahl hochfahren, um „from scratch“ zu starten. Anschließend kann man manuell seine Applikation und Abhängigkeiten über die Shell hinzufügen. Mit docker container commit lässt sich dann der Container wieder als Image verpacken. Für ein erstes Rumprobieren halte ich diese Vorgehensweise für nützlich und zielführend.

Im Gegensatz dazu, steht die Möglichkeit ein Image basierend auf einem Dockerfile zu bauen. In dieser textlichen Form ist es versionierbar, ist dadurch auch für die Kollegen nachvollziehbar und sorgt für eine bessere Automation und Reproduzierbarkeit, die im Umfeld von CI/CD sehr wichtig ist. Wer seit Längerem mit Continuous Delivery Pipelines zu tun hat, weiß aber, dass externe Abhängigkeiten die Reproduzierbarkeit torpedieren können. Wenn der Server, von dem ich eine Abhängigkeit wie ein Tar-Ball oder dergleichen beziehe, offline ist, habe ich trotzdem ein Problem. Das sollte man im Hinterkopf behalten.

It runs on my laptop != It runs in production ⚠️

Wir alle kennen das: Das gerade frisch entwickelte neue Feature funktioniert tadellos auf der eigenen Maschine, aber nicht in Produktion. Die Docker-Container sollen dies aufgrund ihrer Parität (Parity) jedoch minimieren. Ich schreibe bewusst minimieren, denn wir verfolgen mit den verschiedenen Umgebungen verschiedene Interessen. Lokal wollen wir schnell und komfortabel entwickeln. In der Test-Umgebung soll möglichst automatisiert eine Überprüfung der Qualität statttfinden. Auf dem Produktivsystem wünschen wir uns, dass die Applikation möglichst 24/7 erreichbar ist. Das fasst die Ziele natürlich nur ganz grob zusammen, aber ein grundsätzlicher Unterschied, der dadurch entsteht, ist die Verwendung von Volumes in der Produktivumgebung, die nicht die Applikation, sondern nur die Daten persistieren sollen. Lokal arbeitet man in der Regel so, dass die komplette Applikation aus dem Git-Repository ausgecheckt wird und auf der Host-Maschine liegt. Host-Mounts, die unsere Änderung in die gestarteten Container bringen, werden dadurch notwendig. Bereits das ist ein Unterschied, der nicht zu vernachlässigen ist und es geht weiter mit dem Orchestrator. Lokal arbeitet man unter Umständen mit Docker-Compose und das verteilte Live-System wird mit Docker Swarm gesteurt. Eine Anwendungen zentral auf einer Entwicklungsmaschine zu betreiben unterscheidet sich vom Betrieb in verteilten Systemen.

In meinen Augen sollte man daher dafür sorgen, dass zumindest die Images in allen Umgebungen identisch sind. Da ein Dockerfile die Basis für ein Image darstellt, ist es auch der Ausgangspunkt, um eine möglichst große Parität zu erhalten. Ein Image muss daher über die notwendige Intelligenz verfügen, lokal in Debugging-Szenarien zur Seite zu stehen, was in Production mittels Environment Variable oder reingerichter Konfigurationsdatei deaktiviert wird. Das Image stellt also den gemeinsamen Nenner dar und kann mittels Host-Mounts, Ports, Env-Variablen, Secrets und Configs an die jeweilige Umgebung angepasst werden. Das Thema Sicherheit steckt hier natürlich die Grenzen ab. Am Ende wollen wir nicht den lokalen Komfort in der Entwicklung über die Sicherheit im Produktivsystem stellen.

DevOps 👯‍♂️

Ich bin ein Freund des Ansatzes „You build it, you run it!“, der im Zusammenhang mit dem DevOps-Ansatz immer wieder Erwähnung findet. Als Entwickler*in bekommt man mit Docker ein Werkzeug an die Hand, wodurch man sich zwangsläufig mehr mit der Laufzeitumgebung Gedanken machen muss, die man zuvor in Ops Händen gesehen hat. Dadurch stellt sich ein breiteres Wissen selbst bei den Technologien ein, die man bereits jahrelang genutzt hat. Als PHP-Entwickler*in lernt man, wie der FastCGI Process Manager (FPM) arbeitet, wo er seine Log-Dateien und sein Process File ablegt. In meinen Augen bekommt man einen ganzheitlicheren Blick, den man für das Betreiben in der Live-Umgebung auch wirklich benötigt. Menschen, die man klassischer Weise Operations zuordnen würde, fungieren mehr als Makro-Architekten und stecken die Grenzen für Entwickler*innen ab, welche mit Containern sehr viel mehr Freiheiten bekommen.

Offizielle Images

Nutzt die offiziellen Images! Sie stellen ein absolut solides Fundament für eure Applikation dar und werden von Leuten betreut, die sich damit auskennen. Dinge selbst von der Pieke auf zu erstellen, sorgt für einen gehörigen Aufwand, den man nicht unterschätzen sollte. Schnell hat man sich ein Image aus dem Docker Hub geladen und zum Laufen gebracht, aber wer garantiert einem, dass dieses Image sicher ist oder in einem halben Jahr noch gepflegt wird? Mit den offiziellen Images haben die Verantwortlichen bereits ihren langen Atem bei der Pflege der Software-Pakete bewiesen.

Alpine Linux

Es lohnt sich mit dem kleinen Alpine Linux und dessen Paket Manager auseinander zu setzen. Meine ersten Docker-Gehversuche habe ich mit Debian gestartet, weil mir apt vertraut war. Nachdem ich mich mit Alpine auseinander gesetzt habe, kann ich euch nur wärmstens dazu raten, denn die kleinen Images machen beim Bewegen und Bauen extrem viel Spaß. Die CI-/CD-Pipeline wird enorm beschleunigt und auch die lokale Entwicklungsumgebung ist in Nullkommanichts aufgesetzt. Spaß beim Entwickeln ist in meinen Augen ein unterschätztes Gut, was es als Verantwortlicher zu jeder Zeit zu wahren gilt. Das einzig Negative, was ich über Alpine Linux berichten kann, ist der verwendete C Compiler, der in seltenen Fällen für Probleme sorgt und das ein oder andere Derivat an CLI-Tools, was sich vielleicht doch etwas vom Original unterscheidet. Ein Blick wert ist es aber auf jeden Fall und wenn es nur der schmale Footprint ist. Ja auch als Entwickler*in kann man seinen Beitrag zum Umweltschutz leisten. 🌳

Dockerfile Best Practices

Docker selbst hat in seiner Dokumentation Best Practices zum Schreiben von Dockerfiles definiert, die jeder mal gelesen haben sollte. Zur Überprüfung des Regelwerks wurden ein paar Linter-Projekte ins Leben gerufen. Eine Empfehlung dafür kann ich bisher jedoch noch nicht aussprechen.

.dockerignore

Die Datei .dockerignore bekommt für mein Empfinden zu wenig Aufmerksamkeit, obwohl sie den Build-Prozess ähnlich stark beeinflussen kann, wie das schlanke Alpine Linux. Dazu muss man sich jedoch vor Augen führen, wie aus dem Dockerfile ein Image gebaut wird. Wichtig bei diesem Vorgang ist der sogenannte Kontext. Das ist ein Ort, aus dem sich Docker beim Bau von Images bedient, um Anweisungen wie COPY auszuführen. Dateien werden also nicht direkt vom lokalen Dateisystem in das Docker-Image kopiert, sondern gehen einen Umweg über den Kontext. Der Kontext entspricht, wenn nicht anders definiert, dem Ordner in dem ich docker image build .ausführe. Alle Dateien und Ordner innerhalb dieses Verzeichnisses werden also vor dem eigentlichen Build-Prozess zum Docker Daemon übertragen, was bei großen Projekten einige Zeit in Anspruch nimmt, nur um anschließend Dinge aus diesem Kontext ins finale Image zu kopieren. Hier kommt die Datei .dockerignore ins Spiel. Damit kann man, ähnliche Gits „.gitignore“, Dateien und Ordner ausnehmen, damit diese nicht zum Docker Daemon übertragen werden. Klassische Einträge dieser Datei umfassen den Ordner .git, die Dokumentation des Projektes, Pipeline as Code, Infrastructure as Code, Secrets, die im finalen Container nichts zu suchen haben. Mit dieser Datei auseinander gesetzt, kann man die CI/CD Pipeline spürbar beschleunigen.

Build and Runtime Dependencies

Mach dir Gedanken darüber, welche Abhängigkeiten es zur Laufzeit und welche es zur Build Time gibt. Ein Beispiel wäre make zum Kompilieren, welches zur Laufzeit nicht benötigt wird und entsprechend in einem Layer installiert, damit kompiliert und anschließend wieder entfernt werden kann. Gleichzeitig sollte man eine Entwickler*in nicht einschränken. Wenn er makebenötigt, muss das Tool natürlich auch im Container zur Verfügung stehen.

Labels

Das absolute Mindestmaß an Metadaten, das ein Image-Bauer zur Verfügung stellen sollte, sind sein Name und die Kontaktdaten. So sieht man schnell, wer diese Datei verzapft hat und kann sich bei Fragen an ihn wenden. Wer sich hier mehr wünscht, kann sich mit dem Label Schema beschäftigen, welches dafür eine Spezifikation liefert (veraltet). Alternativ kann man bei Project Atomic oder der Open Container Initiative in entsprechende Entwürfe einarbeiten.

FROM golang:1.9.2-alpine3.7
LABEL maintainer "Patrick Baber <patrick.baber@ueber.io>"

Multi-Stage-Builds

Diesem Thema habe ich einen eigenen Artikel gewidmet, weil es sich um eine mächtige Möglichkeit handelt, ein Image basierend auf mehreren Base-Images zu erstellen. Assets bauen, mit Node, die man dann in ein nginx-Image kopiert. Solche Dinge werden damit möglich.

Abschließendes Gebrabbel

Es lohnt sich in schlanke und robuste Images zu investieren. Die Lernkurve ist meiner Meinung nach nicht besonders steil, da man sich den verschiedenen Themen peu à peu annehmen kann, ohne gezwungen zu sein, gleich alles zu erledigen. Im Fokus sollte die Verwendbarkeit des Images in den verschiedenen Umgebungen stehen, was zwar ein wenig zusätzliche Logik erfordert, sich aber zur Freude von Dev und Ops lohnen wird.

Bild von Nick Hirche