So führt man eine Java-Anwendung als Docker-Container aus

Dieser Post beschreibt, wie eine Java-Anwendung in einem Docker-Container ausgeführt werden kann. Ich verwende eine Spring-Boot-App mit Tomcat als Beispiel, aber funktioniert auch mit anderen ausführbaren JAR-Dateien. Ich verwende docker-compose für die Containerkonfiguration. Alles wird mittels eines Maven-Profils zusammengestellt, so dass es einfach als Container gestartet werden kann.

Die Applikation und Voraussetzungen

Ich habe eines meiner Github-Projekte als Basis-App verwendet. Das Projekt ist eine einfache Java-Webanwendung, die Registrierungs- und Anmeldefunktionen bietet. Der Source ist hier verfügbar: https://github.com/benni-wdev/user-webapp.

Der Beitrag erklärt nicht, wie Docker und Docker-Compose installiert werden.

Adding directories for docker files to java project

Die folgenden Verzeichnisse und Dateien wurden dem Projekt hinzugefügt:

src pic: Docker-Verzeichnis unter src/main hinzugefügt

Auf der Target Seite (nach dem install) sieht es so aus:

target pic: Nach der mvn-Installation muss die JAR-Datei im Docker-Verzeichnis abgelegt werden

The Dockerfile inside the java project

Das Dockerfile enthält die Definition des Containers selbst. Der Dateiinhalt sieht wie folgt aus:

FROM openjdk:8
ADD jar/user-webapp.jar user-webapp.jar
EXPOSE 8080 8443
ENTRYPOINT exec java $JAVA_OPTS -jar user-webapp.jar

Erläuterungen:

  1. Zeile 1: FROM openjdk:8 - this is the image our java app image is based on. Since the project is a java 8 project we take the openjdk:8 as base image. In the same way you can use other versions as base image (e.g. FROM openjdk:17 for java 17).
  2. Zeile 2: ADD jar/user-webapp.jar user-webapp.jar - This copies the jar file into our container. The path is relative to the Dockerfile. So when the container is built the jar must be placed in a subfolder called jar. Also have a look at the target pic above (how this is done automatically with pom.xml definition follows later).
  3. Zeile 3: EXPOSE 8080 8443 - Expose the ports the java app listens on to the outside of the container.
  4. Zeile 4: ENTRYPOINT exec java $JAVA_OPTS -jar user-webapp.jar - Finally start the container. An additional environment variable $JAVA_OPTS is used here. This is used to pass arbitrary additional jvm parameters from outside. The definition will be done in the docker-compose file.

Die docker-compose.yml

Docker Compose kann verwendet werden, um verschiedene Container zusammen zu konfigurieren und Docker Artefakte wie Netzwerk zu teilen. Dies ist nützlich, wenn mehrere Container Abhängigkeiten haben. Für weitere Informationen zu docker compose siehe https://docs.docker.com/get-started/08_using_compose/. Ich verwende es hier nur, weil die Containerkonfiguration innerhalb einer docker-compose.yml etwas übersichtlicher aussieht. Der Dateiinhalt sieht wie folgt aus:

