Create the smallest Docker image using JHipster 6 and Java 11+
Tags: docker, java, angular
Published: 2019-05-21 13:57:14

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:

```shell
$ 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.

![IDEA](https://thepracticaldev.s3.amazonaws.com/i/8ivvwhncskw9ntbmj77b.jpeg)

# The 'broken' Dockerfile from JHipster

JHipster provides a Dockerfile which is located in src/main/docker :

```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.

```shell
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.

```docker
#!/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?

![alpine](https://thepracticaldev.s3.amazonaws.com/i/37i55fblr6c42nirm2kc.jpeg)

# 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.

{% youtube 8SdrYGIM384 %}

# Azul's OpenJDK Zulu

Azul provides an Alpine Linux OpenJDK distribution for Java 11, the best of both worlds!

![Azul](https://thepracticaldev.s3.amazonaws.com/i/z1urxzxxqbh2eq6mi9h5.jpeg)

> 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.

```shell
$ 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.


```docker
#
# 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

![Docker](https://thepracticaldev.s3.amazonaws.com/i/kdhn4qntyjyjffjuwwi8.png)

# Addendum

##Jib

Immediate response on my article came from Christophe, thanks for the feedback!

{% twitter 1127830445209600000 %}

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!

![OpenJDK11](https://thepracticaldev.s3.amazonaws.com/i/r25l6yal3xlcx0i9op66.png)

# "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 😄

{% twitter 1127840532363919361 %}

# 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