Java 11 application as lightweight docker image

时光怂恿深爱的人放手 提交于 2019-11-28 14:37:03

问题


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:

    • jlink runtime image size (specifically libjvm.so) created from openjdk is huge. Expected it to be much smaller.
    • Solution: https://github.com/docker-library/openjdk/issues/217#issuecomment-436079779

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?


回答1:


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.




回答2:


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




回答3:


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 docker run -d my-cool-app



回答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


来源:https://stackoverflow.com/questions/53669151/java-11-application-as-lightweight-docker-image

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!