Docker Best Practices: Alle Wege führen nach Docker

Dieser Beitrag ist der Auftakt zu einer mehrteiligen Docker-Best-Practices Reihe und eine kurze Einführung in die Nutzung auf der Kommandozeile. Docker ist mittlerweile aus Anwendungslandschaften und der Software-Entwicklung nicht mehr wegzudenken. Selbst wenn man es nicht in der Produktionsumgebung zur Verfügung hat, kann man schon in der Software-Entwicklung oder als persönliches Werkzeug viele Vorteile dieses Tools genießen.

Hier alle Beiträge dieser Serie:

Um die wesentlichen Vorteile von Docker – selbst ohne die produktive Nutzung – zu nennen, hier eine kurze Aufzählung:

  • Docker zum Betrieb von Infrastruktur für Entwicklung und Test: z. B. für lokale Datenbanken, Reverse Proxy Server, Authentisierungsdienste oder Mock-Dienste als Ersatz für die Umsysteme der eigenen Anwendungen
  • Docker als Plattform für die Anwendung: Für Systemtests einfach eine saubere Umgebung herstellen, spezielle Konfigurationen für Lasttests hochfahren oder einfach einmal die neue JDK-Version testen
  • Docker zum Bauen der Software: Es gibt keine klarere und unverfälschtere Build-Umgebung als ein neu erstellter Docker-Container. Keine Zombie-Installationen oder Reste vorangegangener Builds
  • Docker als Werkzeug für die einfache Bereitstellung von Tools, ohne diese installieren oder konfigurieren zu müssen

Docker kann im Wesentlichen in 2 verschiedenen Modi ausgeführt werden, als Server und als Kommandozeile. Diese beiden Varianten sollen hier kurz vorgestellt werden:

Container als Server

Einen Container als Server starten wir mit “docker run –detach … ” (die kurze Option ist “-d”) und im Laufe des Lebens des Containers arbeiten wir mit “docker start …” und “docker stop …”. Das Kommando und die sinnvollen Optionen dazu sehen aus wie folgt:

docker run --detach --name [name]  [image]

# weitere sinnvolle Kommandos:
docker stop [name]     # Container stoppen
docker start [name]    # Container wieder starten
docker rm [name]       # Container löschen

Wenn man “–detach” oder “-d” vergisst, startet der Container im Vordergrund und hier gibt es kein Entrinnen, außer durch “Ctrl-C”. Danach ist der Container wieder gestoppt. Man könnte den Container mit Ctrl-P Ctrl-Q in den Hintergrund schicken, das erfordert jedoch eine Terminalverbindung (Optionen -ti) die man hierbei entweder sowieso vergisst oder an dieser Stelle überhaupt nicht erwartet.

Startet man den Container mit “–rm”, dann wird der Container nach dem Stoppen oder bei Abbruch direkt wieder gelöscht. “docker start” macht dann keinen Sinn, da der Container beim Stoppen entfernt wurde. Dieses Vorgehen entspricht wohl am besten der Denkweise in Docker – der Container ist ein Wegwerf-Element.

Was wird aber jetzt eigentlich ausgeführt? Docker kennt hier das CMD. Das auszuführende Kommando lassen wir für den Server bei unserem run-Befehl weg, das sollte vom Image kommen. Server-Images definieren normalerweise das CMD, beispielsweise mit

CMD [ "httpd", "-DFOREGROUND" ]

dass der http-Server gestartet werden soll.

D. h., Server-Images sollten CMD definieren, dann kann man den Container mit

docker run ... [image] [Kommando Param1 Param2 ... ParamN]

testweise auch anderweitig verwenden. Häufigster Anwendungsfall ist wohl die Shell “sh” oder “bash”. Damit der Server aber auch nach außen eine Auswirkung zeigen kann, müssen wir die Isolation durch Docker aufheben, z.B. mit –publish:

docker run -d --rm --publish 81:80 nginx 

In diesem Fall starten wir nginx als Web-Server im Hintergrund (-d) und mit automatischem Löschen (–rm) und publizieren (–publish) den Port 81 des Docker-Hosts auf den Port 80 des Containers. Dieser Container ließe sich jetzt mit http://localhost:81 aufrufen. Wir haben die Isolation des Containers nur für diesen einen Port aufgebrochen und so Kommunikation ermöglicht. Damit stellen wir auf unserem Server durch einen Container einen externen Dienst bereit.

Container als CLI-Tool

Die zweite Möglichkeit, Docker-Container zu verwenden wird seltener genutzt, weil es auf den ersten Blick kompliziert erscheint. Ein Alias wird uns helfen, das deutlich zu vereinfachen. Allgemein können Aliase hilfreich sein, da man einige Docker-Optionen sehr häufig benötigt und daher die Kommandos länger und länger werden.

Durch die saubere Umgebung, die der Container beim Neuerstellen bekommt, können wir uns immer darauf verlassen, was der Container enthält. Es gibt keine reineren Installationen als frische Docker-Container. Keine Reste vorheriger Ausführungen, keine verwaisten Software-Installationen. Alles frisch wie am ersten Tag.

