= Create a Docker Image Thomas W. Stütz 2.0.0, 2023-04-08: How to use docker in project work @ HTL Leonding college ifndef::imagesdir[:imagesdir: images] //:toc-placement!: // prevents the generation of the doc at this position, so it can be printed afterwards :sourcedir: ../src/main/java :icons: font :sectnums: // Nummerierung der Überschriften / section numbering :sectnumlevels: 5 :toc: left :toclevels: 5 :docinfo: shared :source-highlighter: highlightjs :highlightjs-theme: github ++++ ++++ :experimental: true :linkattrs: // so window="_blank" will be executed :lang: de :author: Thomas W. Stütz :email: t.stuetz@htl-leonding.ac.at //Need this blank line after ifdef, don't know why... ifdef::backend-html5[] // https://fontawesome.com/v4.7.0/icons/ icon:file-text-o[link=https://raw.githubusercontent.com/htl-leonding-college/docker-lecture-notes/main/asciidocs/{docname}.adoc] ‏ ‏ ‎ icon:github-square[link=https://github.com/htl-leonding-college/docker-lecture-notes] ‏ ‏ ‎ icon:home[link=https://htl-leonding-college.github.io/docker-lecture-notes/] endif::backend-html5[] == Grundlagen === Warum eigene Docker-Images bauen? * Docker-Images können aus Docker-Registries gepullt werden ** https://hub.docker.com/ ** https://quay.io/search ** uvam * Oft ist es aber sinnvoll, ein eigenes Image zu bauen. * *Man kann so seine Applikation in einem Image verpacken, um sie überall lauffähig zu machen, ohne sich um die Umgebung kümmern zu müssen.* === Labs ==== Aufgabe: Hello World Image * Erstellen Sie ein verzeichnis `hello-world` und wechseln Sie in dieses Verzeichnis. * Erstellen Sie eine Datei `hello2.txt` mit dem Inhalt "Hello World!". * Erstellen Sie ein minimalistisches Dockerfile, das ein Basisimage (`ubuntu:latest`) verwendet. * + [source,dockerfile] ---- FROM ubuntu:latest RUN apt update RUN apt dist-upgrade -y RUN echo Hallo > greet.txt RUN ( . /etc/os-release ; echo $VERSION_ID ) RUN ls -lah WORKDIR app2 RUN ls -lah COPY *.txt . ---- + [source,bash] ---- docker build -t hello-world . docker run --rm -it hello-world ---- * Fragen: ** Warum wird dieser Container mit -it (interaktives Terminal) gestartet? + .Antwort [%collapsible] ==== Dieser Container würde ohne -it sofort wieder beendet werden, da kein Kommando ausgeführt wird, das den Container am Leben hält. Mit -it können wir in den Container hineinsehen und interagieren, um die Ausgabe der Befehle zu sehen und zu verstehen, was im Container passiert. ==== ** Wie kann man sich alle Images ansehen? ** Wie kann man sich alle Container ansehen? ** Wie kann man sich die Schichten (Layers) eines Images ansehen? + .Antwort [%collapsible] ==== ---- docker history hello-world ---- .result ---- IMAGE CREATED CREATED BY SIZE COMMENT 5cfde82af460 3 hours ago COPY *.txt . # buildkit 11B buildkit.dockerfile.v0 3 hours ago RUN /bin/sh -c ls -lah # buildkit 0B buildkit.dockerfile.v0 3 hours ago WORKDIR /app2 0B buildkit.dockerfile.v0 3 hours ago RUN /bin/sh -c ls -lah # buildkit 0B buildkit.dockerfile.v0 3 hours ago RUN /bin/sh -c ( . /etc/os-release ; echo $V… 0B buildkit.dockerfile.v0 3 hours ago RUN /bin/sh -c echo Hallo > greet.txt # buil… 6B buildkit.dockerfile.v0 3 hours ago RUN /bin/sh -c apt dist-upgrade -y # buildkit 33.2MB buildkit.dockerfile.v0 3 hours ago RUN /bin/sh -c apt update # buildkit 62.2MB buildkit.dockerfile.v0 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B 3 weeks ago /bin/sh -c #(nop) ADD file:6089c6bede9eca8ec… 101MB 3 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B 3 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B 3 weeks ago /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH 0B 3 weeks ago /bin/sh -c #(nop) ARG RELEASE 0B ---- ==== ** Wie kann man die Schichten eines Images reduzieren? + .Antwort [%collapsible] ==== Indem man mehrere RUN-Kommandos mit && verbindet, um so die Anzahl der Layer zu reduzieren. Es gibt auch andere Möglichkeiten, wie zB das Verwenden von *Multi-Stage Builds*, um unnötige Dateien und Schichten zu vermeiden. [source,dockerfile] ---- FROM ubuntu:latest RUN apt update && \ apt dist-upgrade -y && \ echo Hallo > greet.txt && \ ( . /etc/os-release ; echo Ubuntu $VERSION ) && \ ls -lah WORKDIR app2 RUN ls -lah COPY *.txt . ---- .result ---- IMAGE CREATED CREATED BY SIZE COMMENT 17b2acc6929b 4 seconds ago COPY *.txt . # buildkit 12B buildkit.dockerfile.v0 4 seconds ago RUN /bin/sh -c ls -lah # buildkit 0B buildkit.dockerfile.v0 4 seconds ago WORKDIR /app2 0B buildkit.dockerfile.v0 4 seconds ago RUN /bin/sh -c apt update && apt dist-up… 95.4MB buildkit.dockerfile.v0 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B 3 weeks ago /bin/sh -c #(nop) ADD file:6089c6bede9eca8ec… 101MB 3 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B 3 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B 3 weeks ago /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH 0B 3 weeks ago /bin/sh -c #(nop) ARG RELEASE 0B ---- ==== ==== Abschließende Bemerkungen * Uns fällt auf, dass die *Ausgaben von ls und echo nicht am Bildschirm ausgegeben werden*, sondern in den Layern des Images gespeichert werden. ** Das liegt daran, dass jedes RUN-Kommando in einem eigenen Layer ausgeführt wird und die Ausgabe dieses Kommandos in diesem Layer gespeichert wird. Wenn wir also mehrere RUN-Kommandos haben, werden die Ausgaben in den jeweiligen Layern gespeichert und nicht am Bildschirm ausgegeben. ** Abhilfe schafft hier die Verwendung von `--progress=plain --no-cache` beim Builden des Images, um die Ausgaben der RUN-Kommandos am Bildschirm zu sehen. + ---- docker build --no-cache \ --progress=plain \ -f Dockerfile2 \ -t hello-world . ---- ** Diese Ausgabe ist besonders hilfreich, um zu verstehen, was in den einzelnen Layern passiert, und um Fehler zu debuggen. * Auch sollte man die Caches löschen, die zB bei der Verwendung von `apt` entstehen, da diese sonst in den Layern gespeichert werden und das Image unnötig groß machen. Hierfür gibt es mehrere Möglichkeiten, zB `apt clean` oder `rm -rf /var/lib/apt/lists/*` am Ende eines RUN-Kommandos, um die Caches zu löschen. + .Die optimierte Version des Dockerfiles mit Cache-Bereinigung und einem CMD, damit der Container nicht sofort wieder beendet wird. [source,dockerfile] ---- FROM ubuntu:26.04 RUN apt update \ && apt dist-upgrade -y \ && echo Hallo > greet.txt \ && apt clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* WORKDIR /app2 COPY *.txt . CMD ["/bin/bash"] ---- * Auch wurde das Basisimage von `ubuntu:latest` auf `ubuntu:26.04` geändert, um eine festgelegte Version zu verwenden und so die Reproduzierbarkeit des Images zu gewährleisten. === Zusammenfassung * Man benötigt eine Textdatei mit dem Namen `Dockerfile` (ohne extension). Wird eine andere Datei verwendet, muss diese mit `-f` angegeben werden, zB `docker build -t hello-world -f Dockerfile.dev .` * Wie in einem Script kann eine Umgebung für das Programm gebaut werden. Dadurch wird das Image so konfiguriert, dass am Ende die Applikation im docker-Container laufen wird. * Es gibt hierfür mehrere Kommandos, die wir nun kennenlernen werden. [plantuml,build-image,svg,theme=sketchy-outline] ---- @startuml !theme materia cloud "Docker Registry" { artifact "Docker\nBasis-Image" as img } file Dockerfile as dockerfile artifact "Docker\nImage" as image node "Docker\nContainer" as container img -> dockerfile: FROM dockerfile -> image: docker build image -> container: docker run @enduml ---- * Jedes Kommando ist ein eigener Layer, daher werden RUN - Kommandos oft mit && verbunden, um die Anzahl der Layer zu reduzieren. -> https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#minimize-the-number-of-layers[Minimize the number of layers^] == Important Dockerfile Keywords //image::dockerfile-kommandos.png[] [cols="1,2", options="header"] |=== | Keyword | Meaning | ADD | Copies files to the file system of the image | CMD | Executes the specified command at container startup | COPY | Copies files from the project directory to the image | ENTRYPOINT | Always executes the specified command at container startup | ENV | Sets an environment variable | EXPOSE | Specifies the active ports of the container | FROM | Specifies the base image | LABEL | Sets a character string | RUN | Executes the specified command | USER | Specifies the account for RUN, CMD and ENTRYPOINT | VOLUME | Specifies volume directories |WORKDIR |Sets the working directory for RUN, CMD, COPY, etc. |=== === FROM * Zum Auswählen des 'Base Images* * Man kann hierfür ein Betriebssystem wählen zB u buntu, alpine oder bereits mit einer Applikation, wie zB nginx, mariadb, postgres, node, ... * gibt man keine docker-registry an, wird automatisch https://hub.docker.com verwendet * Die https://docs.docker.com/engine/reference/builder/#from[Syntax^] des FROM-Kommandos sieht wie folgt aus: + ---- FROM [--platform=] [:] [AS ] ---- .Mögliche Verwendung von FROM [source,dockerfile] ---- FROM debian:13-jdk FROM --platform=linux/arm64 debian:13-slim FROM debian:latest FROM --platform=linux/arm64 eclipse-temurin:20-jammy ---- INFO: openjdk ist bereits deprecated und kann nicht mehr verwendet werden === Labs ==== Aufgabe: Java-Programm ausführen . Erstelle folgendes File in einem neuen Folder: + .src/HelloWorld.java [source,java] ---- void main() { IO.println("Hello World!"); } ---- + IMPORTANT: Wir benutzen Java 25+ und keine historischen Java-Versionen. ** Ausführen in der Shell: + [source,shell] ---- java HelloWorld.java ---- oder + [source,shell] ---- javac HelloWorld.java java HelloWorld ---- . Erstellen eines Dockerfiles + [source,dockerfile] ---- FROM debian:latest # <.> RUN apt update && apt install -y openjdk-25-jdk # <.> COPY ./src . # <.> RUN java -version # <.> RUN javac HelloWorld.java # <.> CMD ["java", "HelloWorld"] # <.> ---- <.> Nur beim Entwickeln `:latest` verwenden <.> Installiert das JDK 25 <.> Kopiert alle Files from Ordner ./src in den aktuellen Ordner im Image (root) <.> Gibt die Java-Version aus (zur Ausgabe auf der Console verwenden wir `--no-cache --progress=plain`) <.> kompilieren des Java-Quellcodes <.> CMD ist der eine Befehl, weswegen der Container gestartet wird. . Bauen des Images + [source,shell] ---- docker build --no-cache --progress=plain -t hello-world . ---- . Starten des Containers + [source,shell] ---- docker run --name hello-world hello-world:latest ---- .Ansehen des gestoppten Containers [source,shell] ---- docker container ls -a ---- .Löschen des gestoppten Containers [source,shell] ---- docker container rm hello-world ---- ==== Verwendung eines kleineren Images * Es wird nur das image gewechselt -> eclipse-temurin:25-alpine, da dieses bereits das JDK 25 enthält und somit die Installation des JDKs im Dockerfile entfällt. Außerdem ist alpine ein sehr kleines Image, was die Größe des finalen Images reduziert. [source,dockerfile] ---- FROM eclipse-temurin:25-jdk-alpine-3.23 # <.> COPY ./src . CMD ["java", "HelloWorld.java"] # <.> ---- <.> Nun ist genau festgelegt ** welches JDK verwendet wird -> JDK 25 von https://adoptium.net/[Eclipse Temurin^] ** welches OS verwendet wird -> https://www.alpinelinux.org/about/[Alpine Linux 3.23^], welches sehr klein ist und somit die Größe des finalen Images reduziert <.> Man muss eine Einklassen-Java-Applikation nicht (mehr) kompilieren, um lauffähig zu sein. IMPORTANT: Das Bauen des Images ist sehr schnell, da nur der letzte Layer geändert wurde, da die vorherigen Layer gecached werden. Das ist ein großer Vorteil von Docker, da man so schnell Änderungen am Image vornehmen und testen kann, ohne jedes Mal das gesamte Image neu bauen zu müssen. .Bauen des Images [source,shell] ---- docker build -t hello-world . ---- .Starten des Containers [source,shell] ---- docker run --rm \ # <.> --name hello-world \ hello-world:latest ---- <.> rm ... remove -> der gestoppte Container wird automatisch gelöscht ==== Webseiten am nginx publishen * Aufgabe: ** Erstellen Sie ein docker image, das untenstehende Webseite mit nginx hostet. + .index.html [source,html] ---- HTL

