<!--{"title": "Ein eigenes Docker-Image erstellen - so geht's", "date": "2024-04-21", "slug": "ein-eigenes-docker-Image-erstellen-so-gehts", "cover": "/static/img/ein-eigenes-docker-Image-erstellen-so-gehts.png"}-->
**Ein eigenes Docker-Image erstellen, oder ein vorhandenes Image den eigenen Bedürfnissen anpassen?
Natürlich geht das! Ich zeige euch, wie ihr dies einfach umsetzen könnt.**

## Vorwort

Die Verwendung von Images ist in der Welt von Docker und anderen Container-Diensten absoluter Standard.

Viele Nutzer verwenden diese fertig zusammengestellten Images. Oft ist damit auch alles gut, bis man etwas ändern möchte.

Ich diesem praktischen Beispiel möchte ich euch dies mit dem Beispiel einer beliebten Webanwendung zeigen. Wir verwenden hier die Forum-Software von Woltlab.

Wenn Sie die Software als freies Framework nutzen wollen, genügt in der Regel eine Standard-Konfiguration von einem Webserver-Container, z.B. nginx und der FPM-Version von PHP. Beides kann als Standard-Image von Docker-Hub bezogen werden.

Möchte man nun aber Funktionen von Plugins, wie das Abfragen von E-Mail-Konten über das IMAP-Protokoll verwenden, so kann dies normalerweise nicht erfolgen, da die PHP-Erweiterung IMAP nicht vorhanden ist. Jetzt kommt das große Problem. Wie kann die Erweiterung möglichst dauerhaft konfiguriert werden? Denn jede Änderung an den Container-Einstellungen wird beim Neustart, spätestens jedoch beim Update des Images überschreiben.

Die Antwort: Ein eigenes Image erstellen.

Ich möchte euch hier zeigen, wie ihr ein All-in-One Image erzeugen könnt.

## Vorbereitung

Zunächst sollten wir uns aber alle Quellen ansehen, die wir zur Erstellung des Docker-Images brauchen, oder berücksichtigen müssen.

### Alpine-Linux

Ich erstelle das Image auf Basis von alpine-Linux. Alpine ist eine sehr kleine Linux Distribution und sehr weit verbreitet.

Da ich auf dieser Basis sowohl PHP, als auch nginx erzeugen möchte, benötige ich die konkreten Pakete, die im Image erzeugt und installiert werden müssen.

