# Container Images Um einen Container (beispielsweise mit Docker) zu starten, braucht man ein Image. Ein Image repräsentiert das Dateisystem des Containers zum Startpunkt sowie die Befehle, die beim starten des Containers ausgeführt werden sollten. So ermöglichen uns bekannte Images wie Postgres, eine Datenbank mit einem einfachen Befehl zu erstellen. Für Container von Applikationen die wir nutzen wollen gibt es warscheinlich bereits ein Image. Wenn wir aber unsere eigene Applikation in einem Container laufen lassen wollen, dann müssen wir zuerst ein Image erstellen. ## Dockerfile Einer der Wege solche Images zu erstellen ist über ein Dockerfile. Der Name kann hier teilweise irreführend sein, da Dockerfiles nicht mehr nur von Docker gebaut werden können. Die daraus entstehenden Images sind grundsätzlich auch universell nutzbar und somit nicht für Docker exklusiv. Für unsere Beipiele hier werden wir aber trotzdem mit Docker arbeiten. Die Idee hinter einem Dockerfile ist folgende: - Wir nehmen ein Basis-Image - Wir kopieren unsere Applikation auf das Basissystem - Wir installieren die nötigen Pakete - Wir bauen die Applikation - Wir starten die Applikation Mit dem Caching werden wir später sehen, dass wir einige dieser Schritte noch genauer unterteilen müssen. Vorerst reicht dieser Ablauf jedoch aus. Diese Schritte sind unter den meisten Applikationen gleich. Die Implementation variiert aber selbstverständlich unter den verschiedenen Sprachen. ### Basis-Image Das Basis-Image ist das Image, in welchem wir unsere Schritte ausführen wollen. Für den Bauprozess und auch das starten brauchen wir meistens gewisse Tools. Um eine .NET Applikation zu bauen brauchen wir beispielsweise eine .Net Runtime, für eine Java Applikation wäre das ein JDK, .... Es gibt grunsätzlich drei Vorgehensweisen: #### Anwenderspezifisches Basis-Image Die einfachste und wahrscheinlich auch am weitesten verbreitete Vorgehensweise ist die Wahl eines Anwenderspezifischen Basis-Image. Heutzutage bieten die meisten Anwender ein eigenes Container Image an auf welchem alle nötigen Pakete bereits installiert sind. So bietet Microsoft für .NET beispielsweise ein vorgefertigtes [.NET Runtime](https://mcr.microsoft.com/en-us/product/dotnet/runtime/tags) Image an, welches wir für unsere Applikation nutzen können. #### Minimalistische Distribution Eine Alternative die auch noch genutzt wird ist, als Basis-Image eine minimalistische Distribution zu wälen und auf dieser eine eigene Umgebung mit den nötigen Abhängigkeiten einzurichten. Ein Beispiel für ein solches Basis-Image wäre [Alpine](https://hub.docker.com/_/alpine). Diese Vorgehensweise macht dann Sinn, wenn in einem Image verschiedene Anwender genutzt werden oder wenn die Freiheit der eigenen Pakete wichtig ist. Es werden jedoch mehr Schritte benötigt. #### From scratch Die letzte Vorgehensweise ist dank ihrer Komplexität nicht sehr häufig vorzufinden. Es ist jedoch möglich, auf einem vollkommen leeren Basis-Image aufbauend eine Umgebung einzurichten. Dafür müssen aber alle Binaries, referenzierten Dateien und sonstige Tools manuell auf das Image geschrieben werden. Dies ist dann praktisch, wenn ein möglichst minimalistisches und sicheres Image von Nöten ist. Ansonsten ist von dieser Vorgehensweise abzuraten. Innerhalb unseres Dockerfiles setzen wir das Basis-Image folgendermassen: ```dockerfile FROM mcr.microsoft.com/dotnet/runtime:8.0.6 ``` ### Kopieren von Dateien Als nächstes müssen wir unsere Applikation auf den Container kopieren. Es gibt dafür zwei Befehle welche je nach Anwendungsfall genutzt werden sollten: #### COPY Der einfachere der beiden ist `COPY`. Wie der Name bereits verräht werden hier Dateien vom Host auf das Image-Dateisystem kopiert. Die Anwendung ist sehr einfach: ```dockerfile COPY . . ``` Links wird der Pfad vom Hostsystem und Rechts der des Containers angegeben. Falls in einem bestimmten Ordner auf dem Container gearbeitet werden sollte, empfiehlt es sich diesen vorab zu definieren: ```dockerfile WORKDIR /app ``` Alle auf diese Zeile folgenden Zeilen nehmen diesen Ordner als Ausgangslage. #### ADD `ADD` ist ein bisschen komplizierter, wird aber auch nicht oft genutzt. Innerhalb dieses Befehls verstecken sich zwei Funktionalitäten: Das Kopieren von einer URL und Das Kopieren und Entpacken eines Archivs. Beide sind dem `COPY` Befehl in der Anwendung sehr ähnlich. Für ein Archiv kann einfach als Host-Pfad die Archivdatei (z.B.: \<name\>.tar.gz) angegeben werden und es wird automatisch an den Zeilort extrahiert. ### Installieren der nötigen Pakete und Bauen der Applikation Als nächstes wollen wir die nötigen Abhängigkeiten unsere Applikation istallieren. Dafür nutzen wir die Dockerfile-Anweisung `RUN` mit dem anwenderentsprechenden Befehl. Für eine Node.js Applikation würden wir also folgende Zeile nehmen: ```dockerfile RUN npm install ``` `RUN` kann für jegliche Befehle genutzt werden, vorausgesetzt, das Binary ist vorhanden. So nutzen wir ihn auch um unsere Applikation zu bauen, falls das nötig ist. Bei einer Go Applikation hätten wir also beispielsweise: ```dockerfile RUN go build <name> ``` ### Starten der Applikation Für das Starten gibt es auch zwei Möglichkeiten: #### ENTRYPOINT ```dockerfile ENTRYPOINT [ "node", "src/index.js" ] ``` `ENTRYPOINT` stellt immer den Einstiegsort der Applikation dar. Wenn ein Docker Container mit einem Image gestartet wird, werden alle Befehle aufbauend auf diesem Entrypoint ausgeführt. `docker run` ist so aufgebaut, dass nach dem Namen des Images als Parameter noch ein eigener Startup Befehl mitgegeben werden kann. Dieser Befehl wir jedoch immer an den Entrypoint angehänt. Der Entrypoint muss manuell über `--entrypoint` überschrieben werden. Wenn keiner gesetzt wurde ist der Entrypoint einfach `/bin/sh -c`. #### CMD ```dockerfile CMD [ "node", "src/index.js" ] ``` `CMD` repräsentiert den Befehl welcher an den Entrypoint angehängt wird. Es gilt also innerhalb der Dockerfile immer: `ENTRYPOINT` + `CMD` Die beiden Arrays werden einfach zusammengefügt. Jeglicher Inhalt von `CMD` wird jedoch ersetzt sobald ein Befehl als Argument mitgegeben wir. Man kann sich den `CMD` also als eine Art *default* vorstellen. Zusammengesetzt könnte unsere Dockerfile jetzt folgendermassen aussehen: ```dockerfile FROM node:18-alpine WORKDIR /app COPY . . RUN npm install CMD ["node", "src/index.js"] ``` ## Fazit Es gibt natürlich noch viele Weitere Befehle um den Bauprozess des Images noch genauer zu definieren. Für einfache applikationen reicht das aber meistens schon aus. Die verfügbaren Images können unter einem Registry wie [Docker Hub](https://hub.docker.com), Der [Microsoft Container Registry](https://mcr.microsoft.com) oder [vielen weiteren](https://gist.github.com/sufuf3/19018ed9429bb0247821883a876b10d9) angesehen werden.