使用多阶段构建 Dockerfile 创建<100MB 的 Java17 Alpine 镜像

前言

  1. Alpine Linux 使用的是 musl libc 而不是大多数 Linux 发行版以及 OpenJDK 所使用的 glibc,所以OpenJDK无法直接在alpine镜像中使用
  2. 目前 Oracle 官方只提供JDK 21JavaFX 21JMC 8生产就绪 GA Releases版本,openJdk17 的构建已经过时,hub.docker 所提供的的 OpenJDK 的 docker 官方镜像也只能作为预览使用

jdk/17

docker/openjdk

jdk.java.net/archive/

作为替代,可以使用 amazoncorrettoeclipse-temurin 作为基础镜像

dockerfile 复制代码
FROM amazoncorretto:17-alpine3.18-jdk
dockerfile 复制代码
FROM eclipse-temurin:17-jre-alpine

自建镜像

amazoncorretto

  1. Jdk地址:amazon-corretto-17-downloads
  2. 镜像地址:hub.docker/amazon-corr...
  3. Dockerfile: github/corretto/corretto-docker/17/jdk/alpine/3.18/Dockerfile

eclipse-temurin

  1. Jdk地址:eclipse-temurin-17-downloads
  2. 镜像地址:hub.docker/eclipse-tem...
  3. Dockerfile: github/adoptium/containers/17/jre/alpine/Dockerfile

编写 Dockerfile

从JDK地址中找到需要对应JDK的下载链接和SHA256之后,创建如下dockerfile,不同版本放置对应的链接和SHA256即可

参考上述链接中的 Dockerfile,构建的整体思路如下

  1. 第一构建阶段

    • 使用 alpine:3.18 作为基础镜像。
    • 设置 JDK 下载链接、SHA256 散列值和安装目录。
    • 安装所需的工具,如 wgettargzipbinutils
    • 下载并校验 JDK。
    • 解压 JDK 到指定目录,并移除不必要的文件。
    • 设置环境变量,使 JDK 可用。
    • 编译一个简单的 Java 类,用于在最终镜像中检查文件编码。
    • 使用 jlink 创建一个自定义的 JRE,包含所需的模块。
  2. 第二阶段构建

    • 再次使用 alpine:3.18 作为基础镜像。
    • 将第一阶段构建的自定义 JRE 和编译好的 Java 类复制到新镜像中。
    • 设置环境变量,使 JRE 可用。
    • 安装必要的软件包,如 CA 证书、本地化支持和时区数据,以确保 Java 应用在此环境中正常运行。
    • 最后,检查 Java 运行环境是否正确安装,并清理不再需要的文件。
dockerfile 复制代码
# 第一阶段:使用基础镜像来下载和安装JDK
FROM alpine:3.18 as builder

# 设置 JDK 下载链接、SHA256 散列值和安装目录
ARG JDK_URL='https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.9%2B9/OpenJDK17U-jdk_x64_alpine-linux_hotspot_17.0.9_9.tar.gz'
ARG JDK_SHA256='c2a571a56e5bd3f30956b17b048880078c7801ed9e8754af6d1e38b9176059a9'
ARG JDK_DIR=/usr/lib/jvm/java17

# 安装必要的工具 ,binutils 是使用jlink所需的工具
RUN set -eux; \
    apk add --no-cache wget tar gzip binutils;

# 下载并校验 JDK
RUN set -eux; \
    wget -O jdk.tar.gz ${JDK_URL}; \
    echo "${JDK_SHA256} *jdk.tar.gz" | sha256sum -c -; \
    mkdir -p ${JDK_DIR}; \
    tar --extract \
        --file jdk.tar.gz \
        --directory ${JDK_DIR} \
        # 去除归档中每个文件路径的第一部分,因为每个JDK压缩包都有一个不同名的根文件夹
        --strip-components 1 \
        --no-same-owner; \
    # 删除源文件,虽然这里删不删都不影响第二阶段
    rm -f jdk.tar.gz ${JDK_DIR}/lib/src.zip;

# 设置环境变量
ENV JAVA_HOME=${JDK_DIR}
ENV PATH=$JAVA_HOME/bin:$PATH

