← Back to list

Honey, I shrank the Java image!

Have you ever wondered why deploying your application take so long? Or why your disk is always full? Or why Amazon data transfer bills are…

Daniel Albuquerque in The Hotels.com Technology Blog · 2018-10-10 07:48 · 31 claps · 4.3 min read
#docker #jdeps #jlink #java
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

Honey, I shrank the Java image!

Smaller Docker images with multi-stage builds and Jlink.

Have you ever wondered why deploying your application take so long? Or why your disk is always full? Or why Amazon data transfer bills are so high?

There are plenty of reasons for this to happen and while some are outside of your control, the size of your docker images is not!

Using Jlink to create a minimal custom runtime, we managed to reduce the size of the docker image for a Spring Boot application by an impressive 250MB.

daniel$ docker images | grep example-spring-boot
example-spring-boot-1.1  ea662709c6dd  10 hours ago  99MB
example-spring-boot-1.0  521d3a82a93a 23 hours ago  348MB

How did we do this?

To explain how we did this we’ll start by creating a simple Java application. A sort of a Hello World but with a dependency to a 3rd party library (in this case, Google’s Guava).

import com.google.common.cache.CacheBuilder;
class JlinkTest {
    public static void main(String[] args) {
        var cache = CacheBuilder.newBuilder().build();
        cache.put("foo", "bar");
        System.out.println(cache.getIfPresent("foo"));
    }
}

After using Maven to package this into a jar, we run jdeps and we get the following output:

daniel$ jdeps -s hello.jar
hello.jar -> java.base
hello.jar -> not found

Jdeps is a dependency analyzer for Java. It processes .class files (or jars) and does a static analysis of the dependencies between them. Because this tool is aware of the Java module system we can use it to list the modules that our application will need to run.

In this example Jdeps is telling us that our application depends on the java.base module and also on a “not found” module.

This happens because we didn’t tell Jdeps where to look for the dependencies (remember we added Guava?).

If we try again, but this time we tell Jdeps where our dependencies are, and if we ask it to recursively go through all of them, we get this instead:

daniel$ jdeps -cp 'lib/*' -recursive -s hello.jar
error_prone_annotations-2.1.3.jar -> java.base
guava-25.1-jre.jar -> lib/error_prone_annotations-2.1.3.jar
guava-25.1-jre.jar -> java.base
guava-25.1-jre.jar -> java.logging
hello.jar -> lib/guava-25.1-jre.jar
hello.jar -> java.base

Things make a bit more sense now.

Our application depends on the java.base module, but also on java.logging (via the transitive dependency to Guava).

You can remove the summary flag (-s) to get a more detailed view of which modules are used by each of the imports.

Let’s get to work

Using Jlink

Now that we know what modules we actually need we can use Jlink to create a smaller Docker image.

Jlink was made available with Java 9 and is a command line tool that can assemble a set of modules into a custom runtime image.

To be able to compare the complexity and extra work introduced by this tool I’m going to first create a Docker image with a normal (full size) Java runtime.

FROM debian:9.5-slim