version: '3'
services:
  web:
    build: webapp
    container_name: webapp
    environment:
      JAVA_OPTS: "-XX:+UseContainerSupport -XX:+UseG1GC -XX:+PrintGC -Xms1024m -Xmx1024m"
    ports:
      - "8443:8443"
      - "8080:8080"
    volumes:
      - ./webapp/log:/log
      - ./webapp/keystore:/keystore
  • Zeile 1: Die erste Zeile beschrieb nur die Docker-Compose-Version, die in der Datei verwendet wird (weitere Informationen zu Versionen gibts hier https://docs.docker.com/compose/compose-file/compose-versioning/).
  • Zeile 2: Services-Abschnitt enthält die verschiedenen Container-Definitionen
  • Zeile 3: Der Name des Containers in der docker-compose-Definition
  • Zeile 4: Wir wollen das Image für diesen Container erstellen. Es muss ein Verzeichnis auf derselben Ebene wie die yaml-Datei mit diesem Namen geben, das die entsprechende Docker-Datei für das zu erstellende Image enthält. Eine Alternative wäre, hier einen Image Names für ein vorgefertigtes Image anzugeben.
  • Zeile 5: Der Name des Containers, wenn er gestartet wird.
  • Zeile 6: Das Tag definiert den Start der Umgebungsvariablen Definitionen.
  • Zeile 7: Hier ist die Definition des JAVA_OPTS, das wir in unserem Dockerfile verwendet haben. Ich möchte JVM Container Support, G1 Garbage Collection (einschließlich einiger Logs) und 1 GB Heap-Speicher verwenden.
  • Zeile 8: Tag für den Beginn der Portdefinition.
  • Zeile 9: 8443 auf dem Container sollte nach außen als 8443 verfügbar gemacht werden (hier kann man beispielsweise den internen Port 8443 auch auf die Standard-443 für https abbilden).
  • Zeile 10: Dasselbe für http mit 8080
  • Zeile 11: Tag für den Beginn der Volume-Definition
  • Zeile 12: Die Log-Dateien, die die Anwendung schreibt, sollten von außen sichtbar sein, daher wird das Verzeichnis /webapp/log innerhalb des Containers auf /log gemappt. Andernfalls müssten wir uns immer am Container anmelden, um sie zu überprüfen. Außerdem würden sie beim Neustart des Containers verloren gehen.
  • Zeile 13: Auch der Keystore für die https-Verbindung (und Token Signing) wird von außen gemappt. Hier müssen die p12-Dateien abgelegt werden, die vom Springboot-Tomcat verwendet werden.

New configuration files in Java project

Innerhalb des Verzeichnisses main/resources habe ich ein neues Verzeichnis Docker erstellt, das eine neue Version von application.properties und config.properties enthält. Diese Konfigurationen werden verwendet, wenn das neue Maven-Profil für Docker-Build verwendet wird. Die einzige wichtige Änderung hier ist, dass sich der Parameter server.ssl.key-store auf das neu definierte Keystore-Volume ändert, das so angegeben werden sollte (wie bei der anderen Konfiguration müssen hier alle Details für den Keystore geändert werden):

server.ssl.key-store=file:/keystore/yourkeystore.p12

The maven pom.xml

Um das Standard-Maven-Verhalten nicht zu berühren, habe ich ein anderes Maven-Profil erstellt. Weitere Informationen zu Maven-Profilen: https://www.baeldung.com/maven-profiles.

Die vorhandene Build-Konfiguration wird unter einem neuen Profil mit dem Namen local beibehalten:

...
</dependencies>
<profiles>
    <profile>
        <id>local</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <build>
            <finalName>user-webapp</finalName>
...

Und ein neues Profil namens Docker wird erstellt.

...
<profile>
    <id>docker</id>
    <build>
        <finalName>user-webapp</finalName>
        <plugins>
            ...

lle Plugins und Definitionen aus dem anderen Profil folgen jetzt, aber wir brauchen ein neues Plugin, um unsere Docker-Definitionen und das gebaute JAR zu kopieren. Dafür wird das Maven-Ressource-Plugin wie folgt verwendet.

.....   
<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.0.2</version>
    <executions>
        <execution>
            <id>copy-docker-compose</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${basedir}/target/docker</outputDirectory>
                <resources>
                    <resource>
                        <directory>src/main/docker</directory>
                        <includes>
                            <include>**/*.*</include>
                        </includes>
                    </resource>
                </resources>
            </configuration>
        </execution>
        <execution>
            <id>copy-docker-webapp</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${basedir}/target/docker/webapp</outputDirectory>
                <resources>
                    <resource>
                        <directory>src/main/docker/webapp</directory>
                        <includes>
                            <include>**/*</include>
                        </includes>
                    </resource>
                </resources>
            </configuration>
        </execution>
        <execution>
            <id>copy-docker-webapp-keystore</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${basedir}/target/docker/webapp/keystore</outputDirectory>
                <resources>
                    <resource>
                        <directory>.keystore</directory>
                        <includes>
                            <include>**/*.*</include>
                        </includes>
                    </resource>
                </resources>
            </configuration>
        </execution>
        <execution>
            <id>copy-webapp-jar</id>
            <phase>package</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${basedir}/target/docker/webapp/jar</outputDirectory>
                <resources>
                    <resource>
                        <directory>${basedir}/target</directory>
                        <includes>
                            <include>**/*.jar</include>
                        </includes>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>
</plugins>
....

Es sind insgesamt vier Executions definiert. Drei passieren in der Generate-Sources-Phase, während der letzte in der Package-Phase passieren muss.

  1. copy-docker-compose: kopiert die docker-compose.yml und die zusätzlichen Skripte (unten erklärt) in einen neuen Ordner im Zielverzeichnis ${basedir}/target/docker.
  2. copy-docker-webapp: kopiert den Unterordner webapp inklusive Dockerfile in denselben target ordner.
  3. copy-docker-webapp-keystore: kopiert die Keystore-Datei aus einem Verzeichnis .keystore in das Keystore-Verzeichnis in ${basedir}/target/docker/webapp/. Man sollte niemals den keystore file in einem Code-Repository speichern.
  4. copy-webapp-jar: kopiert die erstellte JAR-Datei von ${basedir}/target in ${basedir}/target/docker/webapp/jar, sodass sie während der Image-Erstellung verwendet werden kann. Der Schritt muss in der Paketphase ausgeführt werden, damit immer das neueste jar aufgenommen wird (oder das überhaupt eins da ist, falls vorher clean ausgeführt wird).

Schließlich benötigt der Ressourcenabschnitt eine kleine Erweiterung, damit die neuen Konfigurationen im Docker-Ressourcenordner von diesem Build-Profil aufgenommen werden.

...                  
<resource>
    <directory>src/main/resources/docker</directory>
    <includes>
        <include>**/*.*</include>
    </includes>
</resource>
...

Statt src/main/resources/dev wird nun src/main/resources/docker verwendet.

Run the java app in a container

Nachdem der Maven-Build mit dem Docker-Profil abgeschlossen ist (z. B. mvn clean install -Pdocker ausführen), sollten die folgenden Dateien hier sichtbar sein.

~/projects/user-webapp/target/docker $ ls -R
.:
activate.sh  docker-compose.yml  shellWebApp.sh  webapp
./webapp:
Dockerfile  jar  keystore  log
./webapp/jar:
user-webapp.jar
./webapp/keystore:
yourkeystore.p12

Um die Anwendung jetzt in einem Container zu starten, muss man zuerst docker-compose build ausführen.

~/projects/user-webapp/target/docker$ docker-compose build
Building web
Step 1/4 : FROM openjdk:8
 ---> b273004037cc
Step 2/4 : ADD jar/user-webapp.jar user-webapp.jar
 ---> Using cache
 ---> c189b7304cc0
Step 3/4 : EXPOSE 8080 8443
 ---> Using cache
 ---> b2b233dd6b29
Step 4/4 : ENTRYPOINT exec java $JAVA_OPTS -jar user-webapp.jar
 ---> Using cache
 ---> af3a12edd1cd
Successfully built af3a12edd1cd
Successfully tagged docker_web:latest

Und dann docker-compose up.

~/projects/user-webapp/target/docker$ docker-compose up
Starting webapp ... done
Attaching to webapp
webapp | [GC pause (G1 Evacuation Pause) (young) 52757K->5312K(1024M), 0.0088325 secs]
webapp | 
webapp |   .   ____          _            __ _ _
webapp |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
webapp | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
webapp |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
webapp |   '  |____| .__|_| |_|_| |_\__, | / / / /
webapp |  =========|_|==============|___/=/_/_/_/
webapp |  :: Spring Boot ::                (v2.5.4)
webapp | 
webapp | 2023-02-16 20:12:45.833  INFO 1 --- [           main] c.wwt.webapp.userwebapp.WebApplication   : Starting WebApplication v0.1 using Java 1.8.0_342 on 8961b007deb7 with PID 1 (/user-webapp.jar started by root in /)
webapp | 2023-02-16 20:12:45.836  INFO 1 --- [           main] c.wwt.webapp.userwebapp.WebApplication   : The following profiles are active: default
webapp | [GC pause (G1 Evacuation Pause) (young) 64704K->9500K(1024M), 0.0137983 secs]

Die App jetzt unter https://localhost:8443/ erreichbar. Ich habe dem Repository zwei zusätzliche Shell-Skripte hinzugefügt. Die erste activate.sh enthält

#!/bin/bash
docker-compose down
docker-compose build
docker-compose up -d

Dies führt den Build und den Start in einem Skript aus (plus einen laufenden Container herunterfahren, falls vorhanden). Zusätzlich trennt es die laufende Anwendung von der ausführenden Shell mit der Option -d (die logs werden dadurch nicht mehr in der shell angezeigt). Ein weiteres Skript namens shellWebapp.sh enthält

#!/bin/bash
sudo docker exec -i -t webapp /bin/bash

Dies ermöglicht es, sich per Shell in den laufenden Container einzuloggen, was manchmal in Fehlersituationen nützlich sein kann. Es würde so aussehen:

root@8961b007deb7:/# dir
bin   dev  home      lib    log    mnt	proc  run   srv  tmp		  usr
boot  etc  keystore  lib64  media  opt	root  sbin  sys  user-webapp.jar  var
root@8961b007deb7:/# 

Hier kann man mit einer Shell innerhalb des Container Linux arbeiten.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert