12.8    Docker-Container erstellen

In diesem Kapitel haben wir bereits viel über das Übersetzen und Installieren von Software, die dafür benötigten Tools und über dynamisch an Programme gelinkte Bibliotheken gesprochen. Aus den vorherigen Kapiteln kennen Sie auch die unterschiedlichen Distributionen mit ihren jeweiligen Paketverwaltungen. Sie können sich sicher vorstellen, dass dies in der Praxis zu umfangreichen und komplizierten Installationsanleitungen führen kann, die sich zu allem Überfluss auch noch je nach Distribution unterscheiden können. Wäre es nicht toll, wenn man Software so »paketieren« könnte, dass man einfach einen Prozess startet und dieser Prozess schon alle seine Abhängigkeiten, Bibliotheken etc. mitbringt und isoliert vom Rest des Systems läuft?

Das ist genau die Grundidee von Docker. Ein Docker-Container ist nichts anderes als ein normaler Linux-Prozess, also ein gestartetes Programm – jedoch mit einigen Unterschieden: Durch die Nutzung bestimmter Kernelfunktionalitäten wie Prozess-Namespaces, Cgroups und Capabilities sind Container von allen anderen Prozessen des Systems isoliert. Sie haben ihr eigenes Dateisystem und können nicht ohne Weiteres auf das Dateisystem des Hauptsystems zugreifen. Außerdem sind sie von allen anderen Prozessen des Systems isoliert, sie haben einen eigenen (virtuellen) Netzwerk-Stack und können bezüglich der zur Verfügung stehenden Systemressourcen wie CPU und RAM limitiert werden.

In einem Docker-Image können Sie also ein komplettes Dateisystem verpacken, verbunden mit einem Prozess, der beim Starten des Docker-Containers aufgerufen wird. Es ist nicht übertrieben zu sagen, dass diese Idee die IT in den letzten Jahren ein Stück weit revolutioniert hat.

12.8.1    Virtualisierung, Container und andere Betriebssysteme

Container ermöglichen damit eine neue und leichtgewichtige Art der Virtualisierung. Anstatt die komplette Hardware ganzer Computersysteme inklusive CPU, RAM, Festplatten, BIOS und aller Peripheriegeräte zu emulieren und als virtuelle Maschine (VM) wiederum ein ganzes Betriebssystem einschließlich aller Overheads laufen zu lassen, ist ein Docker-Container so leichtgewichtig wie ein normaler, gestarteter Linux-Prozess – trotzdem kann eine weitgehende Isolierung einzelner Workloads erreicht werden.

Container basieren dabei auf nativen Features des Linux-Kernels. Zwar ist Docker als Open-Source-Software auch für Windows und macOS verfügbar und kann daher auch für Projekte auf diesen Plattformen genutzt werden, jedoch bringt Docker für diese Betriebssysteme noch eine »versteckte« Linux-VM mit, in der die eigentlichen Container gestartet werden.

12.8.2    Docker-Images bauen

Ein Docker-Image ist eine Blaupause für einen Container. Es besteht im Wesentlichen aus dem Dateisystem Image) und einem Programm, das beim Starten des Containers aufgerufen wird. Ein Docker-Image wird vollständig durch eine Datei, das sogenannte Dockerfile, beschrieben:

FROM debian:11
RUN apt-get update && apt-get -y install nginx
COPY index.html /var/www/html
CMD ["/usr/sbin/nginx","-g","daemon off;"]

Listing 12.51     Ein einfaches Dockerfile

Ein Dockerfile besteht aus mehreren Befehlswörtern, die jeweils am Zeilenanfang stehen. Es wird in YAML-Syntax verfasst, die Einrückungen auf unterschiedliche Ebenen sind also wichtig. Die meisten Editoren unterstützen Sie mit entsprechenden Formatierungshilfen. Unser Beispiel nutzt folgende Befehle:

Ein Dockerfile ist also meist recht einfach und kurz gehalten. Oft wird es gemeinsam mit dem Sourcecode einer Applikation in das Code-Repository abgelegt.

Um ein Docker-Image aus diesem Dockerfile zu bauen, muss docker auf dem lokalen Rechner installiert sein. In einem Verzeichnis mit der Datei Dockerfile (sowie einer schnell erstellten index.html) bauen wir das Image mit dem folgenden Befehl:

