Create a Cloud Native Image using Java Modules

How to create a minimal and optimized Docker image using module system step-by-step

Leonardo Zanivan
criciumadev
5 min readJun 12, 2018

--

In previous post I described a series of benefits of upgrading to Java 10 and by leveraging of new Java Module System you could:

  • Create a minimal JRE image for your application.
  • Decrease application memory footprint.
  • Optimize application startup time.

It's time to demonstrate and proof some of my points.

Cloud Native

The first thing we need to understand is what “Cloud Native” means?

Cloud-native is an approach to building and running applications that exploits the advantages of the cloud computing delivery model. Cloud-native is about how applications are created and deployed, not where.
Source: https://pivotal.io/cloud-native

How to make my application Cloud Native?

One of the most common requirements to deploy your Cloud Native application is using Containers. So, No matter where we deploy it, but how we deploy it. The most easy way to do this is to build a Docker Image.

Nothing new here sorry

How to make my Java application Cloud Native?

Now come to interesting part, because Java Modules allow us to deploy and execute only parts of the Java Runtime Environment (JRE) required by the application and not the full JRE.

This allows a substantial reduce of the image size and the memory footprint, also some noticeable improved startup time.

The Java Linker

jlink is released as part of JDK 9 and is the tool used to build a custom JRE image based on required modules of an application or arbitrary set of modules.

Let's see the sample usage within the Spring PetClinic Java Modules project.

After building the project with Maven, execute the following command:

jlink \
--add-modules java.xml.bind,java.sql,java.naming,java.management,java.instrument,java.security.jgss \
--verbose \
--strip-debug \
--compress 2 \
--no-header-files \
--no-man-pages \
--output ./target/jlink-image

The command above generates a 43mb folder on MacOS using JDK 10.0.1.

It has enough modules to run Spring PetClinic application without any issue using the generated minimal JRE directly:

./target/jlink-image/bin/java \
-Xmx512m \
--upgrade-module-path target/modules \
--module spring.petclinic

Why it still so big?

Spring Framework and some other third party dependencies requires java.xml.bind module which requires java.desktop module, adding 12mb of unneeded classes.

Fortunately, this issue will be resolved by JDK 11 with removal of java.xml.bind module and using a explicitly dependency in Maven.

Why jlink doesn't identify modules automatically?

The answer is that regardless of the application already has a module descriptor, third party dependencies don't. The external dependencies are recognized as "Automatic Modules". Because of that, jlink doesn't support automatic generation of Java Runtime Image for the application.

The Docker Image

There are few ways to run the application inside a Docker container.

Standard OpenJDK JRE image (fat jar)

Nothing new here, it will use the OpenJDK 10 slim JRE Docker as base image in the Dockerfile. This will only be used for testing purposes in this article.

FROM openjdk:10.0.1-jre-slim

ADD spring-petclinic-2.0.0.BUILD-SNAPSHOT.jar app.jar

ARG JVM_OPTS
ENV JVM_OPTS=${JVM_OPTS}

CMD java ${JVM_OPTS} -jar app.jar

Alpine Image + Minimal JRE using glibc

This way is a bit tricker, but it gains the full benefit of Java Modules.

The Dockerfile will run as a multi-stage build and in the end we will have a image with less than 100mb!

First, OpenJDK 10 is used as builder to generate the minimal JRE image.

After, minimal JRE image is copied to Alpine with glibc installed and command is set to run using module layout.

File: src/main/docker/Dockerfile

FROM openjdk:10.0.1 as builder

RUN jlink \
--add-modules java.xml.bind,java.sql,java.naming,java.management,java.instrument,java.security.jgss \
--verbose \
--strip-debug \
--compress 2 \
--no-header-files \
--no-man-pages \
--output /opt/jre-minimal

FROM panga/alpine:3.7-glibc2.25

COPY --from=builder /opt/jre-minimal /opt/jre-minimal

ENV LANG=C.UTF-8 \
PATH=${PATH}:/opt/jre-minimal/bin

ADD modules /opt/app/modules

ARG JVM_OPTS
ENV JVM_OPTS=${JVM_OPTS}

CMD java ${JVM_OPTS} --upgrade-module-path /opt/app/modules --module spring.petclinic

Notes: The OpenJDK port for Alpine musl is in progress and there's no defined target date yet. See Portola Project.

Integrating Docker and Maven

Now we only need to add a couple of Maven plugins configurations in the project pom.xml to get the job done.

<profiles>
<profile>
<id>docker</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>filter-dockerfile</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>src/main/docker</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<contextDirectory>${project.build.directory}</contextDirectory>
<repository>${project.artifactId}</repository>
<tag>latest</tag>
<dockerConfigFile></dockerConfigFile>
<buildArgs>
<JVM_OPTS>-Xmx512m</JVM_OPTS>
</buildArgs>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-archiver</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

The change was added in the main repository using the following PR:

Notes: The plugin configuration were added inside a docker profile to allow non-docker builds.

Packaging the Cloud Native Image

Start the Docker engine daemon and run the following command:

mvn clean package -Pdocker

Running the Cloud Native Image

Before deploying the image to production, you can run it locally and access the application on port 8080 using the following command:

docker run --rm -it -p 8080:8080 spring-petclinic:latest

Benchmarking

Let's compare the standard way to deploy Java applications into Docker containers versus the Java Modules approach presented in this article.

Disclaimer: My local MBP was used in the naive benchmark results.

Image Size

OpenJDK 10 JRE SLIM -> 326mbAlpine Image + Minimal JRE using glibc -> 97.5mb

More than 200% of improvement!

Memory Footprint (after startup)

OpenJDK 10 JRE SLIM -> 451mbAlpine Image + Minimal JRE using glibc -> 291mb

More than 50% of improvement!

Startup Time

OpenJDK 10 JRE SLIM -> 5.8sAlpine Image + Minimal JRE using glibc -> 4.6s

More than 25% of improvement!

Celebrate!

--

--