With JDK9 hitting GA, we finally have a way to build small docker containers with java microservices, all as a result of work done on java modularization. Below is a quick demonstration on how to build small docker containers with customized jdk9. For impatient, code is available on my GitHub
Bit of background first
Before JDK9, one could build java containers based on alpine linux, adding on top jdk/jre and finally the java application itself, or use one of the pre-built openjdk/openjre alpine flavour images as a base, but resulting container would still be large. In the examples, below, I am using intentionally jdk itself. We could use alpine flavour of jre8, and that would still result in 80MB space taken, but with JDK9 we can do even better, with JDK.
Starting from scratch with alpine base
We can start from scratch, using only alpine linux as base image, add jdk distribution and our app. Let's see how this works in terms of size and numbers:
➜ jdk-9-docker git:(master) ✗ docker images REPOSITORY TAG IMAGE ID CREATED SIZE alpine 3.6 37eec16f1872 11 hours ago 3.97MB github-twitter-search 1.0.1-SNAPSHOT 844cae17273b 10 hours ago 10.1MB
So, we have alpine as base, jdk/jre, and our application and on top of this. Let's assume we add in JDK8 compressed archive, which is around 165MB. Inside container we would decompress it, so the size doubles and we end up with ~300MB of JDK8, 3.97MB of alpine docker container base, and 10MB of our actuall application. Things get even worse, if you are using any of the JEE application servers, which would typically add another ~400MB to the container size, but that's a different story all together.
Using jdk/jre-alpine prebuilt containers
As an alternative to starting from the scratch, we could start off with one of the openjdk base images, slim or alpine flavour. Since alpine flavour is smaller, we will use that.
Let's see how this would work, again in terms of size and numbers. If we do
docker pull openjdk:8-jdk-alpine docker pull openjdk:8-jre-alpine
we end up with something like this:
REPOSITORY TAG IMAGE ID CREATED SIZE openjdk 8-jdk-alpine 3b1fdb34c52a 10 hours ago 101MB openjdk 8-jre-alpine 5699ac7295f9 12 hours ago 81.4MB github-twitter-search 1.0.1-SNAPSHOT 844cae17273b 10 hours ago 10.1MB
If we start with openjdk alpine flavour as our base docker image, we end up again with bit over 100MB in container size, better then starting from the scratch, but still not very good, since our application is only 10MB and jdk/jre is 81.4MB-101MB.
JDK9 to the Rescue
Let's see what we can do with JDK9, even without fully modularizing our application, a naive and simplified approach. In this example, I will be using simple application based on Vertx. All it does, is searches GitHub for projects with given name, crossreferencing that with Twitter and extracting couple tweets mentioning that project. It will be available on port 8080, and response will be pretty formated JSON (for no particular reason, other then being pretty to look at, since a human/me/you will be looking at it; otherwise, machine to machine, well, who cares about pretty formatting).
We will build this application with jdk8, and use jdk9 to run it.
So let's start. Assuming we have our application written and working/tested to high standards of software industry these days, we will focus on packaging it into docker container.
In this example, I will use alpine linux as a base docker image, and we will be using docker multi-stage build to add openjdk and customize it as first stage, followed by using that customized small jdk in second stage of the build. You can find out more about multi-stage builds in docker here
But before that, lets see how JDK9 can help us. With JDK9 we can start with standard JDK9 distro, use jdeps on our application to gain more insight into which JDK modules it needs. Once we know the modules we need, we can then build another JDK9 distribution which contains only those, and save on size significantly.
Let's see how it works and how to do it, on our little test vertx application here:
➜ target git:(master) ✗ jdeps github-twitter-search-1.0.1-SNAPSHOT-fat.jar github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> java.base github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> java.compiler github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> java.desktop github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> java.logging github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> java.naming github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> java.sql github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> java.xml github-twitter-search-1.0.1-SNAPSHOT-fat.jar -> jdk.unsupported ....output here will be much longer...
This gives us rough idea on what we would need for this application. In our case, we can trim it down even further, since here I know that java.desktop will not be needed, so I will be very agressive with it and remove it as well, when time comes.
Now that we have some idea on which modules we will need, let's build our own JDK9 distro, starting with alpine base image, and leverage docker multi-stage builds, to keep everything in single docker file.
FROM alpine:3.6 as packager ADD jdk-9-ea+181_linux-x64-musl_bin.tar /opt/ # Build our own jdk9 distro with only these modules, as first stage RUN /opt/jdk-9/bin/jlink \ --module-path /opt/jdk-9/jmods \ --verbose \ --add-modules java.base,java.logging,java.xml,jdk.unsupported \ --compress 2 \ --no-header-files \ --output /opt/jdk-9-minimal # Second stage, add only our custom jdk9 distro and our app FROM alpine:3.6 COPY --from=packager /opt/jdk-9-minimal /opt/jdk-9-minimal COPY github-twitter-search-1.0.1-SNAPSHOT-fat.jar /opt/ ENV JAVA_HOME=/opt/jdk-9-minimal ENV PATH="$PATH:$JAVA_HOME/bin" EXPOSE 8080 CMD java -jar /opt/github-twitter-search-1.0.1-SNAPSHOT-fat.jar
Note that, in order to use docker multi-stage builds, you need later(est) version of docker, at the time of writing this, it was 17.10-ce.
Once this build is executed we get our new docker container, built from alpine linux, containing only the parts of jdk9 we actually need, and our application. And the container size? Well, see below:
➜ jdk-9-docker git:(master) ✗ docker images REPOSITORY TAG IMAGE ID CREATED SIZE github-twitter-search 1.0.1 844cae17273b 10 hours ago 53.5MB
And all of this, without even going deeper into details, of fully modularizing our application. Finally, something much closer to what you could get with statically linked go binaries in containers - not there all the way, but a huge leap for java.
On a side note, this can be automated nicely, as we can create our own jdk9:alpine base image, from alpine by just adding openjdk9 for alpine linux into it, and using that as a base container for first stage of multistage build, as shown here:
FROM dekstroza/openjdk9-alpine as packager RUN /opt/jdk-9/bin/jlink \ --module-path /opt/jdk-9/jmods \ --verbose \ --add-modules java.base,java.logging,java.xml,jdk.unsupported \ --compress 2 \ --no-header-files \ --output /opt/jdk-9-minimal FROM alpine:3.6 COPY --from=packager /opt/jdk-9-minimal /opt/jdk-9-minimal COPY github-twitter-search-1.0.1-SNAPSHOT-fat.jar /opt/ ENV JAVA_HOME=/opt/jdk-9-minimal ENV PATH="$PATH:$JAVA_HOME/bin" EXPOSE 8080 CMD java -jar /opt/github-twitter-search-1.0.1-SNAPSHOT-fat.jar
For above then, we only need to add our application into container in second stage of the build.
Building your own image for the first stage
Dockerfile used below can be found on my GitHub
To create your own base image for the first stage:
- Download openjdk9 for alpine linux from here: http://jdk.java.net/9/ea
- Create Dockerfile like this:
FROM alpine:3.6 ADD jdk-9-ea+181_linux-x64-musl_bin.tar /opt/
- Place the downloaded file next to above Dockerfile
- Build the docker container and push somewhere