Dockerfile Einstieg für Anfänger: Was ist ein Dockerfile und ein Image? Wie kann ich Images anpassen?

Als Video ansehen
Bereitgestellt über YouTube

Dockerfile Einstieg für Anfänger: Was ist ein Dockerfile und ein Image? Wie kann ich Images anpassen?

In den bisherigen Beiträgen haben wir ausschließlich fertige Images aus dem Docker-Hub genutzt, beispielsweise für die Webserver Nginx und Apache. Für einfachere Anwendungsfälle reicht das. Wer sich tiefergehend mit Docker beschäftigt, wird jedoch irgendwann an die Grenzen stoßen und möchte ein Image anpassen oder gar ein komplett eigenes erstellen. Dies möchten wir im heutigen Beitrag genauer anschauen.

Was ist ein Dockerfile?

Die Antwort in der Frage liegt in den Images. Was ist ein Image? Es besteht aus einer Reihe von Anweisungen, um eine bestimmte Software zu installieren und einzurichten. Dies kann beispielsweise der Apache-Webserver sein, eben so wie nahezu jede andere Software. Werden diese Anweisungen ausgeführt, entsteht im sogenannten Build-Prozess ein Image. Diese Anweisungen finden sich im Dockerfile. Zusätzlich noch einige weitere Befehle, mit denen sich ein so erstelltes Image von außen beeinflussen lässt – beispielsweise mit Umgebungsvariablen.

Warum sollte ich mit einem Dockerfile ein eigenes Image erstellen?

Hauptsächlich gibt es zwei Gründe:

  1. Um ein vorhandenes Image umfangreicher anzupassen
  2. Zum „Containerisieren“ von Programmen, für die noch kein bzw. kein (offizielles) Image existiert

Wie sieht ein Dockerfile genau aus?

Der grundsätzliche Aufbau eines Dockerfiles ist leicht erklärt: Kommentare werden mit einer Raute # eingeleitet. Alle Befehle weisen das Format ANWEISUNG <Argumente> auf:

# Diese Zeile wird ignoriert
ANWEISUNG <Argumente>

Da sich Docker durch Modularität auszeichnet, gibt es immer ein Basis-Image. Es wird mit der Anweisung FROM definiert, das Argument ist der Name des Basis-Images. Alles was dort drin ist, wird auch in unserem Image enthalten sein – es sei denn, wir ändern und entfernen etwas davon. In der Praxis ist das Dockerfile eine Textdatei mit gleichem Name, ohne .txt oder anderen Erweiterungen. Ich empfehle, pro Dockerfile einen eigenen Ordner zu erstellen.

Unser erstes Dockerfile: Wir fügen eine Datei hinzu

Beginnen wir mit dem ersten Grund, er ist am einfachsten: Wir haben ein Image, dass grundsätzlich unseren Vorstellungen entspricht. Aber es gibt Dinge, die angepasst werden sollen. Ein mögliches Szenario ist der Apache2 Webserver mit mod_php aus dem vorherigen Beitrag. Hier wird ein PHP-Modul benötigt, etwa MySQLi um sich zu einer MySQL-Datenbank verbinden zu können. Dies ist standardmäßig nicht enthalten bzw. nicht aktiviert.

Um das zu ändern, legen wir uns einen Ordner an, erzeugen eine Datei namens Dockerfile ohne Erweiterung und öffnen sie. Es macht Sinn, mit der FROM Anweisung zu beginnen. Nach einem Leerzeichen geben wir das Apache-PHP-Image mit dem gewünschten Tag an, etwa

FROM php:8-apache-buster
An dieser Stelle wird das Buster-Image (Version 10, statt dem zum Erstellzeitpunkt aktuellsten Bullseye 11) image genutzt. Hintergrund ist dieser Bug im PHP-Image, wodurch der später verwendete Befehl
docker-php-ext-install
ohne Ausgabe nie fertiggestellt wird. Bitte den Status im verlinkten Bugticket prüfen, falls dieser Fehler behoben ist, kann man auch aktuellere Images nutzen! Buster erhält noch bis Ende 2022 Sicherheitsupdates.

Die wohl wichtigste Anweisung nach FROM heißt RUN: Damit führt man einen beliebigen Linux-Befehl aus. Wir nutzen im Folgenden die Shell-Form, das heißt: Im Hintergrund wird eine SH-Shell gestartet und unser Befehl darin ausgeführt. Ein einfaches Beispiel ist ein echo-Befehl:

RUN echo Hallo Docker!

Wichtig zu verstehen ist: Ein Image ist eine Vorlage für einen Container. Es besteht aus mehreren Schichten eines Dateisystemes. Wie bei einer Schablone können wir mithilfe eines Images also beliebig viele identische Container erstellen. Mit diesem Verständnis wird deutlich, dass obiger Befehl wenig Sinn ergibt: Beim ersten Bauen des Images würde der Echo-Befehl ausgeführt. Für das Image hat er keine Bedeutung, da er nichts ins Dateisystem schreibt. Ein sinnvolleres Beispiel wäre, wenn wir etwa eine PHP-Datei für den Webserver ablegen würden:

RUN echo '<?php echo "Hallo vom Dockerfile";' > /var/www/html/hallo.php

Damit sind wir an dieser Stelle für die erste Demo fertig und können die Datei speichern.

Anlegen einer docker-compose.yml, die unser Image baut und einen Container startet

