Java 11 application as lightweight docker image

radistao picture radistao · Dec 7, 2018 · Viewed 32.7k times · Source

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?

Answer

radistao picture radistao · Dec 7, 2018

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.