Author: Brian Vermeer
Containers bring new flexibility and agility to software development and deployment. However, they also introduce a new attack surface that malicious actors can exploit. A compromised container can give an attacker access to other containers and even the host system. Smaller images that contain fewer artifacts are already a great help in achieving a smaller attack surface.
In this blog post, we'll present an in-depth exploration of utilizing JLink to optimize Docker image sizes, enhancing application security and performance. We'll showcase how to use JLink and integrate it with Docker to efficiently deploy your Spring Boot or general Java applications.
Introduction to JLink
Java is one of the most used programming languages for enterprise application development globally. However, developers often struggle with the size of the Docker images when deploying Java applications in Docker containers. One of the ways to solve this problem is to use JLink, a tool introduced in JDK 9.
JLink (Java Linker) is a command-line tool that assembles and optimizes a set of modules and their dependencies into a custom runtime image. This essentially means it creates a minimal Java runtime environment with only the necessary modules required by your application.
$ jlink --module-path $JAVA_HOME/jmods:mlib --add-modules my.module --output myRunTime
In the above command, my.module
is your module, and myRuntime
is the custom runtime image that JLink will create.
The role of JLink in creating smaller Docker images
When creating Docker images for Java applications, the size of the image is often a concern — particularly for Spring Boot applications, which come with many dependencies. A large Docker image can lead to longer startup times, increased storage costs, and slower deployment processes.
In legacy versions of Java, the Java Development Kit (JDK) came with a Java Runtime Environment (JRE). Only the JRE was needed to run the created Java artifact. Therefore, in the past, it was common to use the JRE in your Docker image or choose a JRE base image for your containers. Newer versions of Java don't always come with a JRE, although some vendors might still create a JRE and corresponding base images. You can use these or a more specific Java runtime tailored to your application.
JLink enables you to create a minimal Java runtime with only the necessary modules. By doing so, it significantly reduces the size of your Docker image. For example, a standard Java runtime environment might be over 200 MB, but with JLink, you can bring it down to less than 50 MB.
Using JLink for a Spring Boot Java application
Spring Boot creates a fat JAR for your applications containing all the dependencies. In addition, many Spring Boot applications lack a module declaration. This does not have to be a problem, but we need to determine which modules the application needs and all its dependencies.
Using Jdeps to find the module
Jdeps is a Java tool that shows the package-level or class-level dependencies. The tool, introduced in Java 8, can be used to understand an application's dependencies, which can then be used to create a custom runtime image using JLink.
When ensuring that all our dependencies are located in one directory, we can use jdeps to print a summary of the dependencies.
jdeps -cp 'mydeps/lib/*' -recursive --multi-release 17 -s target/MyJar.jar
Similarly, we can use jdeps to print all module dependencies recursively for the Spring Boot application and the dependencies.
jdeps --ignore-missing-deps -q --recursive --multi-release 17 --print-module-deps --class-path 'mydeps/lib/*' target/MyJar.jar
The output generated by jdeps enables JLink to create a Java Runtime that only contains the modules needed for this Spring-Boot application.
Output Spring Boot dependencies into a folder
As mentioned, Spring Boot creates a fat JAR that includes all dependencies. However, the dependencies are packed in a particular way inside the JAR and, therefore, are not easy to access by jdeps. There are two simple solutions to get the dependencies with jdeps.
-
Unpack the fat JAR file created by Spring Boot.
- This option works great if you already have the build artifact created and are not willing or able to rebuild the application. The dependencies will be unpacked into
/BOOT/libs/
.
- This option works great if you already have the build artifact created and are not willing or able to rebuild the application. The dependencies will be unpacked into
-
Use a plugin in your build tool that copies the dependencies to a specific folder.
- In Maven, this can, for instance, be achieved by using the
maven-dependency-plugin
. In the example below, the dependencies are copied to the/target/dependency
folder after Maven finishes the package phase.
- In Maven, this can, for instance, be achieved by using the
<project>
<!-- ... other configurations ... -->
<build>
<plugins>
<!-- Add the maven-dependency-plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!-- Configure the output directory for the dependencies -->
<outputDirectory>${project.build.directory}/dependency</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- ... other configurations ... -->
</project>
Building a Docker image with a custom Java Runtime
So now, let's combine jdeps and JLink to build a custom Java Runtime. With this runtime, we can create a perfect, minimal Docker image specifically for a Spring Boot application.
FROM maven:3-eclipse-temurin-17 as build
RUN mkdir /usr/src/project
COPY . /usr/src/project
WORKDIR /usr/src/project
RUN mvn package -DskipTests
RUN jar xf target/JavaCoffeeShop.jar
RUN jdeps --ignore-missing-deps -q \
--recursive \
--multi-release 17 \
--print-module-deps \
--class-path 'BOOT-INF/lib/*' \
target/JavaCoffeeShop.jar > deps.info
RUN jlink \
--add-modules $(cat deps.info) \
--strip-debug \
--compress 2 \
--no-header-files \
--no-man-pages \
--output /myjre
FROM debian:bookworm-slim
ENV JAVA_HOME /user/java/jdk17
ENV PATH $JAVA_HOME/bin:$PATH
COPY --from=build /myjre $JAVA_HOME
RUN mkdir /project
COPY --from=build /usr/src/project/target/JavaCoffeeShop.jar /project/
WORKDIR /project
ENTRYPOINT java -jar JavaCoffeeShop.jar
In the example above, I utilized a multistage Docker build. The initial building stage is based on an eclipse-temurin
JDK 17 image containing Maven. This stage is used to:
- Create the Java artifact. Using Maven, I create the fat executable JAR file that contains the complete application.
-
Unpack the JAR file to have all the dependencies. This is only needed if you don’t use the
maven-dependency-plugin
as described earlier. If you included it, you can skip this step -
Use jdeps to get the necessary modules. Point to the file containing all the dependency JAR files and the final artifact, and save the list in
deps.info
. -
Run JLink to create a custom Java Runtime. Using the
deps.info
as input and storing it in/myjre
. We only add the modules needed to JLink and remove debug info, manual pages, and header files.
The second and final stage builds the production image based on a debian:stable-slim
image.
-
Set environment variables. Set the
JAVA_HOME
to the path I’ll copymyjre
to, and addJAVA_HOME
to thePATH
. -
Copy the Java Runtime created by JLink. Reference the first stage and copy the custom Java Runtime to the location defined as
JAVA_HOME
. - Copy the created Java artifact. The created fat executable Spring Boot JAR is copied to the dedicated project directory.
- Set Entrypoint
JLink offers several advantages when it comes to creating Docker images for Spring Boot Java applications:
Reduced Image Size: As mentioned earlier, JLink can help reduce the size of your Docker image, leading to faster deployment and reduced storage costs.
Faster Startup Times: A smaller Docker image means that your application can start up faster, which is crucial for applications that need to scale quickly.
Security: By including only the necessary modules, you reduce the attack surface of your application. Fewer modules mean fewer potential security vulnerabilities.
Speaking of security, it's essential to mention the role of Snyk in ensuring the security of your applications. Snyk is a developer security tool that can scan your source code, open source packages, container images, and cloud configurations for vulnerabilities. With Snyk Container and Snyk Open Source, you can detect and fix security issues in your application and its dependencies — including those in your Docker images.
$ snyk container test your-repo/your-image:tag
In the above command, your-repo/your-image:tag
is your Docker image. Snyk will scan it and report any detected vulnerabilities, along with suggestions on how to fix them.
Create smaller and more secure Docker images for Java applications
Be aware that the examples in this blog post are meant to showcase how to use JLink to create a more concise Docker image for your Java projects. The examples shown do not meet all the best practices for secure Docker images. If you want to know more about that, take a look at our “10 best practices to build a Java container with Docker” article for some inspiration.
In conclusion, JLink is a powerful tool that can help you create smaller, more secure Docker images for your Spring Boot Java applications. Coupled with security tools like Snyk, you can ensure your applications are performant and secure. So, why wait? Sign up for Snyk today and start securing your applications.
Top comments (1)
Consider using syntax highlighting in your code blocks. For example:
Produces: