With the release of Java 9, and the introduction of Project Jigsaw (the Java Platform Module System), we no longer have the need for a full-blown JRE to run our Java applications. It is now possible to construct a stripped-down Java Runtime, containing the minimum set of required modules. This allows us to create slim Docker containers without excess baggage.
The source code belonging to this blog post can be found at: https://github.com/rlippolis/java9-runtime-image
Building and running the Java application
Our example application consists of two Java 9 modules (basically two JARs, with a
module-info.java). We assume the concept of modules is familiar to the reader. If not, you can learn more about it e.g. here: http://openjdk.java.net/projects/jigsaw/
Firstly, we have a
backend module consisting of a class which provides us with a
String (to keep it simple). The backend module has no explicit dependencies on other modules (only an implicit dependency on
Secondly, we have a
frontend module consisting of an executable main class. This class gets the String from the backend and prints it to
System.out (again, very straightforward). This module has an explicit dependency on
backend, and an implicit dependency on
To see the application in action, build it with Maven (
mvn clean package), and run it from the command line:
> java -p frontend-module/target/frontend-module-1.0-SNAPSHOT.jar:backend-module/target/backend-module-1.0-SNAPSHOT.jar \
(or use the provided
-p option sets the module path (similar to the ‘old’ classpath). The
-m option specifies the module and class to run.
Creating a custom Java runtime image with JLink
jlink tool, provided with the Java 9 JDK, allows us to combine our application modules with the required modules from the JDK (in our case, only
java.base) into a custom-tailored JRE. Please note that the generated JRE, like any JRE, is NOT platform independent! The generated JRE is therefore not portable to other platforms.
Because we would like to run our application inside Docker on top of Alpine Linux (a very small Linux distro, approximately 5 MB), we need to run
jlink with a JDK that is compatible with the Alpine OS. Unfortunately, at the time of writing, neither the Oracle JDK nor the OpenJDK release of Java 9 support Alpine Linux yet (see: https://github.com/anapsix/docker-alpine-java/issues/38). However, there is an OpenJDK Early-Access Build available, which we can use, at: http://jdk.java.net/9/ea
We could install this EA JDK and use it to run
jlink, or we could do it all from inside a Docker container, which is cooler.
Dockerfile file is used to perform a multi-stage build. The first stage runs the
jlink command to create the custom JRE. The second stage creates a tiny runnable Docker image, which executes the JRE created in the first stage.
The first stage of the
Dockerfile contains the following:
# First stage: Runs JLink to create the custom JRE
FROM alpine:3.6 AS builder
MAINTAINER JDriven <email@example.com>
ENV JAVA_HOME=/opt/jdk \
RUN set -ex && \
apk add --no-cache bash && \
wget http://download.java.net/java/jdk9-alpine/archive/181/binaries/jdk-9-ea+181_linux-x64-musl_bin.tar.gz -O jdk.tar.gz && \
mkdir -p /opt/jdk && \
tar zxvf jdk.tar.gz -C /opt/jdk --strip-components=1 && \
rm jdk.tar.gz && \
COPY backend-module/target/backend-module-1.0-SNAPSHOT.jar .
COPY frontend-module/target/frontend-module-1.0-SNAPSHOT.jar .
RUN jlink --module-path backend-module-1.0-SNAPSHOT.jar:frontend-module-1.0-SNAPSHOT.jar:$JAVA_HOME/jmods \
--add-modules com.jdriven.java9runtime.frontend \
--launcher run=com.jdriven.java9runtime.frontend/com.jdriven.java9runtime.frontend.FrontendApplication \
--output dist \
--compress 2 \
Build this Docker image using the command:
> docker build -t java9-runtime-image .
(Note the period at the end of the command!)
The builder stage of this Docker image (based on Alpine), downloads and installs the EA JDK for Alpine. Then, it runs the
jlink command on our sources. To be able to access our compiled jars, we copy them into our image.
jlink command takes the following parameters:
--module-pathonce again sets the module path to include our modules, and the default JDK modules (located at
--add-modulesdefines the set of root modules to include. We only need to include the frontend module. The backend module is included transitively, because it is required by frontend.
--launcherspecifies a command name to launch our application, and defines which class in which module is the main class (in the form of
--outputspecifies a destination directory (which should not exist yet!) in which the runtime is generated
The other parameters are included to decrease the image in size, by using compression and stripping some irrelevant data. For more information regarding these parameters, see: https://docs.oracle.com/javase/9/tools/jlink.htm
Building a Docker image for our application
After running JLink, we now have our own custom JRE, which is only approximately 30 MB in size,
in a newly created
/app/dist directory (inside the Docker container). This of course is still quite large for a hello world application. But compared to the default JRE, which is larger than 200 MB, it’s quite an improvement.
The second stage of the Docker build executes the runtime by using the launcher command defined earlier (
which is located at
Examining this script, generated by JLink, we see that it basically just starts the stripped down JRE. This JRE only contains our modules and
java.base. The main module and class we provided earlier (as a
launcher parameter) are passed as command-line arguments:
$DIR/java $JLINK_VM_OPTIONS -m com.jdriven.java9runtime.frontend/com.jdriven.java9runtime.frontend.FrontendApplication $@
Because the created JRE is fully self-contained, all we need is a simple Alpine-based Docker base image to execute our
run command. This is exactly what we do in the second stage of our
The second stage of our Dockerfile is quite simple:
# Second stage: Copies the custom JRE into our image and runs it
MAINTAINER JDriven <firstname.lastname@example.org>
COPY --from=builder /app/dist/ ./
Starting with the Alpine base image, copy the contents of
dist from the first stage into our image, and set
bin/run as an entrypoint.
To test the image, run an instance by executing:
> docker run --rm java9-runtime-image
(or use the provided
docker.sh to build and run it all)
The combination of a small Alpine Linux distro (5 MB) and our stripped down JRE (30 MB), results in a total Docker image size of approximately 35 MB. By comparison, the
openjdk:8-jre-alpine Docker image is 80 MB. A reduction of more than 50 percent!
Of course, any real-life software project also includes a number of third-party dependencies, and will almost certainly need more modules from the JRE than this Hello World example. We have yet to see if this approach will give us a significant benefit in real-life applications.