# Install wget to pull java binaries
RUN apt-get update && apt-get install -y --no-install-recommends wget rm -rf /var/lib/apt/lists/*

# Download java and unpack it
ARG JAVA_DOWNLOAD_CHECKSUM=3784cfc4670f0d4c5482604c7c513beb1a92b005f569df9bf100e8bef6610f2e
RUN cd /opt; wget --no-check-certificate https://download.java.net/java/ga/jdk11/openjdk-11_linux-x64_bin.tar.gz && echo "${JAVA_DOWNLOAD_CHECKSUM}  openjdk-11_linux-x64_bin.tar.gz"  | sha256sum -c && tar zxf openjdk-11_linux-x64_bin.tar.gz && rm -f openjdk-11_linux-x64_bin.tar.gz

ENV JAVA_HOME=/opt/jdk-11
ENV PATH="$PATH:$JAVA_HOME/bin"
ENV DIRPATH /opt/hello

# Create some dirs and copy hello jar and libs
RUN mkdir -p $DIRPATH
COPY target/hello.jar $DIRPATH/
COPY target/lib $DIRPATH/lib
RUN chmod 755 $DIRPATH/hello.jar
WORKDIR $DIRPATH

CMD exec $JAVA_HOME/bin/java $JAVA_JVM_ARGS -jar hello.jar

TL;DR; we start with Debian as our base image, we then grab Oracle’s OpenJDK, install it, and finally we copy our jars.

And unsurprisingly, our Docker image is massive.

daniel$ docker images | grep hello
hello    latest    946a41d508bd    8 minutes ago    375MB

Using Docker multi stage builds

Some time back Docker introduced multi stage builds as a way to keep images size down.

In your Dockerfile you can have multiple FROM instructions, each using a different base image, and each will begin a new stage of the build. What this means is that you can have temporary stages using fat images with the entire JDK, and even Maven or other utilities to build your project, and then selectively copy just what you need to the next build stages.

Let’s look at the following example:

# Image used to create the minimal java distribution
FROM debian:9.5-slim AS build

# Install wget to pull java binaries
RUN apt-get update && apt-get install -y --no-install-recommends wget && rm -rf /var/lib/apt/lists/*

# Download java and unpack it
ARG JAVA_DOWNLOAD_CHECKSUM=3784cfc4670f0d4c5482604c7c513beb1a92b005f569df9bf100e8bef6610f2e
RUN cd /opt; wget --no-check-certificate https://download.java.net/java/ga/jdk11/openjdk-11_linux-x64_bin.tar.gz && echo "${JAVA_DOWNLOAD_CHECKSUM}  openjdk-11_linux-x64_bin.tar.gz"  | sha256sum -c && tar zxf openjdk-11_linux-x64_bin.tar.gz && rm -f openjdk-11_linux-x64_bin.tar.gz

# Set java home and run jlink to create a minimal java distribution with modules required for Spring Boot
ENV JAVA_HOME=/opt/jdk-11
ENV PATH="$PATH:$JAVA_HOME/bin"
RUN jlink \
--module-path /opt/java/jmods \
--compress=2 \
--add-modules java.base,java.logging \
--no-header-files \
--no-man-pages \
--output /opt/jdk-mini

# Start a new image and copy just the minimal java distribution from the previous one
FROM debian:9.5-slim
COPY --from=build /opt/jdk-mini /opt/jdk-mini

# Set our java home and other useful envs
ENV JAVA_HOME=/opt/jdk-mini
ENV PATH="$PATH:$JAVA_HOME/bin"
ENV DIRPATH /opt/hello

# Create some dirs and copy hello jar and libs
RUN mkdir -p $DIRPATH
COPY target/hello.jar $DIRPATH/
COPY target/lib $DIRPATH/lib
RUN chmod 755 $DIRPATH/hello.jar
WORKDIR $DIRPATH

CMD exec $JAVA_HOME/bin/java $JAVA_JVM_ARGS -jar hello.jar

The first few lines are similar to the previous Dockerfile. We start with Debian again and install (a full) Java.

However, this time we run Jlink to create a minimal runtime with just the two modules that we will need.

We then start a new stage with an empty Debian image and copy the minimal runtime instead of installing the full runtime.

Let’s see the difference:

daniel$ docker images | grep hello
hello    latest    946a41d508bd    8 minutes ago    95MB

This small change represents a massive saving of 280MB for this simple Hello World application .

Conclusion

Be careful when using this though as you may run into issues at runtime. If you miss a module you will get a not very cool ClassNotFoundException. The number of things that can go wrong is proportional to the size and complexity of your application, however some (or most?) of these steps can be automated by just adding a few extra lines to the Dockerfile or with some existing maven plugins (check links below). And the best part is that you don’t even need to be using the new modules systems (ie, module descriptors) in your project to get these benefits.

References

Docker multi stage builds

Jdeps

Jlink

CodeFx

Maven jdeps plugin

Maven jlink plugin


메타데이터
post_id
9f737aef8963
slug
honey-i-shrank-the-java-image-9f737aef8963
url
https://medium.com/hotels-com-technology/honey-i-shrank-the-java-image-9f737aef8963
canonical_url
https://medium.com/hotels-com-technology/honey-i-shrank-the-java-image-9f737aef8963
author_url
https://medium.com/@worldtiki
status
ok
fetched_at
2026-07-29 23:21:03