My Personal Site

Lorem ipsum dolor sit amet, consectetur adipiscing elit

Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua

---- ** Nennen Sie das Image `my-webserver` mit der Version `v1.0`. ** Starten Sie den Webserver am Port 8080 .Lösung [%collapsible] ==== .Dockerfile [source,dockerfile] ---- FROM debian:trixie-slim LABEL maintainer="Max Mustermann " RUN apt update && RUN apt install -y nginx COPY index.html /var/www/html/index.html EXPOSE 80 ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"] ---- ---- # build the image docker build -t my-webserver:v1.0 . # start the container docker run --rm \ -p 8080:80 \ --name my-web my-webserver:v1.0 ---- * Man würde eher ein fertiges nginx-image verwenden, als es zu bauen. .Dockerfile [source,dockerfile] ---- FROM nginx:1.29-alpine3.23-slim LABEL maintainer="Max Mustermann " COPY index.html /var/www/html/index.html EXPOSE 80 ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"] ---- IMPORTANT: Beachte, dass geänderte html-Verzeichnis. siehe auch https://betterstack.com/community/questions/what-is-default-public-www-location-in-nginx/[Nginx Default Public Www Location?^] ==== === CMD * Jeder Docker Container führt nur EIN Kommando aus: ** CMD oder ** ENTRYPOINT * Alle .sh files die in `/docker-entrypoint.d` enthalten sind, werden automatisch beim Starten des Containers ausgeführt. [https://www.camptocamp.com/en/news-events/flexible-docker-entrypoints-scripts[source^]] .Dockerfile [source,dockerfile] ---- FROM ... CMD ["tail", "-f", "/dev/null"] ---- * Dieses Kommando verhindert, dass der Docker Container beendet wird. ---- # Beispiel für einen Shell-Befehl echo "Hello, World!" ---- === Labs ==== Aufgabe: CMD vs. ENTRYPOINT * Experimentieren Sie mit `CMD` und `ENTRYPOINT` und deren Zusammenspiel. Erstellen Sie ein Dockerfile, das die Unterschiede demonstriert. == Docker Build Context * `WORKDIR`: Festlegen des Arbeitsverzeichnisses * `COPY`: Kopieren von Dateien und Verzeichnissen * `ADD`: Erweiterte Kopierfunktionen (URLs, automatische Entpackung) * Der `.dockerignore`-File: Ausschließen von Dateien aus dem Build-Kontext image::docker-build-context.png[] === Labs ==== Quarkus/Python/Node.js Anwendung * Erstellen Sie ein Dockerfile, das eine kleine Quarkus-, Python- oder Node.js-Anwendung in das Image kopiert und ausführt. ==== COPY vs. ADD * Demonstrieren Sie den Unterschied zwischen `COPY` und `ADD` (z.B. mit TAR-Archiven oder URLs bei `ADD`). ==== .dockerignore * Fügen Sie eine `.dockerignore`-Datei hinzu, um bestimmte Dateien vom Build-Kontext auszuschließen (z.B. `.git`, `node_modules`, `target`). Zeigen Sie, dass diese Dateien nicht im Image landen. == Umgebungsvariablen und Ports * `ENV`: Definieren von Umgebungsvariablen * `EXPOSE`: Dokumentation von exposed Ports === Labs ==== Konfigurierbare Anwendung . Erstellen Sie ein java-Programm, das durch environment-Variablen konfiguriert wird: ** Je nachdem welchen Wert die ENV-Variable `APP_COLOR` hat, soll die Ausgabe in dieser Farbe erfolgen (zB rot, grün, gelb, blau). Wenn die Variable nicht gesetzt ist, soll die Ausgabe ohne Farbgebung erfolgen. + image::lab-1020-java-result.png[] + .Lösung [%collapsible] ==== .src/Main.java [source,java] ---- void main(String[] args) { // Direkter Zugriff auf die Docker-Umgebungsvariable String colorEnv = System.getenv("APP_COLOR"); String colorCode; if (colorEnv == null) { colorCode = "\u001B[0m"; // Reset } else { switch (colorEnv.toUpperCase()) { case "RED": colorCode = "\u001B[41m"; break; case "GREEN": colorCode = "\u001B[42m"; break; case "YELLOW": colorCode = "\u001B[43m"; break; case "BLUE": colorCode = "\u001B[44m"; break; default: colorCode = "\u001B[0m"; } } IO.println(colorCode + " " + "\u001B[0m"); IO.println(colorCode + " HALLO AUS DER JAVA-DOCKER-WELT! " + "\u001B[0m"); IO.println(colorCode + " Farbe aus ENV: " + (colorEnv != null ? colorEnv : "Standard") + " ".repeat(22-colorEnv.length()) + "\u001B[0m"); IO.println(colorCode + " " + "\u001B[0m"); } ---- oder .src/Main.java [source,java] ---- void main() { // Holen der Variable (Optional zur Sicherheit, falls null) String colorEnv = System.getenv("APP_COLOR"); // Der Switch als Expression: Kompakt, sicher und liest sich fast wie Prosa String colorCode = switch (colorEnv) { case null -> "\u001B[0m"; case String s when s.equalsIgnoreCase("RED") -> "\u001B[41m"; case String s when s.equalsIgnoreCase("GREEN") -> "\u001B[42m"; case String s when s.equalsIgnoreCase("YELLOW") -> "\u001B[43m"; case String s when s.equalsIgnoreCase("BLUE") -> "\u001B[44m"; default -> "\u001B[0m"; }; String displayEnv = (colorEnv == null) ? "Standard" : colorEnv; IO.println(""" %s \u001B[0m %s HALLO AUS DER JAVA-DOCKER-WELT! \u001B[0m %s Farbe aus ENV: %-20s \u001B[0m %s \u001B[0m """.formatted(colorCode, colorCode, colorCode, displayEnv, colorCode)); } ---- ==== . Erstellen sie ein Dockerfile, das dieses Java-Programm in ein Image packt und die Umgebungsvariable `APP_COLOR` auf "RED" setzt. + .Lösung [%collapsible] ==== .Dockerfile [source,dockerfile] ---- FROM eclipse-temurin:25-jdk-alpine-3.23 COPY src/Main.java /app/Main.java WORKDIR /app ENV APP_COLOR=BLUE CMD ["java", "Main.java"] ---- NOTE: Auch hier ersparen wir uns das Kompilieren, da es sich um eine Einklassen-Java-Applikation handelt. Bei komplexeren Java-Anwendungen würde man natürlich den Quellcode kompilieren und die .class-Dateien oder das .jar-File in das Image kopieren. ==== . Bauen Sie das Image + [source,shell] ---- docker build -t my-java-env-test:1.0 . ---- . Starten Sie den Container + [source,shell] ---- docker run --rm --name java-env-test my-java-env-test:1.0 ---- + image::lab-1020-java-result-2.png[] . Überschreiben Sie die Farbe beim Starten des Containers, indem Sie die Umgebungsvariable `APP_COLOR` auf "GREEN" setzen. + [source,shell] ---- docker run --rm \ --name java-env-test \ -e APP_COLOR=GREEN \ my-java-env-test:1.0 ---- + image::lab-1020-java-result-3.png[]