Dockerizing With a Custom JRE
Join the DZone community and get the full member experience.
Join For FreeIt is generally considered good-practice to have a small Docker image. While we can reduce the size of the base image of the operating system, for instance Alpine Linux which is only 5 MB, before Java 9 there was nothing we could do about the JRE. The lightest was the alpine JRE (openjdk:8-jre-alpine) coming in at about 107MB. This is because we are including classes that have nothing to do with the application, such as the applet or awt classes, even if your application is headless.
Starting in Java 9, the JRE was broken up into modules, so that it became possible to only include the modules used by the application. Java 9’s jLink enables us to create a custom, "just enough" JRE that only consists of the application classes and the modules it depends on.
In this paper, we will see how to dockerize a Java application built with jLink. We will illustrate with a running example built in Eclipse with Maven. Along the way, we will provide details on how to build a modular Java application in Eclipse. I will assume that you have a version of Eclipse later than Oxygen running on a version of Java at or later than Java 9.
First, create a Maven project with the quick start archetype and give it the GAV (com.tn.jlink;example). Create a package, com.tn.jlink.example. Right-click on the project and choose Configure -> create module-info.java and call the module com.tn.jlink. (See this for best practices in naming modules). Eclipse creates the file, and it already contains "exports com.tn.jlink.example". Add "requires java.logging" to that.
xxxxxxxxxx
module com.tn.jlink {
exports com.tn.jlink.example;
requires java.logging;
}
For simplicity, we will just use a built-in Java module
In that package, create this class:
xxxxxxxxxx
package com.tn.jlink.example;
/**
* Hello world!
*
import java.util.logging.Logger;
public class HelloWorld
{
private static final Logger LOG = Logger.getLogger(HelloWorld.class.getName());
public static void main( String[] args )
{
LOG.info( "Hello World!" );
}
}
Now, modify the pom by adding this section after the <dependencies>
.
xxxxxxxxxx
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
</plugins>
</build>
Note that for an application that is Java 9 and beyond, the compiler plugin has to be 3.8 or higher.
You can check this by building it (Run -> Run as…, Maven Build). For Goals enter clean install, and under target directory, you will find the example-0.0.1-SNAPSHOT.jar file.
That concludes the introduction on how to build a modular Java project as a modular JAR. We now turn to compiling this with jLink. The command line syntax to run jLink is:
xxxxxxxxxx
jlink [options] –module-path modulepath
–add-modules module [, module…]
--output <target-directory>
But, it must have been years since anyone used the command line to build a Java project. Besides, the command line in Windows sucks. Instead we will the use the ModiTect plugin for Maven . It supports many goals for the Java Module system, among them the one we are interested it, namely creating a custom runtime image. Now add the following to the pom
xxxxxxxxxx
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.Beta2</version>
<executions>
<execution>
<id>create-runtime-image</id>
<phase>package</phase>
<goals>
<goal>create-runtime-image</goal>
</goals>
<configuration>
<modulePath>
<path>
${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}
</path>
</modulePath>
<modules>
<module>com.tn.jlink</module>
</modules>
<launcher>
<name>hello</name>
<module>
com.tn.jlink/com.tn.jlink.example.HelloWorld
</module>
</launcher>
<outputDirectory>
${project.build.directory}/jlink-image
</outputDirectory>
<stripdebug>yes</stripdebug>
<noManPages>yes</noManPages>
<noHeaderFiles>yes</noHeaderFiles>
</configuration>
</execution>
</executions>
</plugin>
A brief explanation of the elements:
modulePath
: is the path to the directories that contain the modules. For us, we just put the JAR file we just built on this path. Note that the java.logging module is a Java module that is implicitly included. We cheated a little bit by using a Java module. Most modules are in external JAR files, and to keep this element simple, we should put the JARs in a sub-directory and put that on the module path.- modules/module: we just include the name we entered when we created the module-info. We list out the modules, by given name, one per module element.
outputDirectory
: directory in which the runtime image should be created- launcher: jLink can create shell scripts to launch the main class. Here, we give the file name. We called it "hello".
- launcher/module: this is a bit confusing. If you don’t get it right, you will get an error that you didn’t specify the main class. This is the fully qualified name of the class we want to launch, which is moduleName/fully qualified class name.
stripDebug
: whether to strip debug symbols or not. This is a jLink command line option. We set it to true to reduce the size of the imagenoManPages
,noHeaderFiles
: same as above.
Now, if we build it, you will find a new subdirectory of target called, as we asked, jlink-image. Look in the bin directory. In it are the launch scripts ‘hello’ and ‘hello.bat’. Run it to see:
xxxxxxxxxx
INFO: Hello World!
Now we come to the final step of stuffing it into a Docker image. As Java developers, we are used to being platform agnostic. But the jLink image is dependent on the platform on which it was built. Recall that Docker runs a small in-memory Linux kernel. If we built this on say, Windows it will not work. So, we need to build the image on linux. Since we wish to keep the image small, as a base image, we will use the alpine image which is just under 5MB.
Now, we run into a different problem. Alpine distributions use musl instead of libgc used by other Linux distributions. They are both C++ APIs over the Linux kernel. C or C++ code which is compiled against glibc will not run on a musl system, and vice-versa. And the JVM is written in C++ (at least for now). Bottom line is we must build it on Alpine. If you look at Maven distros in Docker Hub, there is only a Java 12 alpine distro, which is what we will use. Finally, here is the Dockerfile (which we will create under the project in Eclipse)
xxxxxxxxxx
FROM maven:3.6-jdk-12-alpine as build
WORKDIR /wrk
COPY pom.xml .
COPY src src
RUN mvn clean install
FROM alpine:3.8
COPY --from=build /wrk/target/jlink-image /app
ENTRYPOINT ["/app/bin/hello"]
Now, build the image and call it “example”.
xxxxxxxxxx
docker image build -t example
And run it
xxxxxxxxxx
docker container run -ti example
And see the INFO:Hello World
. Run the docker images
command to see the size:
xxxxxxxxxx
REPOSITORY TAG IMAGE ID CREATED SIZE
example latest 89e421476953 47 hours ago 54MB
Usually, the size of a Java Hello World image is north of a 120 MB. Using jLink yields a significantly smaller image.
We end with a word of caution. Image size is not everything. An image is downloaded once and cached and used over many, many applications. But, a jLink image is specific to the application and is not reusable. So, your mileage will vary depending on what you are trying to do.
Opinions expressed by DZone contributors are their own.
Trending
-
Building and Deploying Microservices With Spring Boot and Docker
-
JavaFX Goes Mobile
-
Reducing Network Latency and Improving Read Performance With CockroachDB and PolyScale.ai
-
Google Becomes A Java Developer's Best Friend: Instantiations Developer Tools Relaunched For Free
Comments