问题
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:
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
- download and install
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
andHASH
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.*
modulesAs 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
- use
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