# 编译用于检查 file.encoding 的 Java 类,在第二阶段中,由于没有javac,需要提前编译
RUN echo 'public class CheckEncoding { public static void main(String[] args) { System.out.println(System.getProperty("file.encoding")); } }' > CheckEncoding.java && \
    javac CheckEncoding.java && \
    rm CheckEncoding.java

# 使用 jlink 创建自定义 JRE,扫描jmods目录添加除GUI、实验性孵化模块以外的所有模块
# 特别注意的是,如果你使用spring程序,务必保留 jdk.unsupported 模块
# 否则会导致objenesis无法跳过构造函数创建代理,从而对没有默认构造函数的BEAN引发NoSuchMethodException
# https://stackoverflow.com/questions/51332334/nosuchmethodexception-springframework-boot-autoconfigure-http-httpmessageconver
# 如果是java 21+ 应使用 compress=zip-6 https://bugs.openjdk.org/browse/JDK-8293667
RUN set -eux; \
    MODULES=$(ls ${JAVA_HOME}/jmods | sed 's/.jmod$//' | grep -vE 'java.desktop|jdk.incubator.foreign|jdk.incubator.vector' | tr '\n' ',' | sed 's/,$//') \
    && $JAVA_HOME/bin/jlink \
         --module-path ${JAVA_HOME}/jmods \
         --add-modules $MODULES \
         --strip-debug \
         --no-man-pages \
         --no-header-files \
         --compress=2 \
         --output /jre;

# 为最终镜像设置新的基础镜像
FROM alpine:3.18
# 注意第一阶段的变量无法保留到第二阶段
ARG JDK_HOME_DIR=/opt/java/jdk17

# 复制自定义 JRE 和 CheckEncoding.class 到新镜像
COPY --from=builder /jre ${JDK_HOME_DIR}
COPY --from=builder /CheckEncoding.class /CheckEncoding.class

# 设置环境变量
ENV JAVA_HOME=${JDK_HOME_DIR}
ENV PATH=$JAVA_HOME/bin:$PATH

# 添加所需软件
RUN set -eux; \
    apk add --no-cache \
        # utilities for keeping Alpine and OpenJDK CA certificates in sync
        # https://github.com/adoptium/containers/issues/293
        ca-certificates p11-kit-trust \
        # locales ensures proper character encoding and locale-specific behaviors using en_US.UTF-8
        musl-locales musl-locales-lang \
        tzdata \
    ; \
    rm -rf /var/cache/apk/*;

# 设置其它环境变量
ENV TZ='Asia/Shanghai'
ENV LANG=C.UTF-8

# 检查JAVA运行环境并删除 CheckEncoding.class
RUN set -eux; \
    echo "Verifying install ..."; \
    echo "java --version"; java --version; \
    echo "file.encoding:"; java -cp / CheckEncoding | grep -q 'UTF-8'; \
    rm -f /CheckEncoding.class; \
    echo "Complete.";

将Dockerfile上传到仓库,并建立云效流水线

由于需要下载JDK、系统镜像、软件,建议选择云效中国香港构建集群

运行流水线并拉取镜像

查看镜像大小,可以发现镜像解压后只有 99MB

shell 复制代码
[root@localhost ~]# docker image ls registry.cn-shanghai.aliyuncs.com/xxx/alpine
REPOSITORY                                      TAG                   IMAGE ID       CREATED       SIZE
registry.cn-shanghai.aliyuncs.com/xxx/alpine   3.18-temurin-jre17    f0958c9ca363   8 hours ago   97.7MB
registry.cn-shanghai.aliyuncs.com/xxx/alpine   3.18-corretto-jre17   cf9a27c2bfee   8 hours ago   99.2MB

而 eclipse-temurin 提供的 alpine jre 大小为

shell 复制代码
[root@localhost ~]# docker image ls eclipse-temurin
REPOSITORY        TAG             IMAGE ID       CREATED       SIZE
eclipse-temurin   17-jre-alpine   2d5a7235045b   4 weeks ago   177MB
相关推荐
一颗花生米。21 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼21 分钟前
Java基础-单例模式的实现
java·开发语言·单例模式
ok!ko4 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2401_857622664 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰5 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没6 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
雪域迷影6 小时前
PostgreSQL Docker Error – 5432: 地址已被占用
数据库·docker·postgresql
编程、小哥哥6 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring