Creating the smallest JVM microservice deployment
By: Maarten Blokker
Tags: Jdk, Java, Containers, Docker, Spring Boot

When reading up on some new OpenJDK projects, I found a link to the JDK source code repository and a guide on how to build it. Building the JDK is actually not that hard at all, and details are very well documented. It even has a section for the impatient, this inspired me to write a blogpost!

In this blogpost I thought it would be fun to see how far you can go to create the smallest booting Java microservice image without making sacrifices in the stability of the application. To have a small image is something you should always aim for in a containerised environment for a number of reasons:

Fast container startup means quick deployment

Reduces network / disk cost

Smaller image means less security vulnerabilities

The rules

So let’s start with some rules, we are going to deploy the spring pet clinic application. It’s a nice demo showcase of what spring-boot has to offer and approximates the size of a real world microservice. We will be using Java 12, the latest release. We will not make any changes to the application itself, only focussing on the container image.

If you want to run the code snippets in this article and follow along, all you need is GIT, Docker and JDK 8 or newer. Let’s start by cloning the spring boot pet clinic demo and build it.



➜ cd

➜ ./mvnw install ➜ git clone https://github.com/spring-projects/spring-petclinic.git ➜ cd spring-petclinic ➜ ./mvnw install ... ➜ du -h target/*.jar

44M target/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar

After executing these commands we should be left with a jar file in the target directory that we can deploy. The produced “.jar” file is 44.7MB big and contains a webserver, a standalone database and a front-end.

Round 1: Alpine + OpenJDK

Let’s slap the executable in a Dockerfile and try to run it.

Simple, but effective. With just a few lines we have a fully functional Docker image. This image is based on an image provided by the AdoptOpenJDK community, and it uses Alpine Linux in combination with JDK12. Alpine Linux is a good choice to base our image on to keep the size down. Alpine is a distribution of linux that aims to be secure and resource efficient. No wonder it’s quite popular in the container world.

Let’s build the image and see how big it is:

➜ docker build . -t petclinic

➜ docker image ls petclinic --format={{.Size}}

325MB

Yikes, 325MB is quite big, that’s about 280MB added on top of our application. The biggest contributor to the image size seems to be the Java runtime and it’s dependencies.

➜ du -sh /opt/java/openjdk

244.8M /opt/java/openjdk

Round 2: JLink

Daniel Story — turnoff.us — Jlink uses the jigsaw modularisation system

Obviously, we can do better. The Java runtime is massive and contains a lot of code that ends up unused in the image. Java 9 introduced jlink, which is a tool that allows us to create a custom runtime with only the modules that we actually use.

Unfortunately modularisation is not that popular because it’s hard to implement. The maintainers of Spring don’t think its worth implementing modularisation because of the costs. So, to use the jlink command, we have to manually add all the modules to the runtime, one by one, until our application runs. After some trial and error, we end up with the following image:

➜ docker image ls petclinic — format={{.Size}}

109MB

Now we’re cooking, lets have a looksy at the JDK now:

➜ du -sh /opt/java/jdk

52.3M /opt/java/jdk

Thats much better compared to our previous image. The new image is only 109MB, we lost 216MB in total and the JDK lost 192.5MB, not bad at all!

Round 3

Get yourself some coffee, building these images is going to take a while

The jlink image is already quite optimised, and there’s not a lot of room for improvement. Everything from here on is shaving off a couple of megabytes, the goal of this article was to see how far you can go. So let’s start creating our own custom JDK, without unnecessary stuff like unused garbage collectors, debugging support and other helpful tools.

If you read the “building the JDK docs” you will find that building from source is straight forward with using “autoconf”. You first need to get a hold of the JDK source code, I used the AdoptOpenJDK mirror from Github. The original source code lives on Mercurial, but the repo seems to be quite slow. After getting the source you need a “boot JDK”, apparently you need Java to compile Java, im also using AdoptOpenJDK for this. Lastly we need some dependencies for the build, these are all outlined in the document.

After preparation we can start configuring the build. You can start off with a preset, that come in the flavours: server , client , minimal , core , zero and custom . I went with the minimal preset, but also added a couple extra features that I need to run our application:

compiler2: The C2 compiler for better performance

The C2 compiler for better performance g1gc: The default garbage collector in Java 12

The default garbage collector in Java 12 nmt: Native memory tracking, needed to compile the management feature

Native memory tracking, needed to compile the management feature management: Application monitoring, used by JMX, the application doesn’t run without it

Putting all of this together, we end up with a ridiculous Docker file. Creating an image out of this takes about 12 minutes on a MacBook Pro 2018.

Here’s a quick summary:

Get the JDK12 source code Get a compiled boot JDK11, needed for compilation Configure the build to produce a minimal JDK Compile Create a JLinked runtime Put it all together

Lets see what we end up with now:

➜ docker image ls petclinic — format={{.Size}}

94.5MB

We lost another 14.5MB, I think this is as far as we can take it without modifying the application itself. We went down from 325MB, and are now left with only 94.5MB, thats almost 3.5X smaller!

Conclusion

If you want to keep your containerised Java images small, I would definitely recommend jlink. Without too much effort you can strip down the JDK to have a runtime with only the components you actually use.

Now, does it make sense to build your own minimal version of the JDK? Probably not, the builds you download from AdoptOpenJDK have been thoroughly tested. Although you can run tests yourself as well, it does require quite some setup.

Creating your own build does come with a couple advantages. Oracle announced licensing changes to Java on the first of January 2019, eliminating free commercial use. Another advantage is that creating regular builds will give access to the latest security updates quick. You don’t have to wait for a publicly accessible build.

Some other stuff I tried:

Google’s distroless — I couldn’t quite get it to work with jlink and the image size seems comparable with Alpine.

I considered using GraalVM for a native image, but Martijn beat me to it!

I tried to figure out how to compile OpenJDK’s project “Portola”, a build that should run natively on Alpine. This seems like too much a stretch right now.

Please let me know if there’s other way’s to shrink image sizes, i’d love to hear it!