Dockerized app with Java module system, Helidon, and Alpine based JDK 12 EA

Uday Chandra
Oracle Developers
Published in
4 min readDec 26, 2018
Photo by frank mckenna on Unsplash

As you may know, JDK 9 introduced a native module system to design and build applications from a host of individual, independent modules that communicate with each other over well defined public interfaces. Some of the advantages of using the Java module system are:

  • Strong encapsulation that promotes maintainability since the implementation details are hidden
  • Reliable configuration to allow modules to declare their dependencies and address some of the issues with the traditional classpath mechanism
  • Ability to build custom runtime images specific to an application
  • Improved platform integrity and better performance through faster startup time and reduced memory footprint
  • Indirect improvement in security due to strong encapsulation and a potential to reduce the platform and application’s attack surface

In terms of container integration and support, JDK releases 10 and 11 made improvements in the JVM to ensure that the memory and CPU constraints set on a container are adhered to by the JVM. Not only that, JDK 12 early-access release now includes an Alpine Linux based binary that uses the musl C library. The biggest advantage of using the Alpine Linux based binary is that you can now build smaller docker images to run your Java applications. Smaller image sizes in-turn means we get benefits like faster downloads, faster startups, and reduced attack surface for security exploits.

Let’s build a sample application to take a look at some of these great features provided by the JDK. The application is a simple static image server built using Helidon and deployed as a docker container. We’ll use Gradle as the build tool.

We begin by defining a gradle build script to setup the Java module system and our app’s dependencies:

plugins {
id "java-library"
id "com.zyxist.chainsaw" version "0.3.1"
}
repositories {
mavenCentral()
}
sourceCompatibility = "11"dependencies {
compile "io.helidon.webserver:helidon-webserver:0.10.5"
compile "io.helidon.webserver:helidon-webserver-netty:0.10.5"
}

Since we are using the native Java module system, we will create a “module-info” class to define our module:

module example.imageserver {
exports example.imageserver;

requires java.logging;
requires io.helidon.webserver;
}

Next, we create a static image server using Helidon’s built-in static content handler:

var contentSupport = StaticContentSupport
.create(Paths.get("/app/images"));

var routing = Routing.builder()
.register("/images", contentSupport)
.build();

var config = ServerConfiguration.builder()
.port(8080)
.build();

WebServer.create(config, routing).start();

Yes, that is code written in Java :-). And that is all it takes to create a static web server. Now let’s turn our attention to building a docker image. We will make use of the palantir gradle plugin and configure it to build our docker image. Here’s the relevant gradle build file snippet:

plugins {
...
id 'com.palantir.docker' version "0.20.1"
}
...docker {
name "image-server:${version}"
files configurations.compileClasspath, "${jar.archivePath}"
copySpec.into("app")
}

Next, we define the “Dockerfile” that uses the multi-stage build process to create the docker image for our application:

FROM alpine:latest as build

# Check the JDK 12 EA downloads link to get the latest version
RUN mkdir -p /opt/jdk \
&& wget -q "https://download.java.net/java/early_access/alpine/20/binaries/openjdk-12-ea+20_linux-x64-musl_bin.tar.gz" \
&& tar -xzf "openjdk-12-ea+20_linux-x64-musl_bin.tar.gz" -C /opt/jdk

RUN ["/opt/jdk/jdk-12/bin/jlink", \
"--compress=2", \
"--strip-debug", \
"--no-header-files", \
"--no-man-pages", \
"--module-path", "/opt/jdk/jdk-12/jmods", \
"--add-modules", "java.base,java.logging,jdk.unsupported", \
"--output", "/custom-jre"]

FROM alpine:latest
COPY --from=build /custom-jre /opt/jdk/
ADD
app /app

CMD ["/opt/jdk/bin/java", \
"--upgrade-module-path", "/app", \
"-m", "examples.imageserver/examples.imageserver.Server"]

Notice how we used the first stage of the docker build process to download the Alpine Linux based JDK and create a custom JRE using jlink. For the custom JRE, apart from adding the “java.base” module and the “java.logging” module, we also added the “jdk.unsupported” module to allow netty (that powers Helidon) access to internal JDK classes.

The second stage of the docker build will start with Alpine Linux as the base image (roughly around 4.5 MB). We then add the custom JRE that was built in the first stage along with our application modules. The last step is to add the command to run the application. Equipped with these files and configurations, it’s time to build our app and package it as a docker image. Let’s ask gradle to do that for us:

gradlew clean build docker

The above command will build a docker image that just contains our custom JRE along with the application jars. If you now run “docker images”, you will see that our docker image is a measly 58.6 MB in size. Isn’t that wonderful? We can now run our application as a docker container:

docker run -d -p 8080:8080 imageserver

Using a browser, navigate to http://localhost:8080/images to see our dockerized application in action.

You can head over to Github to clone and play with the sample project.

--

--