$ docker build .
[+] Building 1.5s (9/9) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 192B
 => [internal] load .dockerignore
 => => transferring context: 2B
 => [internal] load metadata for docker.io/library/debian:11
 => [auth] library/debian:pull token for registry-1.docker.io
 => [1/3] FROM docker.io/library/debian:11@sha256:cc58a29c[...]
 => [internal] load build context
 => => transferring context: 39a51ca2767f1b096a3a1962124e8[...]
 => [2/3] RUN apt-get update && apt-get -y install nginx
 => [3/3] COPY index.html /var/www/html
 => exporting to image
 => => exporting layers
 => => writing image sha256:39a51ca2767f1b096a3a1962124e8d[...]

Listing 12.52     Docker-Image bauen

Mittels des Parameters build wird das Docker-Image gebaut, und der Punkt gibt an, dass das aktuelle Verzeichnis der Buildkontext ist. Im Buildkontext wird automatisch nach einer Datei gesucht, die Dockerfile heißt (alternativ könnte auch mit -f ein anderer Dateiname angegeben werden), und sie wird übersetzt. Der Buildkontext ist auch die Basis für alle COPY-Befehle. (Achtung: Dateien aus übergeordneten Verzeichnissen können aus Sicherheitsgründen nicht kopiert werden.)

Während des Bauens werden die einzelnen RUN- und COPY-Kommandos ausgeführt. Am Ende wird ein Image erzeugt, das mit einem Hash (39a51ca...) identifiziert wird. Dieser Hash ist der Name des Images, mit dem Sie den Container starten können. Sie müssen dabei nicht den ganzen Hash angeben, es reichen die ersten 5–6 Zeichen des Hashes – diese müssen nur eindeutig sein, sollte man mehrere Images lokal gebaut oder gespeichert haben. Alternativ können Sie das Image auch »taggen« und ihm einen »richtigen« Namen sowie ein Tag geben:

$ docker tag 39a51 jploetner/nginx-test:v1

Listing 12.53     Docker-Image taggen

Mit dem passenden Tag kann das Image sogar direkt auf den Dockerhub (https://hub.docker.com/) veröffentlicht werden. Dazu muss man wissen, dass der Repository-Name im Imagenamen angegeben wird – die Konvention lautet Servername/Repository/Imagename:Tag. Ist wie im obigen Beispiel kein Servername explizit angegeben, wird Dockerhub bzw. docker.io angenommen. Auf https://hub.docker.com/ können Sie sich auch selbst einen (aktuell noch kostenlosen) Account anlegen. Der Accountname entspricht dem Repository-Namen.

Im obigen Beispiel lautet der Dockerhub-Account jploetner, der Imagename nginx-test sowie der Tag v1. Um ein Image auf Dockerhub zu veröffentlichen, müssen Sie das mit Ihrem Accountnamen korrekt getaggte Image nach einem Login auf der Kommandozeile »pushen«:

$ docker login
[...]
$ docker push jploetner/nginx-nginx-test:v1
The push refers to repository [docker.io/jploetner/nginx-test]
afcc13ba555c: Pushed
1df77f15b23c: Pushed
afa3e488a0ee: Mounted from library/debian
[...]

Listing 12.54     Docker-Image zu Dockerhub pushen

In diesem Beispiel haben wir auch gesehen, dass Docker für jedes Kommando im Dockerfile einen eigenen »Layer« erstellt hat. Diese Layer werden einzeln gepusht, was den Vorteil hat, dass bekannte Layer wie das debian:11-Image schon vorhanden sind und nicht mehrfach gespeichert werden müssen. Nach dem Push ist das Image überall auf der Welt verfügbar und kann von jedem anderen Rechner im Internet heruntergeladen werden (»pull«):

$ docker pull jploetner/nginx-test:v1
v1: Pulling from jploetner/nginx-test
627b765e08d1: Pull complete
523f49663665: Pull complete
68f1e9413f31: Pull complete
Digest: sha256:dd08f90e8866831e5ad12045be3cad7a4afe318[...]
Status: Downloaded newer image for jploetner/nginx-test:v1
docker.io/jploetner/nginx-test:v1

Listing 12.55     Docker-Image herunterladen

Damit haben wir erfolgreich ein Dockerfile geschrieben, daraus ein Docker- Image gebaut und im Internet veröffentlicht.

12.8.3    Docker-Container starten und verwalten

Um einen Docker-Container zu starten, wird ebenfalls das docker-Kommando bemüht:

$ docker run -d jploetner/nginx-test:v1
64ac4203d5aee2f8097a33176b8e3b680a536083cd44e2349bd2c98b10dd7590

Listing 12.56     Docker-Container starten

Via docker run -d wird ein neuer Docker-Container im Hintergrund (»d« steht für »detached«) gestartet, der standardmäßig über eine UUID identifiziert wird. Diese UUID ist dabei nicht mit der im letzten Abschnitt genannten Image-ID, einem Hashwert, zu verwechseln.

Selbstverständlich kann das gleiche Image auch in mehreren Containern instanziiert werden, wobei jede Instanz ihre eigene, zufällige UUID bekommt. Alle laufenden Container sehen Sie sich mit docker ps:

$   docker ps
CONTAINER ID   IMAGE                     COMMAND                  
64ac4203d5ae   jploetner/nginx-test:v1   "/usr/sbin/nginx -g ?"  [..] 
cfdfbbbea648   jploetner/nginx-test:v1   "/usr/sbin/nginx -g ?"  [..] 

Listing 12.57     docker ps

Über die Container-ID können Sie einen laufenden Container auch stoppen und wieder starten:

$ docker stop 64ac4203d5ae
64ac4203d5ae
$ docker ps
[der Container wird nicht als laufend angezeigt]
$ docker ps --all
[--all zeigt auch gestoppte Container]
$ docker start 64ac4203d5ae
64ac4203d5ae

Listing 12.58     Container stoppen und starten

Alternativ können Sie anstatt der UUID auch den Namen des Containers verwenden. Um einen eigenen Namen zu setzen, rufen Sie docker run mit der Option --name sowie dem zu verwendenden Namen auf.

12.8.4    Mit Containern interagieren

Es gibt mehrere Möglichkeiten, mit Docker-Containern zu interagieren. Die wichtigsten zwei Möglichkeiten sind Port-Forwards von Netzwerkports des Hostsystems zum Container sowie Mounts von Verzeichnissen des Hostsystems in den Container. Diese Optionen werden als zusätzliche Optionen zum docker run-Befehl hinzugefügt:

$ docker run -d -p 8080:80 -v /home/jploetner/html:/var/www/html 
jploetner/nginx-test:v1

Listing 12.59     Ein Container mit Port-Forward und Volume-Mount

Die Option -p erwartet den Port auf dem Hostsystem, einen Doppelpunkt als Trenner und daneben gleich den Zielport im Container. In unserem Beispiel läuft im Container ein NGINX-Webserver auf Port 80, und wir mappen Port 8080 des Hostsystems auf diesen Containerport. Wenn Sie also in der Folge http://localhost:8080 in Ihrem Browser aufrufen, werden Sie mit dem Webserver im Container verbunden.

Die Option -v erwartet ein Verzeichnis auf dem Hostsystem, einen Doppelpunkt als Trenner und den Pfad im Container, in den das Hostverzeichnis eingebunden werden soll. Im obigen Beispiel mappen wir das Verzeichnis /home/jploetner/html in den Container auf das Verzeichnis, das der Webserver als Hauptverzeichnis nutzt. Legen Sie in dieses Verzeichnis eine Datei index.html mit einem beliebigen Text, und besuchen Sie dann noch einmal http://localhost:8080 im Browser.

Über den Befehl docker exec können Sie auch zusätzliche Prozesse im Container starten:

$ docker exec -it 64ac4203d5ae /bin/bash
root@64ac4203d5ae:/#

Listing 12.60     Eine Shell im Container ausführen

Die Optionen -i und -t erstellen eine interaktive Terminal-Session und verbinden diese mit dem aufgerufenen Prozess, in unserem Fall wird das (im Container vorhandene) Programm /bin/bash, also eine Shell, gestartet. Dieses Vorgehen wird gern zu Debuggingzwecken genutzt, setzt aber voraus, dass es tatsächlich eine Shell als Binary im Container gibt.