Java11, jlink and Docker
Despite all the fuss around Java8 (old LTS) end of life and Java11 (current LTS) licensing model, there is good reasons to move away from Java8 when it comes to containers.
End of Public Updates of Java SE 8
Java SE 8 is going through the End of Public Updates process for legacy releases. Oracle will continue to provide free public updates and auto updates of Java SE 8, until at least the end of December 2020 for Personal Users, and January 2019 for Commercial Users.
The main reasons are that Java8 cannot propertly read the memory limitations from the cgroup. The JVM will try to gobble more RAM than it’s been allowed, resulting in Out-of-memory killing by the Docker runtime.
And the other one, Java11 doesn’t have a JRE per se, only the JDK. However, it’s able to build a custom one for your application via jlink. We will explore both those aspects in the following article. The modules aspect of Java11 won’t be covered here.
A boring hello world
$ mkdir -p src/java/ch/dosimple/hello$ vim src/java/ch/dosimple/hello/Main.java# write, write, write, :wq$ javac src/java/ch/dosimple/hello/Main.java$ java -cp src/java ch.dosimple.hello.Main
Writing the content of the Main.java file is left as an exercise.
Its jar (Java archive)
This is the simplest way of doing so, you may notice that it also contains the source (Main.java), which is usually not wanted.
$ jar --create --file hello.jar \
-e ch.dosimple.hello.Main \
-C src/java .$ java -jar hello.jar
and the necessary JRE
Java11 lets you create your own JRE, which is of a decent size.
$ jlink --add-modules java.base --output jre$ jre/bin/java -jar hello.jar
Hello world!$ du -h jre
Now that this works, let’s bundle everything into a docker image. Having super small Alpine-base images is fancy. NB, your JRE directory might be a bit bigger than mine. See the explanations concerning binutils below.
In practice, it’s not uncommon to rely on Ubuntu images using their packages. The ones from the Docker Hub are usually built from source. It makes them harder to compare with a local usage. You’re free to disagree with me on that.
We’re using a multi-staged build with a first building stage following by the running stage containing only the final artifacts.
FROM ubuntu:cosmic AS buildARG DEBIAN_FRONTEND=noninteractiveRUN apt-get update -q \
&& apt-get upgrade -q -y \
&& apt-get install -q -y \
openjdk-11-jdk-headlessCOPY ./src /srcRUN javac /src/java/ch/dosimple/hello/Main.java \
&& jlink -G --add-modules java.base --output /opt/java \
&& strip -p --strip-unneeded /opt/java/lib/server/libjvm.so \
&& jar --create \
--file /opt/java/hello.jar \
-e ch.dosimple.hello.Main \
-C src/java \
.FROM ubuntu:cosmicCOPY --from=build /opt/java /opt/javaCMD [ "/opt/java/bin/java", "-jar", "/opt/java/hello.jar" ]
- ubuntu:bionic bundles OpenJDK10 as OpenJDK11, hence ubuntu:cosmic or a fresher one must be used instead.
- binutils is the fancy trick here. Without it, the final image is 544MB big. libjvm.so contains debugging symbols that should go away with -G but they don’t. Hence they have to be manually removed.
$ docker build -t greut/hello-11 .$ docker run --rm greut/hello-11
Hello world!$ docker history greut/hello-11
IMAGE CREATED CREATED BY SIZE COMMENT
2f5f0ad8e3fb 3 seconds ago /bin/sh -c #(nop) CMD ["-jar" "/opt/java/he… 0B
1f4a55128f90 4 seconds ago /bin/sh -c #(nop) ENTRYPOINT ["/opt/java/bi… 0B
80e6c93b1705 4 seconds ago /bin/sh -c #(nop) COPY dir:95c574b8023497f87… 42.1MB
88fdd57278f5 11 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 11 days ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 11 days ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 11 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
<missing> 11 days ago /bin/sh -c #(nop) ADD file:cafdec598e5c4e1cb… 73.7MB
The total of 116MiB for the Docker image are composed of:
- 74MiB from the Ubuntu Cosmic base layer;
- and 42MiB for the JRE and the Java application.
When the whole infrastructure is using the same base layer, that one paid once and then reused everywhere. Going distroless sounds nice but is probably overkill in most cases. NB, using Ubuntu Bionic as a base for the second layer works as well.