The Spring Boot Maven Plugin makes creating a Docker image from your application very easy! In this post, we give you some extra tips and examples for configuring Spring Boot to define the Java runtime used in such a Spring Boot Docker image and explain how to add additional environmental options to ease debugging.
For this post, I got the help of DaShaun Carter, Spring Developer Advocate, who immediately jumped in when I raised some questions about these configuration options. Thanks, DaShaun!
Spring Boot Petclinic Project
To illustrate our approach, we’ll use the Spring Boot Petclinic project, a Spring Boot application built using Maven or Gradle, to demonstrate how a Spring Boot project is set up fully and how the code is structured and must be packaged.
You can get the complete project, including a lot of configurations for various use cases, from GitHub:
$ git clone https://github.com/spring-projects/spring-petclinic
$ cd spring-petclinic
Modify and extend the default settings
We just need a few additional configurations in the spring-boot-maven-plugin
section in the pom.xml
file of the project to modify the OpenJDK distribution in the generated Docker image. Don’t change the executions; just insert the configuration section. The goal of this Docker image is to include all possible debug and test tools, so it has all the following options. Of course, which ones you use will depend on your use case.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<buildpacks>
<buildpack>paketobuildpacks/azul-zulu</buildpack>
<buildpack>paketobuildpacks/java</buildpack>
</buildpacks>
<env>
<BP_JVM_VERSION>17</BP_JVM_VERSION>
<BP_JVM_TYPE>JDK</BP_JVM_TYPE>
<BPE_DELIM_JAVA_TOOL_OPTIONS xml:space="preserve"> </BPE_DELIM_JAVA_TOOL_OPTIONS>
<BPE_APPEND_JAVA_TOOL_OPTIONS>-Xlog:gc:/tmp/gc.log</BPE_APPEND_JAVA_TOOL_OPTIONS>
<BPE_DEFAULT_BPL_DEBUG_ENABLED>true</BPL_DEBUG_ENABLED>
<BPE_DEFAULT_BPL_DEBUG_PORT>8000</BPL_DEBUG_PORT> <!-- This is the default value -->
<BPE_DEFAULT_BPL_JMX_ENABLED>true</BPL_JMX_ENABLED>
<BPE_DEFAULT_BPL_JMX_PORT>5000</BPL_JMX_PORT> <!-- This is the default value -->
<BPE_DEFAULT_BPL_JAVA_NMT_ENABLED>true</BPL_JAVA_NMT_ENABLED> <!-- This is the default value -->
<BPE_DEFAULT_BPL_JFR_ENABLED>true</BPL_JFR_ENABLED>
<BPE_DEFAULT_BPL_JFR_ARGS>dumponexit=true,filename=/tmp/rec.jfr,duration=600s</BPL_JFR_ARGS>
</env>
</image>
</configuration>
<executions>
...
</executions>
</plugin>
To better understand this section, we need to look at all the different options separately:
- The
<buildpacks>
section defines which buildpacks are used to generate the Docker image.- On github.com/paketo-buildpacks/azul-zulu you can see a list of available configurations.
- The
<env>
section groups all environment-related changes, with different targets described on the Paketo Docs > How To Configure Paketo Buildpacks > Environment Variables.- BP_: variables used to configure the build process itself, e.g. setting the Java version.
- BPE_: image embedded environment variables.
- BPL_: runtime features of the app image, which are set as environment variables in the app container. You must append them with
BPE_DEFAULT_
to “bake” them into the container image to automatically use them.
Building the Docker image
At this point, building the Docker image is a single-line command, and the output will show a few references to the Azul JDK and our settings after the various test cycles:
$ ./mvnw spring-boot:build-image
[INFO] Scanning for projects...
[INFO]
[INFO] ------------< org.springframework.samples:spring-petclinic >------------
[INFO] Building petclinic 3.1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...
[INFO] --- spring-boot:3.1.1:build-image (default-cli) @ spring-petclinic ---
[INFO] Building image 'docker.io/library/spring-petclinic:3.1.0-SNAPSHOT'
[INFO]
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:base' 100%
...
[INFO] > Pulling buildpack image 'docker.io/paketobuildpacks/azul-zulu:latest' 100%
[INFO] > Pulled buildpack image 'paketobuildpacks/azul-zulu@sha256:79419af00c95f85c088e68808f61b2486c39a7e12a0033995970c97e95408069'
...
[INFO] > Running creator
[INFO] [creator] ===> ANALYZING
[INFO] [creator] Image with name "docker.io/library/spring-petclinic:3.1.0-SNAPSHOT" not found
[INFO] [creator] ===> DETECTING
[INFO] [creator] 8 of 27 buildpacks participating
[INFO] [creator] paketo-buildpacks/azul-zulu 10.1.5
...
[INFO] [creator] ===> BUILDING
[INFO] [creator]
[INFO] [creator] Paketo Buildpack for Azul Zulu 10.1.5
[INFO] [creator] https://github.com/paketo-buildpacks/azul-zulu
[INFO] [creator] Build Configuration:
[INFO] [creator] $BP_JVM_JLINK_ARGS --no-man-pages --no-header-files --strip-debug --compress=1 configure custom link arguments (--output must be omitted)
[INFO] [creator] $BP_JVM_JLINK_ENABLED false enables running jlink tool to generate custom JRE
[INFO] [creator] $BP_JVM_TYPE JDK the JVM type - JDK or JRE
[INFO] [creator] $BP_JVM_VERSION 17 the Java version
[INFO] [creator] Launch Configuration:
[INFO] [creator] $BPL_DEBUG_ENABLED true enables Java remote debugging support
[INFO] [creator] $BPL_DEBUG_PORT 8000 configure the remote debugging port
[INFO] [creator] $BPL_DEBUG_SUSPEND false configure whether to suspend execution until a debugger has attached
[INFO] [creator] $BPL_HEAP_DUMP_PATH write heap dumps on error to this path
[INFO] [creator] $BPL_JAVA_NMT_ENABLED true enables Java Native Memory Tracking (NMT)
[INFO] [creator] $BPL_JAVA_NMT_LEVEL summary configure level of NMT, summary or detail
[INFO] [creator] $BPL_JFR_ARGS dumponexit=true,filename=/tmp/rec.jfr,duration=600s configure custom Java Flight Recording (JFR) arguments
[INFO] [creator] $BPL_JFR_ENABLED true enables Java Flight Recording (JFR)
[INFO] [creator] $BPL_JMX_ENABLED true enables Java Management Extensions (JMX)
[INFO] [creator] $BPL_JMX_PORT 5000 configure the JMX port
[INFO] [creator] $BPL_JVM_HEAD_ROOM 0 the headroom in memory calculation
[INFO] [creator] $BPL_JVM_LOADED_CLASS_COUNT 35% of classes the number of loaded classes in memory calculation
[INFO] [creator] $BPL_JVM_THREAD_COUNT 250 the number of threads in memory calculation
[INFO] [creator] $JAVA_TOOL_OPTIONS the JVM launch flags
[INFO] [creator] Using Java version 17 from BP_JVM_VERSION
[INFO] [creator] A JDK was specifically requested by the user, however a JRE is available. Using a JDK at runtime has security implications.
[INFO] [creator] Azul Zulu JDK 17.0.7: Contributing to layer
...
[INFO] Successfully built image 'docker.io/library/spring-petclinic:3.1.0-SNAPSHOT'
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:29 min
[INFO] Finished at: 2023-07-03T11:26:39+02:00
[INFO] ------------------------------------------------------------------------
Checking the created Docker images
During the build process, various images were downloaded and created. As you can see, there are more than we may expect, but a few of these are used during the unit tests.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
paketobuildpacks/run base-cnb f2e5000af0cb 3 days ago 87.1MB
postgres 15.3 1921dda0e2c5 2 weeks ago 412MB
mysql 5.7 2be84dd575ee 2 weeks ago 569MB
testcontainers/ryuk 0.5.1 ec913eeff75a 6 weeks ago 12.7MB
paketobuildpacks/builder base 99ec7fb86b9d 43 years ago 1.34GB
paketobuildpacks/azul-zulu latest 276db25e20db 43 years ago 10.4MB
paketobuildpacks/java latest 2ddc6cc7d346 43 years ago 207MB
spring-petclinic 3.1.0-SNAPSHOT c05d70c78109 43 years ago 496MB
Running the Docker image
You can now start the Docker image. Three ports are configured for various use cases:
- 8080: to access the website
- 8000: debug port
- 5000: JMX port
Via the terminal, start the Docker image.
$ docker run -p 8080:8080 -p 8000:8000 -p 5000:5000 --name petclinic spring-petclinic:3.1.0-SNAPSHOT
Setting Active Processor Count to 16
Debugging enabled on port *:8000
Enabling Java Flight Recorder with args: dumponexit=true,filename=/tmp/rec.jfr,duration=600s
JMX enabled on port 5000
...
[0.349s][info][jfr,startup] Started recording 1. The result will be written to:
[0.349s][info][jfr,startup]
[0.349s][info][jfr,startup] /tmp/rec.jfr
|\ _,,,--,,_
/,`.-'`' ._ \-;;,_
_______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______
| | '---''(_/._)-'(_\_) | | | | | | | | |
| _ | ___|_ _| | | | | |_| | | | __ _ _
| |_| | |___ | | | | | | | | | | \ \ \ \
| ___| ___| | | | _| |___| | _ | | _| \ \ \ \
| | | |___ | | | |_| | | | | | | |_ ) ) ) )
|___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / /
==================================================================/_/_/_/
:: Built with Spring Boot :: 3.1.1
2023-07-06T10:55:23.329Z INFO 1 --- [ main] o.s.s.petclinic.PetClinicApplication : Starting PetClinicApplication v3.1.0-SNAPSHOT using Java 17.0.7 with PID 1 (/workspace/BOOT-INF/classes started by cnb in /workspace)
...
Validating the Docker image
At this point, you can perform various steps to validate the Docker image we created with the additional settings:
- Open a browser and point it to
localhost:8080
. The Petclinic web interface allows you to browse pet owners and veterinarians. - Open a second terminal and perform the following checks inside the running Docker:
$ docker exec -it petclinic sh
# Check the Java version
$ /layers/paketo-buildpacks_azul-zulu/jdk/bin/java -version
openjdk version "17.0.7" 2023-04-18 LTS
OpenJDK Runtime Environment Zulu17.42+19-CA (build 17.0.7+7-LTS)
OpenJDK 64-Bit Server VM Zulu17.42+19-CA (build 17.0.7+7-LTS, mixed mode, sharing)
# Check if we can execute JCMD which is not included in the default build with a JRE
$ /layers/paketo-buildpacks_azul-zulu/jdk/bin/jcmd
1 org.springframework.boot.loader.JarLauncher
181 jdk.jcmd/sun.tools.jcmd.JCmd
# Check the generated files we can use later for further investigation
# The application must be running for a longer time, before data is saved in rec.jfr
$ ls -l /tmp
total 20
drwxr-xr-x 2 cnb cnb 4096 Jul 10 06:44 2023_07_10_06_44_57_1
-rw-r--r-- 1 cnb cnb 1819 Jul 10 06:45 gc.log
drwxr-xr-x 2 cnb cnb 4096 Jul 10 06:44 hsperfdata_cnb
-rw-r--r-- 1 cnb cnb 0 Jul 10 06:44 rec.jfr
drwx------ 2 cnb cnb 4096 Jul 10 06:44 tomcat-docbase.8080.16382390067193381204
drwx------ 3 cnb cnb 4096 Jul 10 06:44 tomcat.8080.7527925084169386730
# Check the Java info, using Process ID 1 we found with jcmd
$ /layers/paketo-buildpacks_azul-zulu/jdk/bin/jinfo 1
Java System Properties:
#Mon Jul 10 06:46:46 UTC 2023
com.sun.management.jmxremote.rmi.port=5000
java.specification.version=17
sun.jnu.encoding=ANSI_X3.4-1968
com.sun.management.jmxremote.authenticate=false
java.class.path=/workspace
java.vm.vendor=Azul Systems, Inc.
...
java.library.path=/layers/paketo-buildpacks_azul-zulu/jdk/lib\:/usr/java/packages/lib\:/usr/lib64\:/lib64\:/lib\:/usr/lib
...
VM Flags:
-XX:ActiveProcessorCount=16 -XX:CICompilerCount=12 -XX:CompressedClassSpaceSize=117440512 -XX:ConcGCThreads=3 -XX:+ExitOnOutOfMemoryError -XX:+FlightRecorder -XX:G1ConcRefinementThreads=13 -XX:G1EagerReclaimRemSetThreshold=128 -XX:G1HeapRegionSize=16777216 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=536870912 -XX:+ManagementServer -XX:MarkStackSize=4194304 -XX:MaxDirectMemorySize=10485760 -XX:MaxHeapSize=23857201152 -XX:MaxMetaspaceSize=133729280 -XX:MaxNewSize=14310965248 -XX:MinHeapDeltaBytes=16777216 -XX:MinHeapSize=16777216 -XX:NativeMemoryTracking=summary -XX:NonNMethodCodeHeapSize=7602480 -XX:NonProfiledCodeHeapSize=122027880 -XX:+PrintNMTStatistics -XX:ProfiledCodeHeapSize=122027880 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=23857201152 -XX:StartFlightRecording=dumponexit=true,filename=/tmp/rec.jfr,duration=600s -XX:ThreadStackSize=1024 -XX:+UnlockDiagnosticVMOptions -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC
VM Arguments:
jvm_args: -Djava.security.properties=/layers/paketo-buildpacks_azul-zulu/java-security-properties/java-security.properties -XX:+ExitOnOutOfMemoryError -Xlog:gc:/tmp/gc.log -XX:ActiveProcessorCount=16 -agentlib:jdwp=transport=dt_socket,server=y,address=*:8000,suspend=n -XX:StartFlightRecording=dumponexit=true,filename=/tmp/rec.jfr,duration=600s -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=5000 -Dcom.sun.management.jmxremote.rmi.port=5000 -XX:MaxDirectMemorySize=10M -Xmx23297904K -XX:MaxMetaspaceSize=130595K -XX:ReservedCodeCacheSize=240M -Xss1M -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -Dorg.springframework.cloud.bindings.boot.enable=true
java_command: org.springframework.boot.loader.JarLauncher
java_class_path (initial): /workspace
Launcher Type: SUN_STANDARD
Making use of the logging and debug configurations
Analyzing and debugging the application inside Docker with the various tools and configurations is beyond the scope of this article. But we can validate all of them quickly to confirm their correct configuration, and how you can use them.
Getting files out of the Docker
You can copy both the JFR recording and the Garbage Collector files inside the Docker image to your PC.
$ docker cp petclinic:/tmp/rec.jfr rec.jfr
$ docker cp petclinic:/tmp/gc.log gc.log
Connecting to debug
From within your IDE, for instance, IntelliJ IDEA, you can start a debug connection to the running application inside the Docker via port 8000 and set breakpoints in your code.
Connected to the target VM, address: 'localhost:8000', transport: 'socket'
Connecting to JMX
The JMX connection configured on port 5000 allows you to connect to the running application inside the Docker with VisualVM or Azul Mission Control. For instance, you can start a JFR recording for a given duration and get the results immediately visualized.
Click on any of the three images below for a larger image.
Conclusion
This post’s extended pom.xml
configuration includes various ways to enable remote debugging and generate log files. You should never use all of them simultaneously, and definitely not in production! But the goal is to determine if the default Java distribution in the Spring Boot Docker can be replaced and extended with additional settings.
And… mission accomplished!
Read more
- Video created by DaShaun Carter
- Paketo docs:
- Spring docs:
- GitHub:
- StackOverflow:
We Love to Talk About Java
We’re here to answer any questions about Azul products, Java, pricing, or anything else.