1. Story
- As a Java developer I package my project into a runnable immage, for example package it into Docker images as explained here and here. So I have runtime images in which there is a JRE(Java Runtime Environment) to execute my code. But is it performant enaugh to have entire JRE there?
- Is it possible to select only parts of JRE that my program needs them?
- Smaller JRE means smaller image size and also prevents wasting memory. Wow..
- So in this post we will see how to reduce JRE size to and fits it on our project/module/code.
Please note that this all commands in this post are executable on linux and if you are using Windows or IOS, then little fitnesses is needed.
2. Solution idea by using jlink
Before Java 9, JRE was a monolithic software system. But in Java 9 a new feature comes in as a game changer with huge benefits. Java modules is the feature provided by Java Platform Module System(JPMS) in Java 9.
The general idea with Java modules is that it makes it possible to remove parts of a program which your application not be using.
With this new feature, JRE as a software system also restructured into a modular system, and now we want to recruit only JRE modules wich our application needs them.
In the other side, as the main player of our goal in this post, jlink is a tool that comes with JDK and makes it possible to assemble a set of Java modules and their dependencies into a custom and optimised runtime image.
jlink can be called via command like this:
1 2 3 4 5 6 7 8 9 |
#### # jlink add modules of mod and their dependencies # and also add modules in modulepath # to build a custom runtime image ### jlink [options] --module-path modulepath --add-modules mod [,mod...] ### |
In comming steps we use jlink in the process of packaging our application.
3. Sample java project
You can recruit any Java project that you prefer to carry out with this tutorial, but a recommendation would be using this Github repository as a very simple hello world Spring-Boot project.
4. What was the state before using jlink
At first we build a Docker image straight forward and without using jlink to see its size and in next step we will show how it will be reduced by using jlink. So in this step we built a Docker image as it explained here as "Simple Java + Docker". And we can see that its size is 238MB.
But let's see how we can make it even smaller than 238MB by using jlink.
5. Using jlink for our application
At first check your Java version by "java -version" and if it is above 9 then it would be OK for this step.
Jlink tool operates on jar files and our application is a jar file. But if your application is a war file you can copy it into a jar file by a simple command as below:
1 2 3 4 5 6 7 |
### # copy war file into jar file ### cp target/hellojavadocker.war target/hellojavadocker.jar ### |
This is the main command we call for compose our custom JRE by using jlink:
1 2 3 4 5 6 7 8 9 10 |
### # jlink command for building our custom and lightweight jre ### jlink --output myjre \ --add-modules $(jdeps --print-module-deps target/hellojavadocker.jar), \ jdk.unsupported,java.xml,java.sql,java.naming,java.desktop, \ java.management,java.security.jgss,java.instrument ### |
In above command there is a call for jdeps which is a tool in JDK which can list dependencies of our jar file.
So in jlink command we use --add-modules to add Java modules which our jar file depends on them and also we add some other Java modules that we know Spring needs them.
6. Spring dependencies for custom JRE
I couldn't find a straight forward way for knowing Spring depends on which Java modules. But with some knowledge and also some try and errors this is the list that I found at last: jdk.unsupported,java.xml,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument
7. Build docker image
Now we have both our application jar file and our custom JRE in a directory named myjre. So let's:
- create a Dockerfile for building final Docker image.
- as the final step we build the Docker image and show its size:
- Now we can call "docker image ls | grep amir/hello" and see that the image file is 180MB and much less than 238MB. Also we can think about the performance as we already have a JRE without unwanted modules in running memory when we run it.
- For final testing we can run the container by calling "docker run -p 8080:8080 amir/hello" and then go to browser and bring up "http://localhost:8080", then you could see a "Hello" in response.
1 2 3 4 5 6 7 8 9 10 11 12 |
### # Dockerfile with our custom JRE ### FROM debian:10-slim EXPOSE 8080 COPY target/hellojavadocker.jar /opt/target COPY myjre /opt/myjre WORKDIR /opt/target CMD ["/opt/myjre/bin/java", "-jar", "hellojavadocker.jar"] ### |
Put this file in the root of your project
1 2 3 4 5 6 7 |
### # Build Docker image ### sudo docker build -t amir/hello . ### |
8. Multi-Stage Docker image
As you see everything is done and we built our optimised Docker image for our Java application. But how we can simplify these steps by using multi-stage Docker file. For this purpose we can easily have a Dockerfile in our project root which would be able to:
- build the project
- and then build needed custom JRE
- and at last wrap them all up into a Docker image
You can see such a Dockerfile below, Also it's available as a whole project in Github.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
### # Multi-stage Dockerfile with our custom JRE ### # Step : build and package FROM maven:3.8.1-openjdk-11-slim as BUILD WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src/ /build/src/ RUN mvn package RUN jlink --output myjre --add-modules $(jdeps --print-module-deps target/hellojavadocker.jar),jdk.unsupported,java.xml,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument # Step : final docker image FROM debian:10-slim EXPOSE 8080 COPY --from=BUILD /build/target /opt/target COPY --from=BUILD /build/myjre /opt/myjre WORKDIR /opt/target CMD ["/opt/myjre/bin/java", "-jar", "hellojavadocker.jar"] ### |