Aus dem Dockerfile lässt sich ein Image bauen, welches für einen Container genutzt werden kann. Diese Schritte lassen sich händisch und getrennt ausführen. In der Praxis ist das mühsam. Wenn man Images für sich selbst entwickelt, die nicht über z.B. den Docker-Hub mit anderen geteilt werden sollen, ist Docker-Compose auch hier der einfachste und sinnvollste Weg. Wir können dort nämlich automatisch ein Image bauen und einen Container erstellen lassen.

version: "2.4"

services:
    mein-webserver:
        build: .
        restart: always
        ports:
            - 80:80

Die Besonderheit liegt darin, dass wir kein image angeben – sondern build. Das legt den sogenannten Buildkontext fest, also den Pfad, in dem ein Dockerfile gebaut wird. Der Punkt steht unter Linux für das aktuelle Verzeichnis. Dort sucht Compose nach einem Dockerfile. Sofern wir dies nicht anders benennen, müssen wir keinen Dateiname angeben. Mit docker-compose up -d –build lösen wir nun zwei Vorgänge aus:

  1. Anhand des Dockerfiles wird ein Image gebaut
  2. Das Starten eines Containers mit dem zuvor erstellten Image
$ docker-compose up -d --build
Sending build context to Docker daemon     335B
Step 1/3 : FROM php:8-apache-buster
 ---> e2070fb1b17d
Step 2/3 : RUN echo Hallo Docker!
 ---> Running in d51571677206
Hallo Docker!
 ---> 356e1b361b67
Step 3/3 : RUN echo '<?php echo "Hallo vom Dockerfile";' > /var/www/hallo.php
 ---> Running in 05bb1b73251e
 ---> 3376002c07d3
Successfully built 3376002c07d3
Successfully tagged erstes-dockerfile_mein-webserver:latest
[+] Running 1/1
 ⠿ Container erstes-dockerfile-mein-webserver-1  Started                                                                                                                                1.2s
$ docker-compose up -d --build
Sending build context to Docker daemon     335B
Step 1/3 : FROM php:8-apache-buster
 ---> e2070fb1b17d
Step 2/3 : RUN echo Hallo Docker!
 ---> Running in d51571677206
Hallo Docker!
 ---> 356e1b361b67
Step 3/3 : RUN echo '<?php echo "Hallo vom Dockerfile";' > /var/www/hallo.php
 ---> Running in 05bb1b73251e
 ---> 3376002c07d3
Successfully built 3376002c07d3
Successfully tagged erstes-dockerfile_mein-webserver:latest
[+] Running 1/1
 ⠿ Container erstes-dockerfile-mein-webserver-1  Started                                                                                                                                1.2s

Hier sieht man, dass der erste echo-Befehl ausgeführt wird – aber eben mangels Schreiboperation keine Auswirkung auf das Image hat. Der Zweite schreibt dagegen in eine PHP-Datei. Nach dem Start des Webservers können wir diese über den Hostname oder die IP-Adresse des Pi aufrufen, wo das Skript ausgeführt und der Text ausgegeben wird:

Realer Anwendungsfall: Installation von PHP-Modulen

Kommen wir zum anfangs erwähnten realistischeren Anwendungsfall: Wir möchten ein PHP-Modul installieren. Zunächst fügen wir im Dockerfile einen weiteren RUN-Befehl ein, um die PHP-Info auszugeben. Sie zeigt, welche Module installiert sind.

RUN echo '<?php phpinfo();' > /var/www/html/info.php

Anschließend das Image neu bauen und den Container ebenfalls neu erstellen. Damit ein neu erstelltes Image Anwendung findet, müssen alle Container, die es verwenden neu erstellt werden. Docker-Compose kümmert sich bei allen Containern der aktuellen docker-compose.yml Datei automatisch darum:

docker-compose up -d --build

Durch den Aufruf unseres erstellten Skriptes info.php können wir sehen, dass die MySQLi-Erweiterung nicht installiert ist. Auf der gesamten Seite gibt es nur einen Treffer in den Credits:

Wäre die Erweiterung installiert, müsste ein eigener Bereich mit ihrer Konfiguration weiter oben auftauchen. PHP bietet uns in den offiziellen Docker-Images zwei Werkzeuge für Erweiterungen: docker-php-ext-install zur Installation von Erweiterungen und docker-php-ext-configure für zusätzliche Konfigurationsparameter. Letzteres ist nicht bei allen Erweiterungen nötig.

Um die MySQLi-Erweiterung zu installieren, benötigen wir einen RUN-Befehl, der docker-php-ext-install aufruft und als Parameter den Name der Erweiterung übergibt:

RUN docker-php-ext-install mysqli

Wie schon zuvor gezeigt, müssen wir nun docker-compose up -d –build aufrufen, um das neue Image zu erstellen. Da die Erweiterung kompiliert werden muss, kann dies – vor allem auf dem Raspberry Pi – ein paar Minuten dauern. Wird anschließend die PHPInfo im Browser aktualisiert, sehen wir einen eigenen Bereich zu MySQLi:

Die Erweiterung wurde also erfolgreich installiert, sie kann nun im PHP-Code genutzt werden, um auf eine MySQL-Datenbank zuzugreifen.

Auf ähnlichem Wege kann man auch ein eigenes Image erstellen, um etwa eine Anwendung zu dockerisieren. Dies schauen wir uns in einem eigenen Teil an.

Leave a Reply