94

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):

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?

2

5 Answers 5

103

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:

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

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

    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 fixing ClassNotFoundException. Waiting for some further Spring Boot developers recommendations/guides which Java modules to include and when, as same as removing some redundant dependencies, like java.desktop, which seems to be used only for PropertyEditorSupport
    • 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.* and jdk.* 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.

Sign up to request clarification or add additional context in comments.

4 Comments

any updates for 11.0.3? Where can I find JDK_HASH? Tried with oracle.com/webfolder/s/digest/11-0-3-checksum.html but using "d50908ea53c2ad154a797aa0930eafb7813247dae13d9d891116df889814ebf3" failed with: "sha256sum: WARNING: 1 computed checksum did NOT match"
Thanks @radistao Any way to add CAs when creating docker image on the build server?
I'm basically "asking for a friend" because of stackoverflow.com/questions/56523042/…
that's another topic, so please don't clog this thread. Could you remove the messages starting from "Any way to add CAs..."? Thank you.
27

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)

Using Alpine community repository

FROM 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"] 

Using AdoptOpenJDK

FROM 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

7 Comments

This is the tiniest solution I have found yet ~55Mb. Keep in touch with updates, please.
This is basically installing and using glibc in a distribution specifically based on musl. Seems like a hack.
It may be a hack, but I think I may be OK with that. Although I would recommend adding the jvm arg -XX:+UseContainerSupport in the CMD line. That's a signal to java to correct how it calculates memory and CPU while inside a container.
Dude your container srsly kick asses! Added -Xmx128m -Xms128m -XX:+UseContainerSupport as suggested, limited container memory usage to 192MB and voila, even greedy spring-boot container now eats around 155MB total. Down from 705MB.
Wonderful, thank you. Can't figure out why it's so easy with Java 8 and 13 but such a pain with Java 11.
|
17

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 

images

1 Comment

This was amazing!
4

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 

Comments

4

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 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.