Приложение Java 11 как облегченный образ докера

Вдохновленный вопросом Почему базовый образ Docker для Java 11 такой большой? (openjdk: 11-jre-slim) Я обнаружил, что эта тема в мире Java до сих пор не решена.

По состоянию на 07 Dec 2018 есть общие проблемы/подводные камни (обсуждаемые в билете выше):

В результате этих проблем даже тонкие базовые образы Oracle Java 11 являются довольно тяжелыми и считаются нестабильными: https://hub.docker.com/_/openjdk/

Итак, вопрос в следующем:

Каковы оптимизированные или рекомендуемые способы создания и доставки приложений Java 11 в виде образов докеров?

Ответ 1

UPD от 07.2019: fooobar.com/questions/16702234/...

Взяв в качестве примера простое приложение с весенней загрузкой (только с одной конечной точкой REST), я смог найти следующие решения (учитывая, что jar приложения находится в build/libs/spring-boot-demo.jar до сборки Docker):

  1. Путь джедая, если мы хотим использовать официальный дистрибутив Oracle OpenJDK в стабильной версии Slim Linux (пока Debian 9 "Stretch"):

    • использовать debian:stretch-slim (последняя стабильная) базовое изображение
    • использовать многоэтапную сборку Docker

      1. Первый этап сборки Docker:

        • загрузите и установите архив Oracle OpenJDK на первом этапе сборки Docker
        • скомпилировать минимальный дистрибутив Java для вашего проекта (он же JRE) с помощью инструмента jlink
      2. Второй этап сборки Docker:

        • скопировать скомпилированный минимальный дистрибутив Java из этапа 1 в новое изображение
        • настроить путь для доступа к Java
        • скопировать флягу приложения на изображение

    Итак, финал Dockerfile выглядит примерно так

    (актуализировать значение JDK VERSION, URL и HASH):

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

    Примечание:

    • в минимальный пример JRE включены 5 java-модулей (java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument). Я нашел их "вручную", запустив приложение и исправив ClassNotFoundException. Ожидание дальнейших рекомендаций/руководств для разработчиков Spring Boot, какие модули Java следует включать и когда, а также удаление некоторых избыточных зависимостей, таких как java.desktop, который, кажется, используется только для PropertyEditorSupport
    • если вы боитесь пропустить некоторые модули - они достаточно легкие и все вместе дают увеличение примерно на 2 МБ. Получить полный список модулей java.* и jdk.* 11:

      java --list-modules | grep -E "^java\.[^@]*" | cut -d @ -f 1
      java --list-modules | grep -E "^jdk\.[^@]*" | cut -d @ -f 1

    Размер получаемого изображения в моем случае составил 123 МБ с минимальными 7 модулями Spring Boot и 125 МБ со всеми модулями java.*

    Как дополнительное усовершенствование этого рабочего процесса сборки:

    • Предварительно соберите образ с загруженным и извлеченным JDK и используйте его в качестве базового образа для первого этапа
    • если вы знаете, какие модули включать каждый раз - предварительно скомпилируйте базовый образ с минимально скомпилированным JRE и включенными модулями
  2. Простой способ с вендорами Open JDK:

    В противоположность Oracle Azul Zulu JDK 11 поддерживает альпийский порт и имеет соответствующее базовое изображение докера.

Таким образом, если Zulu JVM/JDK соблюдается, сборка Docker будет намного проще:

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

Полученное изображение составляет 73 МБ, как и ожидалось с раздетыми дистрибутивами Alpine.

Ответ 2

По состоянию на 07.2019

(Примечание: изображение первого этапа может быть настолько толстым, насколько вы пожелаете: можно использовать debian/ubuntu/что угодно и включать git/gradle/что угодно - это не повлияет на конечный размер получаемого изображения, который полностью на основании последней (второй) стадии)

Использование репозитория сообщества Alpine

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

Использование 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"]

Также читай https://blog.gilliard.lol/2018/11/05/alpine-jdk11-images.html

Ответ 3

Основываясь на ответе Radistao (классная штука!), Я создал образ на основе Amazon Corretto JDK11. Это также доступно на DockerHub.

Минимальное изображение Maslick/minimalka: jdk11 Corretto составляет ~ 108 МБ (55 МБ сжато на Dockerhub).

Если вы добавите простой jar Springboot к нему, результирующее изображение будет ~ 125 МБ (71 МБ сжато на 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

Вы также можете посмотреть на liberica openjdk11 от компании Bellsoft. Извините за много цитат, но в любом случае, здесь это

Liberica - это 100% реализация Java 11 с открытым исходным кодом. Он построен из OpenJDK, чему способствует BellSoft, тщательно протестирован и прошел JCK, предоставленный по лицензии от OpenJDK...

Их стандартная облегченная версия занимает до 100 МБ. Он не имеет модулей javafx, а его модули сжаты (jlink --compress=2 в их Dockerfile). Кроме того, в Bellsoft Docker Hub account есть различные репозитории с различными опциями OS/glibc/arch. Например. на liberica-openjdk-alpine-musl говорят:

Dockerfile для Alpine Linux (вариант musl) поддерживает три целевых изображения из коробки:

база: минимальный образ времени выполнения со сжатым модулем java.base, удаленной виртуальной машиной сервера и необязательными файлами, ~ 37 МБ с базой Alpine

lite: образ Liberica JDK lite с минимальной занимаемой площадью и виртуальной машиной сервера, ~ 100 МБ (по умолчанию)

полный: полный образ Liberica JDK с виртуальной машиной сервера и jmod, может использоваться для создания произвольного набора модулей, ~ 180 МБ

Для экономии места пользователям рекомендуется создавать собственные среды выполнения с помощью команды jmod, достаточной для запуска целевого приложения.

И вы можете пойти еще дальше за счет производительности:

Если вы готовы пожертвовать производительностью ради статического следа, рассмотрите возможность использования минимальной виртуальной машины вместо серверной виртуальной машины или клиентской виртуальной машины. Благодаря этому можно создать среду выполнения размером & lt; 20 Мб

Несколько примеров с моей машины:

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