Using GraalVM to Build Minimal Docker Images for Java Applications
Optimizing the size of Docker images has several benefits. One of these is faster deployment times, which is very important if your application needs to scale out quickly to respond to an unexpected traffic burst. In this post, I’ll show you an interesting approach for optimizing Docker images for Java applications, which also helps to improve startup times. The examples used are based on another post that I published several months ago, Reactive Microservices Architecture on AWS.
How does the Java application work?
The Java application is implemented using Java 11, with Vert.x 3.6 as the main framework. Vert.x is an event-driven, reactive, non-blocking, polyglot framework to implement microservices. It runs on the Java virtual machine (JVM) by using the low-level I/O library Netty. The application consists of five different verticles covering different aspects of the business logic.
To build the application, I used Maven with different profiles. The first profile (which is the default profile) uses a “standard” build to create an Uber JAR – a self-contained application with all dependencies. The second profile uses GraalVM to compile a native image. The standard build uses jlink to build a custom Java runtime with a limited set of modules. (A command line tool, jlink allows you to link sets of modules and their transitive dependencies to create a runtime image.)
Build a custom JDK distribution using jlink
An interesting feature of JDK 9 is the Java Platform Module Feature (JPMS), also known as Project Jigsaw, which was developed to build modular Java runtimes that include only the necessary dependencies. For this application, you need only a limited set of modules, which can be specified during a build process. To prepare for your build, download Amazon Corretto 11, unpack it, and delete any unnecessary files such as the src.zip-file which is shipped with the JDK. In the following sections, to improve understanding, a multi-stage build is used, and the different parts of the build are covered separately.
Step 1: Build a custom runtime module
In the first step of the build process, build a custom runtime with just a few modules necessary to run your application, and then write the result to /opt/minimal: