Java 11 application as lightweight docker image
JavaDockerAlpineJava 11Java Problem Overview
Inspired by question Why is the Java 11 base Docker image so large? (openjdk:11-jre-slim) I found that this topic in Java world is still not settled.
As for 07 Dec 2018
there are common issues/pitfalls (discussed in the ticket above):
-
JRE is not distributed as a separate "package". Modules from JDK should be used instead
-
Oracle OpenJDK 11 doesn't support Linux Alpine, so lightweight images can't be easily created
- In the same time current stable Debian versions still doesn't have Java 11 packages (Ubuntu has Java 10 installed under openjdk-11 packages), that's why unstable sid versions are used for base docker images
-
currently available Oracle openjdk-11 images build unstripped
libjvm.so
module, which has hundreds megabyte and must be stripped separately:
As a result of these issues even slim Oracle Java 11 base images are quite heavy and considered to be unstable: https://hub.docker.com/_/openjdk/
So the question is:
what are optimized or recommended ways to build and deliver Java 11 applications as docker images?
Java Solutions
Solution 1 - Java
UPD from 07.2019: https://stackoverflow.com/a/57145029/907576
Taking as an example of simple spring boot application (with only one REST endpoint) so far i was able to figure out the following solutions (considering application jar is located at build/libs/spring-boot-demo.jar
before Docker build:
- Jedi path if we want to use official Oracle OpenJDK distribution on stable slim Linux version (
Debian 9 "Stretch"
for now):- use
debian:stretch-slim
(latest stable) base image - use Docker multi-stage build
- First Docker build stage:
-
download and install
Oracle OpenJDK
archive on the first Docker build stage -
compile Java minimal distribution for your project (aka JRE) using
jlink
tool
-
Second Docker build stage:
- copy compiled minimal Java distribution from stage 1 to the new image
- configure path to access Java
- copy application jar to the image
- use
So, final Dockerfile
looks smth like this
(actualize JDK VERSION
, URL
and HASH
value):
# First stage: JDK 11 with modules required for Spring Boot
FROM debian:stretch-slim as packager
# source JDK distribution names
# update from https://jdk.java.net/java-se-ri/11
ENV JDK_VERSION="11.0.1"
ENV JDK_URL="https://download.java.net/java/GA/jdk11/13/GPL/openjdk-${JDK_VERSION}_linux-x64_bin.tar.gz"
ENV JDK_HASH="7a6bb980b9c91c478421f865087ad2d69086a0583aeeb9e69204785e8e97dcfd"
ENV JDK_HASH_FILE="${JDK_ARJ_FILE}.sha2"
ENV JDK_ARJ_FILE="openjdk-${JDK_VERSION}.tar.gz"
# target JDK installation names
ENV OPT="/opt"
ENV JKD_DIR_NAME="jdk-${JDK_VERSION}"
ENV JAVA_HOME="${OPT}/${JKD_DIR_NAME}"
ENV JAVA_MINIMAL="${OPT}/java-minimal"
# downlodad JDK to the local file
ADD "$JDK_URL" "$JDK_ARJ_FILE"
# verify downloaded file hashsum
RUN { \
echo "Verify downloaded JDK file $JDK_ARJ_FILE:" && \
echo "$JDK_HASH $JDK_ARJ_FILE" > "$JDK_HASH_FILE" && \
sha256sum -c "$JDK_HASH_FILE" ; \
}
# extract JDK and add to PATH
RUN { \
echo "Unpack downloaded JDK to ${JAVA_HOME}/:" && \
mkdir -p "$OPT" && \
tar xf "$JDK_ARJ_FILE" -C "$OPT" ; \
}
ENV PATH="$PATH:$JAVA_HOME/bin"
RUN { \
java --version ; \
echo "jlink version:" && \
jlink --version ; \
}
# build modules distribution
RUN jlink \
--verbose \
--add-modules \
java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
# java.naming - javax/naming/NamingException
# java.desktop - java/beans/PropertyEditorSupport
# java.management - javax/management/MBeanServer
# java.security.jgss - org/ietf/jgss/GSSException
# java.instrument - java/lang/instrument/IllegalClassFormatException
--compress 2 \
--strip-debug \
--no-header-files \
--no-man-pages \
--output "$JAVA_MINIMAL"
# Second stage, add only our minimal "JRE" distr and our app
FROM debian:stretch-slim
ENV JAVA_HOME=/opt/java-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"
COPY --from=packager "$JAVA_HOME" "$JAVA_HOME"
COPY "build/libs/spring-boot-demo.jar" "/app.jar"
EXPOSE 8080
CMD [ "-jar", "/app.jar" ]
ENTRYPOINT [ "java" ]
Note:
-
there are 5 java modules included to the minimal JRE example (
java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument
). I found them "manually" running the application and fixingClassNotFoundException
. Waiting for some further Spring Boot developers recommendations/guides which Java modules to include and when, as same as removing some redundant dependencies, likejava.desktop
, which seems to be used only forPropertyEditorSupport
-
if you are afraid to miss some modules - they are quite lightweight and all of them together give about 2 MB size increasing. Get a full list of
java.*
andjdk.*
11 modules:`java --list-modules | grep -E "^java\.[^@]*" | cut -d @ -f 1` `java --list-modules | grep -E "^jdk\.[^@]*" | cut -d @ -f 1`
The resulting image size in my case was 123 MB with minimal 7 Spring Boot modules and 125 MB with all java.*
modules
As an optional improvement of this build workflow:
- Pre-build an image with downloaded and extracted JDK and use it as a base image for first stage
- if you know which modules to include every time - pre-build a base image with compiled minimal JRE and included modules
2. Easy way with vendor's Open JDK distributions:
Opposite to Oracle Azul's Zulu JDK 11 supports Alpine port and has respective base Docker image.
Thus, if Zulu JVM/JDK is respected, Docker build is much simpler:
FROM azul/zulu-openjdk-alpine:11 as packager
RUN { \
java --version ; \
echo "jlink version:" && \
jlink --version ; \
}
ENV JAVA_MINIMAL=/opt/jre
# build modules distribution
RUN jlink \
--verbose \
--add-modules \
java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
# java.naming - javax/naming/NamingException
# java.desktop - java/beans/PropertyEditorSupport
# java.management - javax/management/MBeanServer
# java.security.jgss - org/ietf/jgss/GSSException
# java.instrument - java/lang/instrument/IllegalClassFormatException
--compress 2 \
--strip-debug \
--no-header-files \
--no-man-pages \
--output "$JAVA_MINIMAL"
# Second stage, add only our minimal "JRE" distr and our app
FROM alpine
ENV JAVA_MINIMAL=/opt/jre
ENV PATH="$PATH:$JAVA_MINIMAL/bin"
COPY --from=packager "$JAVA_MINIMAL" "$JAVA_MINIMAL"
COPY "build/libs/spring-boot-demo.jar" "/app.jar"
EXPOSE 8080
CMD [ "-jar", "/app.jar" ]
ENTRYPOINT [ "java" ]
The resulting image is 73 MB, as expected with stripped Alpine distributions.
Solution 2 - Java
As of 07.2019
(Note: first stage image could be as fat as you wish: one can use debian/ubuntu/whatever and include git/gradle/whatever - this won't influence the final resulting image size, which is completely based on the last (second) stage)
Alpine community repository
UsingFROM alpine:latest as packager
RUN apk --no-cache add openjdk11-jdk openjdk11-jmods
ENV JAVA_MINIMAL="/opt/java-minimal"
# build minimal JRE
RUN /usr/lib/jvm/java-11-openjdk/bin/jlink \
--verbose \
--add-modules \
java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
--compress 2 --strip-debug --no-header-files --no-man-pages \
--release-info="add:IMPLEMENTOR=radistao:IMPLEMENTOR_VERSION=radistao_JRE" \
--output "$JAVA_MINIMAL"
FROM alpine:latest
ENV JAVA_HOME=/opt/java-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"
COPY --from=packager "$JAVA_HOME" "$JAVA_HOME"
COPY build/libs/application.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
AdoptOpenJDK
UsingFROM adoptopenjdk/openjdk11:x86_64-alpine-jdk-11.0.4_11 as packager
ENV JAVA_MINIMAL="/opt/java-minimal"
# build minimal JRE
RUN jlink \
--verbose \
--add-modules \
java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
--compress 2 --strip-debug --no-header-files --no-man-pages \
--output "$JAVA_MINIMAL"
FROM alpine:latest
# magic to make Java binaries work in Alpine
# https://github.com/AdoptOpenJDK/openjdk-docker/blob/master/11/jdk/alpine/Dockerfile.hotspot.releases.slim#L24-L54
RUN apk add --no-cache --virtual .build-deps curl binutils \
&& GLIBC_VER="2.29-r0" \
&& ALPINE_GLIBC_REPO="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" \
&& GCC_LIBS_URL="https://archive.archlinux.org/packages/g/gcc-libs/gcc-libs-9.1.0-2-x86_64.pkg.tar.xz" \
&& GCC_LIBS_SHA256="91dba90f3c20d32fcf7f1dbe91523653018aa0b8d2230b00f822f6722804cf08" \
&& ZLIB_URL="https://archive.archlinux.org/packages/z/zlib/zlib-1%3A1.2.11-3-x86_64.pkg.tar.xz" \
&& ZLIB_SHA256=17aede0b9f8baa789c5aa3f358fbf8c68a5f1228c5e6cba1a5dd34102ef4d4e5 \
&& curl -LfsS https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub -o /etc/apk/keys/sgerrand.rsa.pub \
&& SGERRAND_RSA_SHA256="823b54589c93b02497f1ba4dc622eaef9c813e6b0f0ebbb2f771e32adf9f4ef2" \
&& echo "${SGERRAND_RSA_SHA256} */etc/apk/keys/sgerrand.rsa.pub" | sha256sum -c - \
&& curl -LfsS ${ALPINE_GLIBC_REPO}/${GLIBC_VER}/glibc-${GLIBC_VER}.apk > /tmp/glibc-${GLIBC_VER}.apk \
&& apk add /tmp/glibc-${GLIBC_VER}.apk \
&& curl -LfsS ${ALPINE_GLIBC_REPO}/${GLIBC_VER}/glibc-bin-${GLIBC_VER}.apk > /tmp/glibc-bin-${GLIBC_VER}.apk \
&& apk add /tmp/glibc-bin-${GLIBC_VER}.apk \
&& curl -Ls ${ALPINE_GLIBC_REPO}/${GLIBC_VER}/glibc-i18n-${GLIBC_VER}.apk > /tmp/glibc-i18n-${GLIBC_VER}.apk \
&& apk add /tmp/glibc-i18n-${GLIBC_VER}.apk \
&& /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true \
&& echo "export LANG=$LANG" > /etc/profile.d/locale.sh \
&& curl -LfsS ${GCC_LIBS_URL} -o /tmp/gcc-libs.tar.xz \
&& echo "${GCC_LIBS_SHA256} */tmp/gcc-libs.tar.xz" | sha256sum -c - \
&& mkdir /tmp/gcc \
&& tar -xf /tmp/gcc-libs.tar.xz -C /tmp/gcc \
&& mv /tmp/gcc/usr/lib/libgcc* /tmp/gcc/usr/lib/libstdc++* /usr/glibc-compat/lib \
&& strip /usr/glibc-compat/lib/libgcc_s.so.* /usr/glibc-compat/lib/libstdc++.so* \
&& curl -LfsS ${ZLIB_URL} -o /tmp/libz.tar.xz \
&& echo "${ZLIB_SHA256} */tmp/libz.tar.xz" | sha256sum -c - \
&& mkdir /tmp/libz \
&& tar -xf /tmp/libz.tar.xz -C /tmp/libz \
&& mv /tmp/libz/usr/lib/libz.so* /usr/glibc-compat/lib \
&& apk del --purge .build-deps glibc-i18n \
&& rm -rf /tmp/*.apk /tmp/gcc /tmp/gcc-libs.tar.xz /tmp/libz /tmp/libz.tar.xz /var/cache/apk/*
ENV JAVA_HOME=/opt/java-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"
COPY --from=packager "$JAVA_HOME" "$JAVA_HOME"
COPY build/libs/application.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Also read https://blog.gilliard.lol/2018/11/05/alpine-jdk11-images.html
Solution 3 - Java
a list of jdk 11 images by size
openjdk:11.0.6-jre-buster
openjdk:11.0.6-jre
openjdk:11.0.6-jre-slim-buster
openjdk:11.0.6-jre-slim
openjdk:11.0.6-jre-stretch
adoptopenjdk:11.0.6_10-jre-openj9-0.18.1
adoptopenjdk:11.0.6_10-jre-hotspot
adoptopenjdk:11.0.6_10-jre-openj9-0.18.1-bionic
adoptopenjdk:11.0.6_10-jre-hotspot-bionic
adoptopenjdk/openjdk11:jre-11.0.6_10-ubuntu
adoptopenjdk/openjdk11:jre-11.0.6_10
adoptopenjdk/openjdk11:jre-11.0.6_10-ubi-minimal
adoptopenjdk/openjdk11:jre-11.0.6_10-ubi
adoptopenjdk/openjdk11:jre-11.0.6_10-debianslim
adoptopenjdk/openjdk11:jre-11.0.6_10-debian
adoptopenjdk/openjdk11:jre-11.0.6_10-centos
adoptopenjdk/openjdk11:jre-11.0.6_10-alpine
adoptopenjdk/openjdk11:x86_64-alpine-jre-11.0.6_10
adoptopenjdk/openjdk11:x86_64-debian-jre-11.0.6_10
adoptopenjdk/openjdk11:x86_64-debianslim-jre-11.0.6_10
adoptopenjdk/openjdk11:x86_64-ubi-jre-11.0.6_10
adoptopenjdk/openjdk11:x86_64-ubi-minimal-jre-11.0.6_10
adoptopenjdk/openjdk11:x86_64-centos-jre-11.0.6_10
adoptopenjdk/openjdk11:x86_64-ubuntu-jre-11.0.6_10
mcr.microsoft.com/java/jre:11u6-zulu-alpine
mcr.microsoft.com/java/jre:11u6-zulu-centos
mcr.microsoft.com/java/jre:11u6-zulu-debian8
mcr.microsoft.com/java/jre:11u6-zulu-debian9
mcr.microsoft.com/java/jre:11u6-zulu-debian10
mcr.microsoft.com/java/jre:11u6-zulu-ubuntu
azul/zulu-openjdk-alpine:11.0.6-jre
Solution 4 - Java
Based on the answer by radistao (cool stuff!) I created an Amazon Corretto JDK11 based image. It's also available on DockerHub.
The minimal maslick/minimalka:jdk11 Corretto image is ~108MB (55MB compressed on Dockerhub).
If you add a simple Springboot jar to it, the resulting image would be ~125MB (71MB compressed on Dockerhub):
FROM maslick/minimalka:jdk11
WORKDIR /app
EXPOSE 8080
COPY my-cool-app.jar ./app.jar
CMD java $JAVA_OPTIONS -jar app.jar
docker build -t my-cool-app:latest .
docker run -d my-cool-app
Solution 5 - Java
You can also look at liberica openjdk11 by bellsoft. Sorry for lots of quotations but anyway, here it is
> Liberica is a 100% open-source Java 11 implementation. It is built from OpenJDK which BellSoft contributes to, is thoroughly tested and passed the JCK provided under the license from OpenJDK...
Their out of the box lite version takes as much as ~100MB. It does not have javafx modules and its modules are compressed (jlink --compress=2
at their Dockerfile). Apart from that, there are various repos at bellsoft Docker Hub account with different options of OS/glibc/arch. E.g. at liberica-openjdk-alpine-musl they say:
> Dockerfile for Alpine Linux (musl variant) supports three target images out of the box:
>base: minimal runtime image with compressed java.base module, Server VM and optional files stripped, ~37 MB with Alpine base
>lite: Liberica JDK lite image with minimal footprint and Server VM, ~ 100 MB (default)
>full: Liberica JDK full image with Server VM and jmods, can be used to create arbitrary module set, ~180 MB
>To save space, users are encouraged to create their own runtimes using jmod command sufficient to run the target application
And you can go even further at the expense of performance:
> If you are ready to sacrifice performance for static footprint, please consider using Minimal VM instead of Server VM or Client VM. With that, it's possible to create a runtime as small as < 20 Mb
Some examples from my machine:
docker images 'bellsoft/liberica-openjdk-*' --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY TAG SIZE
bellsoft/liberica-openjdk-alpine-musl 11.0.4-x86_64 102MB
bellsoft/liberica-openjdk-alpine 11.0.4 127MB
bellsoft/liberica-openjdk-centos latest 307MB