Stephan
Posted on May 21, 2019
This article will explain how to create the smallest Docker image possible using JHipster 6 and Java 11+.
Make sure to first read the "Better, Faster, Lighter Java with Java 12 and JHipster 6" by Matt Raible.
Today (Monday 13th of May 2019) Mohammed Aboullaite (from Devoxx Morocco) gave an awesome related talk "Docker containers & java: What I wish Iβve been told!" with lots of interesting info. Make sure to check out his slide deck.
Setup your Java 11 development environment
You can skip this part if you've Java 11 already running on your development machine.
SDKman is a great tool for installing multiple versions of Java. This tool also allows you to switch very easily between different java versions. #MustHave
After installation you can list all the available Java SDK versions.
$ sdk list java
You can select (and install) the Java 11 SDK version as follows:
$ sdk use java 11.0.3-zulu
Now we can change the maven pom.xml java.version from 1.8 to 11.
<java.version>11</java.version>
The SDK files are located in a hidden directory .sdkman which makes it a bit harder to be re-used in IDEA. Adding a symbolic link is a pragmatic solution:
$ cd /Library/Java/JavaVirtualMachines
$ sudo ln -s /Users/stephan/.sdkman/candidates/java/11.0.3-zulu 11.0.03-zulu
Now you can add SDK 11 to your IDEA.
The 'broken' Dockerfile from JHipster
JHipster provides a Dockerfile which is located in src/main/docker :
FROM openjdk:11-jre-slim-stretch
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
JHIPSTER_SLEEP=0 \
JAVA_OPTS=""
# Add a jhipster user to run our application so that it doesn't need to run as root
RUN adduser -D -s /bin/sh jhipster
WORKDIR /home/jhipster
ADD entrypoint.sh entrypoint.sh
RUN chmod 755 entrypoint.sh && chown jhipster:jhipster entrypoint.sh
USER jhipster
ENTRYPOINT ["./entrypoint.sh"]
EXPOSE 8080
ADD *.war app.war
I've several issue's with this Dockerfile, the main one is... it doesn't work π
Make sure to read the addendum why the Dockerfile is broken.
1) The adduser command gives an error when building the Docker image.
Option d is ambiguous (debug, disabled-login, disabled-password)
Option s is ambiguous (shell, system)
adduser [--home DIR] [--shell SHELL] [--no-create-home] [--uid ID]
[--firstuid ID] [--lastuid ID] [--gecos GECOS] [--ingroup GROUP | --gid ID]
[--disabled-password] [--disabled-login] [--add_extra_groups] USER
Add a normal user
adduser --system [--home DIR] [--shell SHELL] [--no-create-home] [--uid ID]
[--gecos GECOS] [--group | --ingroup GROUP | --gid ID] [--disabled-password]
[--disabled-login] [--add_extra_groups] USER
Add a system user
adduser --group [--gid ID] GROUP
addgroup [--gid ID] GROUP
Add a user group
addgroup --system [--gid ID] GROUP
Add a system group
adduser USER GROUP
Add an existing user to an existing group
general options:
--quiet | -q don't give process information to stdout
--force-badname allow usernames which do not match the
NAME_REGEX configuration variable
--help | -h usage message
--version | -v version number and copyright
--conf | -c FILE use FILE as configuration file
The command '/bin/sh -c adduser -D -s /bin/sh jhipster' returned a non-zero code: 1
2) The Dockerfile should add a JAR file and not a war file (see the maven pom.xml file packaging field).
The entrypoint.sh script sshould also use a jar file instead of a war.
#!/bin/sh
echo "The application will start in ${JHIPSTER_SLEEP}s..." && sleep ${JHIPSTER_SLEEP}
exec java ${JAVA_OPTS} -noverify -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -jar "${HOME}/app.war" "$@"
DockerFile V2
FROM openjdk:11-jre-slim-stretch
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
JHIPSTER_SLEEP=0 \
JAVA_OPTS=""
# Add a jhipster user to run our application so that it doesn't need to run as root
RUN adduser --home /home/jhipster --disabled-password jhipster
WORKDIR /home/jhipster
ADD entrypoint.sh entrypoint.sh
RUN chmod 755 entrypoint.sh && chown jhipster:jhipster entrypoint.sh
USER jhipster
ENTRYPOINT ["./entrypoint.sh"]
EXPOSE 8080
ADD *.jar app.jar
This produces a Docker image of 340Mb but can we make it smaller?
From Debian to Alpine Linux (to Distroless)
The JHipster Dockerfile uses an OpenJDK 11 runtime image which is based on Debian, that explains partially why the image is 340Mb. Switching to Alpine Linux is a better strategy!
Mohammed from Devoxx MA suggested to look into an even smaller possibility using Google's "Distroless" Docker images. #NeedMoreTimeToInvestigate
HINT: Consider watching this very interesting Voxxed Days Zurich 2019 presentation from Matthew Gilliard on Java Containers. He takes a Hello World example and deploys it using different strategies including native images.
Azul's OpenJDK Zulu
Azul provides an Alpine Linux OpenJDK distribution for Java 11, the best of both worlds!
The Azul runtime integrates and natively supports the musl library, which makes the integration more efficient (in terms of the footprint and runtime performance).
See also the Portola Project - The goal of this project is to provide a port of the JDK to the Alpine Linux distribution, and in particular the musl C library.
Let's Strip with JLink
Now that we're (finally) on Java 9+ we can take advantage of the Java modules system. This means we can create a custom JVM which only includes the Java modules used by our application.
To find out which modules are used we can use jdeps to introspect our project jar file.
$ jdeps --list-deps myapp-1.0.0.jar
java.base
java.logging
java.sql
Looks like the app only requires 3 Java modules. Unfortunately this is not correct, more on this later.
Next step is to create a custom JVM using jlink and add the 3 required modules:
$ jlink --output myjdk --module-path $JAVA_HOME/jmods --add-modules java.base,java.sql,java.logging
The above command creates a myjdk directory where everything is included to run our jar file.
The Final Dockerfile
After running the JHipster application on a production machine I noticed several modules were still missing to run the Spring Boot web app using Java 11.
java.desktop // For Java Beans getter's and setters
java.management // JMX
jdk.management // JDK-specific management interfaces for the JVM
java.naming // JNDI
jdk.unsupported // Sun.misc.Unsafe
jdk.crypto.ec // SSL
java.net.http // HTTP
It's obvious that depending on your project functionality, you'll need to add more modules.
Now that we know which Java modules are required we can create the following Dockerfile.
Part 1 : Take Azul's zulu OpenJDK jvm and create a custom JVM in /jlinked directory.
Part 2
Use Alpine linux and copy the jlinked JDK into /opt/jdk and start the java app.
Undertow forced me to run Spring Boot as root because it could otherwise not open some sockets. Further investigation is needed, suggestions are always welcome.
#
# Part 1
#
FROM azul/zulu-openjdk-alpine:11 as zulu
RUN export ZULU_FOLDER=`ls /usr/lib/jvm/` \
&& jlink --compress=1 --strip-debug --no-header-files --no-man-pages \
--module-path /usr/lib/jvm/$ZULU_FOLDER/jmods \
--add-modules java.desktop,java.logging,java.sql,java.management,java.naming,jdk.unsupported,jdk.management,jdk.crypto.ec,java.net.http \
--output /jlinked
#
# Part 2
#
FROM alpine:latest
COPY --from=zulu /jlinked /opt/jdk/
RUN apk update
RUN rm -rf /var/cache/apk/*
ENV CFP_JAVA_OPTS="-Xmx512m"
ENV CFP_PERFORMANCE_OPTS="-Dspring.jmx.enabled=false -Dlog4j2.disableJmx=true"
CMD /opt/jdk/bin/java $CFP_JAVA_OPTS $CFP_PERFORMANCE_OPTS -XX:+UseContainerSupport \
-noverify -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -jar /app.jar
ADD target/*.jar /app.jar
EXPOSE 80
The above example is heavily inspired on the ALF.io provided Dockerfile
We now have a 180Mb Docker image which we can deploy to production ππͺπ»
Can we Go Faster?
On my to do list is to investigate the Application Class Data Sharing (CDS), if configured correctly the app can have a 25% faster startup time!
CDS was a commercial only feature of the Oracle JDK since version 7, but it has also been available in OpenJ9 and now included in OpenJDK since version 10.
Another strategy to investigate is using an exploded jar file, not sure if that will give any noticeable increase in startup time?
Can we Go Smaller?
Absolutely!
Imagine if JHipster could produce a Quarkus and/or Micronaut project based on your JDL. This would mean we could create a native image thanks to GraalVM.
Producing an even smaller Docker image and stellar fast startup... a stellar combination with Google Cloud Run!
TheFutureLooksBright
Comments and suggestions are very welcome!
Cheers,
Stephan
Part 2 of this article series is now available @ https://dev.to/stephan007/the-jhipster-quarkus-demo-app-1a1n
Addendum
Jib
Immediate response on my article came from Christophe, thanks for the feedback!
Christophe Bornet@cbornet_@Stephan007 @brunoborges @alpinelinux @AzulSystems @Docker @OpenJDK @alfio_event Interesting. Note that @java_hipster doesn't use the Dockerfile anymore and it has been removed from master. We now use jib instead.06:58 AM - 13 May 2019
It seems JHipster is now using Jib instead of the provided Dockerfile. Will need to investigate how the Dockerfile looks like and if it provides a smaller image?!
Jib builds optimised Docker and OCI images for your Java applications without a Docker daemon - and without deep mastery of Docker best practices. It is available as plugins for Maven and Gradle and as a Java library.
./mvnw package -Pprod verify jib:dockerBuild
More details @ https://www.jhipster.tech/docker-compose/#-building-and-running-a-docker-image-of-your-application
"3 Days Ago"
Another response on the article informed me that the JHipster team had switched to OpenJDK11 using Alpine 3 days ago. That's what I love about JHipster, they're at the top of their game!
"Distroless" Docker Images
My Devoxx Morocco friend Mohammed (and Docker Champion) suggested in a Twitter reply to look at Google's Distroless docker images. Looks very promising indeed, need more time to investigate π
Mohammed Aboullaite@laytoun@Stephan007 @alpinelinux @AzulSystems @Docker @OpenJDK @alfio_event As an alternative to alpine (and the issues with Musl) you can use a very lightweight linux distro from Google (part of the distroless project github.com/GoogleContaineβ¦)
The base image is glibc based and it size is almost 8M! You can achieve similar results with openjdk hotspot07:38 AM - 13 May 2019
Illegal Reflective Access via Undertow
Spring Boot uses Undertow and has a dependency on jboss XNIO-NIO. As a result Java 11 will throw a warning : illegal reflective access operation.
Switching to Jetty instead of Undertow might resolve this?
WARNING: An illegal reflective access operation has occurred
[dvbe19-cfp-app-64676889d-4g8nv dvbe19-app] WARNING: Illegal reflective access by org.xnio.nio.NioXnio$2 (jar:file:/app.jar!/BOOT-INF/lib/xnio-nio-3.3.8.Final.jar!/) to constructor sun.nio.ch.EPollSelectorProvider()
[dvbe19-cfp-app-64676889d-4g8nv dvbe19-app] WARNING: Please consider reporting this to the maintainers of org.xnio.nio.NioXnio$2
[dvbe19-cfp-app-64676889d-4g8nv dvbe19-app] WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
[dvbe19-cfp-app-64676889d-4g8nv dvbe19-app] WARNING: All illegal access operations will be denied in a future release
And another reflective warning. But for this we don't have an alternative (yet).
[INFO] --- maven-war-plugin:2.2:war (default-war) @ cfp ---
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.thoughtworks.xstream.core.util.Fields (file:/Users/stephan/.m2/repository/com/thoughtworks/xstream/xstream/1.3.1/xstream-1.3.1.jar) to field java.util.Properties.defaults
WARNING: Please consider reporting this to the maintainers of com.thoughtworks.xstream.core.util.Fields
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
References
https://developer.okta.com/blog/2019/04/04/java-11-java-12-jhipster-oidc
https://spring.io/blog/2018/12/12/how-fast-is-spring
https://blog.gilliard.lol/2018/11/05/alpine-jdk11-images.html
https://docs.oracle.com/en/java/javase/11/vm/class-data-sharing.html
Docker containers & java: What I wish I've been told! https://docs.google.com/presentation/d/1d2L6O6WELVT6rwwhiw_Z9jBnFzVPtku4URPt4KCsWZQ/edit#slide=id.g5278af057a_0_124
"Docker containers & java: What I wish I've been told!" Video @ https://www.docker.com/dockercon/2019-videos?watch=docker-containers-java-what-i-wish-i-had-been-told
References
- https://developer.okta.com/blog/2019/04/04/java-11-java-12-jhipster-oidc
- https://spring.io/blog/2018/12/12/how-fast-is-spring
- https://blog.gilliard.lol/2018/11/05/alpine-jdk11-images.html
- https://docs.oracle.com/en/java/javase/11/vm/class-data-sharing.html
- https://docs.google.com/presentation/d/1d2L6O6WELVT6rwwhiw_Z9jBnFzVPtku4URPt4KCsWZQ/edit#slide=id.g5278af057a_0_124
- "Docker containers & java: What I wish I've been told!" Video @ https://www.docker.com/dockercon/2019-videos?watch=docker-containers-java-what-i-wish-i-had-been-told
Posted on May 21, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.