Trotzdem wollen wir ein Tool verwenden und eine Auswirkung auf unserer lokalen Maschine bewirken – entgegen der Isolation durch Docker. Ähnlich wie das Publizieren eines Ports oben brechen wir die Isolation mittels “bind mount” auf. Außerdem nutzen wir bind mounts mit einem Trick:

docker run -v ${PWD}:${PWD} -w ${PWD} --rm -ti [image] [Kommando und Parameter]
  • -v ${PWD}:${PWD} (oder -v …) wir nehmen das aktuelle Verzeichnis der Shell (linkes ${PWD}) und binden es unter dem gleichen Pfad in den Container ein (rechtes ${PWD})
  • -w ${PWD} das neue Arbeitsverzeichnis, im Host sind wir ja in diesem Verzeichnis, da wollen wir auch im Container sein.
  • –rm der Container wird nach der Ausführung automatisch gelöscht, den Zombie brauchen wir nicht mehr
  • -ti Mit Terminal und interaktiv, so können wir auch Ein- und Ausgaben machen

Verzeichnisse oberhalb des aktuellen Verzeichnisses sind im Container nicht sichtbar, aber alle darunter liegenden. Als Alias wird das Kommando noch viel einfacher (die richtigen Hochkommas sind wichtig!)

alias docker-here='docker run -v ${PWD}:${PWD} -w ${PWD} --rm -ti'

Beifinden wir uns mit der Shell in einem Java-Maven-Projektverzeichnis und führen aus:

docker-here maven:3.6.1-jdk-11 clean package
docker-here -p 8080:8080 openjdk:11 java -jar target/application.jar

dann bauen und starten wir das Projekt im Container, das Projekt mit Java 12 zu testen ist ähnlich einfach, wir müssen nur die Version des Image austauschen:

docker-here -p 8080:8080 openjdk:12 java -jar target/application.jar

Würde der Container den ENTRYPOINT “java” definieren also z.B. mit einem Dockerfile wie

# Build with 
# docker build . -t java:11
FROM openjdk:11
ENTRYPOINT ["java"]

könnte der Aufruf sogar noch kompakter aussehen:

docker-here java:11 -jar target/application.jar

Der Entrypoint wird dabei dem definierten CMD oder dem auf der Kommandozeile angegebenen Kommando vorangestellt. Dieser Entrypoint kann ein Skript sein, das den Container vorbereitet oder eben ein Programm, das dann nur noch parametrisiert werden muss.

Fallstricke

  • Die Option -ti für das Starten einer Shell im Container zu vergessen, ist vermutlich einer der häufigen Fehler. Manchmal merkt man es nicht, weil man ja einen Prompt erhält, leider immer noch von der Host-Maschine. Docker kehrte unverrichteter Dinge zurück, weil die Shell keinen Input erhalten konnte.
  • Analog gilt für vorhandenes -ti, wenn man wie in einem cron-job keine Konsole zur Verfügung hat. Dann schlägt das Docker-Kommando mit -ti einfach fehl und die Option muss in diesen Fällen weggelassen werden.
  • Mounts (-v) funktionieren auch bei vorhandenen Inhalten auf Dateien und Verzeichnissen – im Container sowieso, da sollen sie auch hin. Aber auch auf dem Host, schlimmstenfalls werden irgendwo leere Verzeichnisse durch root angelegt. Wer etwas expliziter arbeiten möchte, kann stattdessen –mount nutzen.
  • Bind mounts, insbesondere bei dem hier vorgestellten “docker-here” sollten nicht im Suchpfad (/bin, /usr/local/bin) genutzt werden, da sonst Binaires und Bibliotheken von Host und Image im Container ungewollt gemischt werden
  • Sicherheit des Docker-Zugriff – jeder Benutzer, der Docker nutzen darf, kann durch gestartete Container auch jedes Verzeichnis eines Hosts mit beliebigem User im Container lesen und ändern und ist damit effektiv root.
  • Bind mounts wie in docker-here und der Zugriff darauf erfolgen im Docker-Container mit dem entsprechenden User im Container. Daher erhalten die Dateien auch die entsprechenden Owner- und Gruppen-Flags. Dies kann in einem Verzeichnis eines non-Root Benutzers nicht erwünscht sein.

Fazit

Docker ist ein mächtiges Werkzeug. Mit einfachen Unix-Bordmitteln kann man Docker auch lokal sehr einfach nutzen und auch schon während der Entwicklung sehr viel vereinfachen. Ohne Docker als goldenen Hammer für alles positionieren zu wollen – man kann viele lokale Software-Installationen mit Docker vermeiden und vieles einfach dockerisieren. Auf https://hub.docker.com findet man sehr leicht nützliche Images die direkt ausgeführt werden können, ohne die eigentliche Software lokal zu installieren. Durch die Isolation ist das auch sehr gefahrlos für die eigene Maschine.
Spätestens mit dem parallelen Test verschiedener Versionen der gleichen Software (Java 11, 12, 13, …) sticht man dann die manuelle lokale Installation mit Docker aus.

Short URL for this post: https://wp.me/p4nxik-3qM
This entry was posted in Java Runtimes - VM, Appserver & Cloud. Bookmark the permalink.

Leave a Reply