If you’ve been working with JVM-based languages (Java, Kotlin, Scala, and so on) for a while, you may have noticed that, beginning with Java 11, the Java Runtime Environment (JRE) no longer has a separate distribution. Because of this decision, many Java Development Kit (JDK) docker image distributors, such as OpenJDK, Amazon Correto, and others, do not provide JRE as a separate docker image. Using these images resulted in an overall Docker image size of about 360 MB, while the actual application jar size was about 26 MB. The overall Docker image size is too large in my opinion, and it should be reduced in order to save space and network bandwidth for everyone who will use this Docker image. Now, let’s see how to drastically reduce the Docker image size.

The root of this problem

The Java Platform Module System (JPMS) was introduced with Java 9. We can use JPMS to create our own custom JRE that is suitable for specific applications. For example, if an application does not use audio, image, or JavaBeans-related features, we can remove the java.desktop module entirely to free up space in our Docker image.

As previously stated, there is no separate JRE distribution from Java 11 onwards. That means that even if we just want to run a simple JVM-based application, we must install the entire JDK. This is due to the modularity introduced in Java 9. The main philosophy is that instead of providing a generic JRE that meets everyone’s needs, everyone should be able to create their own JRE. Many JDK image providers follow the same philosophy by omitting JRE distributions.

Unfortunately, using such images significantly increases the size of the Docker image.

To better understand this issue, let’s look at the basic Dockerfile required to run a simple JVM-based application.

# greetings.Dockerfile

FROM amazoncorretto:17-alpine
EXPOSE 8080
COPY ./greetings/build/libs/greetings.jar /app/
WORKDIR /app

CMD ["java", "-jar", "greetings.jar"]

We’re using amazoncorretto:17-alpine as the base image here, and we’re copying the application jar file to it. At the end we run the jar file.

Let’s run this Dockerfile and see how big it is.

ls -lh greetings/build/libs/greetings.jar | awk '{print $5, $9}'
# 26M greetings/build/libs/greetings.jar

docker build -t greetings:jdk -f greetings.Dockerfile .

docker image ls | grep greetings

# The output looks like following
# greetings jdk ca39786a6f62 2 hours ago 361MB

That is, the image size of 361 MB is quite large for a jar file of 26 MB, isn’t it? So, how can we make it smaller?

The Solution

Along with modularity, Java 9 includes a tool called jlink. The main purpose of this tool is to assist us in creating custom JRE based on our needs. This tool offers a few options for fine-tuning JRE and the required modules, but it also offers the option of creating a generic JRE that includes all of the modules.

Custom JRE

Let’s start with a look at the generic Docker image.

# greetings.Dockerfile

FROM amazoncorretto:17-alpine as corretto-jdk

# required for strip-debug to work
RUN apk add --no-cache binutils

# Build small JRE image
RUN jlink \
--add-modules ALL-MODULE-PATH \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /jre

FROM alpine:latest
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

COPY --from=corretto-jdk /jre $JAVA_HOME

EXPOSE 8080
COPY ./greetings/build/libs/greetings.jar /app/
WORKDIR /app

CMD ["java", "-jar", "greetings.jar"]

Let’s go over this file quickly.

  • In this case, we used Docker multi-staged builds.
  • We use the same amazoncorretto:17-alpine Docker image as the base image in the first stage.
  • Following that, we install binutils, which is required by the jlink tool. The jlink tool is then used to create a custom JRE. The most important part of this command is --add-modules ALL-MODULE-PATH, which adds all the modules to JRE. On the Oracle documentation page, you can learn more about all of the options.
  • The alpine:latest image is used as the base image in the second stage.
  • Then we copy the newly created custom JRE from the previous stage.
  • Finally, we are running our application jar file.

Let’s now build this new dockerfile and examine the image size.

docker build -t greetings:jre -f greetings.Dockerfile .

docker image ls | grep greetings

# The output looks like following
# greetings jre d5f20dab834c 2 hours ago 123MB

That is the new image size is only 123 MB, which is nearly a third of the original image size, and this includes all modules.

Can we further reduce the size by only including the modules that are required? Yes, but the main question is how to determine which modules are required for the application to function properly.

Slim JRE

We can use the jdeps command to determine the necessary modules. jdeps was first introduced in Java 8 to examine dependencies in an application. Additionally, jdeps can discover every Java module that every library dependency uses. Before running the jdeps command, we must extract the jar file in order for it to function properly.

unzip ./greetings/build/libs/greetings.jar -d temp

jdeps \
--print-module-deps \
--ignore-missing-deps \
--recursive \
--multi-release 17 \
--class-path="./temp/BOOT-INF/lib/*" \
--module-path="./temp/BOOT-INF/lib/*" \
./greetings/build/libs/greetings.jar

# The output will look like following
# java.base,java.compiler,java.desktop,java.instrument,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported

rm -rf temp

As you can see, we first extract the application jar file to a temporary directory before running the jdeps command with few configuration options. Finally, we remove the temporary directory.

Note: jdeps will not be able to print required modules used by Reflection. For example, if application contains spring security, we need to add jdk.crypto.ec and jdk.crypto.cryptoki module manually.

Now we’ll replace ALL-MODULE-PATH with the list printed by jdeps.

# greetings.Dockerfile

FROM amazoncorretto:17-alpine as corretto-jdk

# required for strip-debug to work
RUN apk add --no-cache binutils

# Build small JRE image
RUN jlink \
--verbose \
--add-modules java.base,java.compiler,java.desktop,java.instrument,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported,jdk.crypto.ec,jdk.crypto.cryptoki \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /jre

FROM alpine:latest
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

COPY --from=corretto-jdk /jre $JAVA_HOME

EXPOSE 8080
COPY ./greetings/build/libs/greetings.jar /app/
WORKDIR /app

CMD ["java", "-jar", "greetings.jar"]

Let’s now build this new dockerfile and examine the image size.

docker build -t greetings:slimjre -f greetings.Dockerfile .

docker image ls | grep greetings

# The output looks like following
# greetings slimjre 450a64815cb3 46 minutes ago 89.8MB

That is the new image size is only 90 MB, which is nearly a quarter of the original image size. Isn’t that preferable?

Problem with Slim JRE and how to fix it

We knew from previous results that slim JRE is superior to generic JRE. However, the slim JRE has a minor flaw. If the application is still in development, we may need to make frequent changes to our Dockerfile. Also, because we are changing the Dockerfile, Docker may not be able to reuse all of the layers.

Automated Slim JRE

If you continue to rely on using the slim JRE, we can at least automate the above process to make our lives a little easier. To automate the process, see the following GitHub gist:

Final Thoughts

image size comparison

As you can see, we were able to reduce the image size by nearly three times with minimal effort. We have two alternatives.

  • Slim JRE, the image size is extremely small, and it contains only the required Java module, which may necessitate frequent updates to the dockerfile, and Docker may be unable to reuse layers across projects.
  • Generic JRE, the image size of the generic JRE is slightly larger than that of the slim JRE, but it contains all of the Java modules.

It is up to you to determine which JRE is best suited for your application. However, with either option, you will be able to drastically reduce the image size.

Note: Slim JRE concepts is getting used in gitactionboard, you can find the size difference on docker page.


How to reduce JVM docker image size by at least 60% was originally published in DevOps.dev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Originally posted on blog.devops.dev.