Dafür schaue ich mir die Paket-Details auf dem Paket-Server von alpine-Linux an: [https://pkgs.alpinelinux.org/packages](https://pkgs.alpinelinux.org/packages)

### PHP unter Alpine-Linux

Speziell sehe ich nach PHP, da hier die Version eine besondere Rolle bei der Kompatibilität mit den Anwendungen spielt:

[Alpine Linux packages](https://pkgs.alpinelinux.org/package/edge/community/armhf/php83)

Wenn Sie eine andere PHP-Version installieren möchten, können Sie im oberen der zwei Links im Feld "Package name" einfach ``php*`` eingeben und die Architektur auf x86_64 einstellen und suchen.

Ich kompiliere jedoch die zur Zeit der Veröffentlichung aktuellste und mit Woltlab kompatible Version PHP 8.3 (8.3.6).

Beachten Sie, dass Sie neben dem Haupt-Paket von PHP auch alle benötigten Erweiterungen installieren müssen.

### nginx unter Alpine-Linux

Beim Paket von nginx ist die Installation eindeutiger. Hier gibt es nur das Paket "nginx".

### Redis unter Alpine-Linux

Ebenso sieht es beim Paket für den Redis-Dienst aus. Auch hier gibt es nur das Paket "redis".

### Basis-Pakete unter Alpine-Linux

Zudem müssen bestimmte Basis-Pakete installiert werden, um die Voraussetzungen zum Start der Dienste zu gewährleisten, oder um für spätere Wartungsarbeiten direkt die wichtigsten Tools an der Hand zu haben.

Hier sind besonders zu nennen:

* bash
* autoconf
* automake
* make
* gcc
* g++

## Aufbau der Datei "Dockerfile"

Ein Dockerfile ist eine Textdatei, die eine Reihe von Anweisungen enthält, um ein Docker-Image zu erstellen. Hier ist eine typische Struktur eines Dockerfiles:

### Basisimage wählen

Der erste Schritt in einem Dockerfile ist die Auswahl eines Basisimages, auf dem das neue Image aufbauen wird. Dies geschieht mit dem FROM-Befehl, gefolgt von dem Namen des Basisimages und optional der Versionsnummer.

Beispiel:

```
FROM ubuntu:20.04
```

### Pakete und Abhängigkeiten installieren

Anschließend können Anwendungen oder Bibliotheken installiert werden, die für die Ausführung der gewünschten Software benötigt werden. Dies geschieht normalerweise mit Paketverwaltungstools wie apt für Ubuntu-basierte Images oder yum für CentOS-basierte Images.

Beispiel:

```
RUN apt-get update && apt-get install -y <paketname>
```

### Arbeitsverzeichnis festlegen

Mit dem WORKDIR-Befehl kann das Arbeitsverzeichnis innerhalb des Containers festgelegt werden, in dem Befehle ausgeführt werden.

Beispiel:

```
WORKDIR /app
```

### Dateien hinzufügen/kopieren

Dateien und Verzeichnisse können mit dem ADD- oder COPY-Befehl in das Image kopiert werden. COPY ist dabei meist vorzuziehen, da es weniger Funktionalität als ADD bietet, aber auch weniger überraschendes Verhalten hat.

Beispiel:

```
COPY ./app /app
```

### Umgebungsvariablen setzen

Mit dem ENV-Befehl können Umgebungsvariablen innerhalb des Containers gesetzt werden, die von der ausgeführten Anwendung genutzt werden können.

Beispiel:

```
ENV DB_HOST=localhost
```

### Ports freigeben

Wenn die Anwendung einen Netzwerkport verwendet, muss dieser mit dem EXPOSE-Befehl im Dockerfile bekannt gegeben werden. Dies beeinflusst jedoch nicht, wie der Container tatsächlich gestartet wird.

Beispiel:

```
EXPOSE 8080
```

### Befehl zum Ausführen der Anwendung

Schließlich wird ein Befehl definiert, der beim Start des Containers ausgeführt werden soll. Dies wird durch den CMD- oder ENTRYPOINT-Befehl erreicht.

Beispiel:

```
CMD ["python", "app.py"]
```

## Konkretes Beispiel für mein Dockerfile mit PHP, nginx und Redis auf Alpine-Linux

Nachdem wir jetzt wissen, was wir für das Erstellen eines Images benötigen können wir uns dem eigentlichen Herzstück für die Erstellung eines eigenen Images widmen, dem Dockerfile.

```
FROM alpine:latest
RUN apk update && apk upgrade
RUN apk add bash
RUN apk add autoconf automake make gcc g++ 
RUN apk add nginx
RUN apk add php83 php83-fpm php83-opcache php83-tokenizer
RUN apk add php83-gd php83-zlib php83-curl php83-bz2 php83-bcmath php83-exif php83-fileinfo php83-iconv php83-imap php83-intl php83-ldap php83-mbstring php83-mysqli php83-odbc php83-pdo php83-pdo_mysql php83-pdo_odbc php83-pdo_pgsql php83-pdo_sqlite php83-pear php83-pecl-imagick php83-pecl-memcache php83-pecl-memcached php83-pecl-mongodb php83-pecl-redis php83-pecl-smbclient php83-pecl-ssh2 php83-pecl-xdebug php83-pecl-yaml php83-pgsql php83-phar php83-phpdbg php83-session php83-simplexml php83-snmp php83-soap php83-sockets php83-sodium php83-sqlite3 php83-sysvmsg php83-tidy php83-xml php83-xmlreader php83-xmlwriter php83-xsl php83-zip
RUN apk add redis
#COPY server/etc/nginx /etc/nginx
#COPY server/etc/php /etc/php83
#COPY server/etc/redis/redis.conf /etc/redis.conf
#COPY src /usr/share/nginx/html
RUN mkdir /var/run/php
EXPOSE 80
EXPOSE 443
EXPOSE 6379
EXPOSE 9000
STOPSIGNAL SIGTERM
CMD ["/bin/bash", "-c", "php-fpm83 && chmod 755 /usr/share/nginx/html/* && nginx -g 'daemon off;' && redis-server /etc/redis.conf"]
```

In meinem Dockerfile wird nun alles zum Image hinzugefügt, was für den Betrieb notwendig ist.

Als Basis-Image dient die letzte Version von alpine (alpine:latest). Danach werden alle offenen Updates des Betriebssystems abgerufen und installiert (RUN apk update && apk upgrade). Im Anschluss werden die Basis-Pakete installiert (RUN apk add bash | RUN apk add autoconf automake make gcc g++). Danach werden der Webserver nginx, PHP mit seinen Erweiterungen und der Caching-Dienst Redis installiert.

Die Auskommentierten COPY-Befehle würden die Konfigurationen relativ zum Verzeichnis des Dockerfile direkt in das Image schreiben.

Der nächste Befehl (RUN mkdir /var/run/php) erstellt ein Verzeichnis, welches für PHP benötigt wird.

Mit den EXPOSE-Befehlen geben wir dem Image an. Schnittstellen an den Container freizugeben, die von außerhalb erreichbar gemacht werden sollen. Dazu gehören die Ports des nginx (80, 443), der PHP-FPM-Port (9000) und der Redis-Port (6379).

Zuletzt geben wir noch das Start und Stoppverhalten vor.

Damit ist das Dockerfile einmal erläutert. Wie man das Image jetzt erzeugt, folgt jetzt.

## Erzeugen des Images

Das Erzeugen des Images ist denkbar einfach. Dennoch gibt es ein paar Dinge zu beachten, damit Ihre Image-Verwaltung nicht in einem Debakel endet.

* Erzeugen Sie Images möglichst immer mit einem eindeutigen Tag (alpine_np83r:1.0)
* Wenn Sie eine eigene, interne Containerregistrierung haben, können Sie auch direkt den vollständigen Tag verwenden. z.B.: (registry.homelab.lan/alpine_np83r:1.0)
* Erstellen Sie zudem den Tag "latest" zu ihrem letzten, funktionierenden Image.

Erstellen können Sie das oben definierte Image mit dem Befehl ```sudo docker build -t alpine_np83r:1.0 .```.

Beachten Sie, das Sie mit der Konsole zuvor in das Verzeichnis navigieren müssen, indem sich die oben erstellte Datei (Dockerfile) befindet.

Danach können Sie das Image innerhalb dieser Maschine verwenden.

Möchten Sie das Image auf anderen Maschinen verwenden, so müssen Sie dieses Image auf eine Container-Registrierung hochladen.

Haben Sie beim Erstellen des Images den vollständigen Tag-Namen mit Ihrer Registry eingegeben, können Sie das image mit folgendem Befehl in Ihre Registrierung "pushen": ```sudo docker push registry.homelab.lan/alpine_np83r:1.0```.

## Fazit

Ich hoffe ich konnte Ihnen die Sorgen rund um das Thema "Eigene Docker Images erstellen" etwas nehmen. Ich habe für mich festgestellt, das es nur etwas Übung braucht, um mit der Thematik "warm" zu werden. Insgesamt ist mein Fazit, das es sich definitiv auszahlt, eigene Images zu erstellen und auch in einer eigenen Registrierung vorzuhalten.

In diesem Sinne, bis zum nächsten Thema!