从零构建 Java 17 S2I 镜像并接入 Jenkins 流水线 — 实战全记录
从零构建 Java 17 S2I 镜像并接入 Jenkins 流水线 — 实战全记录
本文完整记录了制作 OpenShift 兼容的 Java 17 S2I(Source-to-Image)构建器镜像、解决国内网络与镜像体积两大痛点、最终接入 Jenkins CI/CD 流水线部署若依(RuoYi-Cloud)微服务项目的全过程。
镜像: docker pull registry.cn-hangzhou.aliyuncs.com/pkyit/s2i:java17
github地址: https://github.com/pkyit/s2i
为什么需要 S2I?
S2I(Source-to-Image)是 OpenShift 提出的"源码到镜像"标准化方案。它的核心理念很简单:开发者只需要提供源代码,构建器镜像负责编译打包并生成可运行的容器镜像,整个过程对开发者透明。
但目前社区能找到的 S2I Java 镜像大多停留在 Java 11,而 Java 17 已经成为 LTS 主流版本。于是决定:自己造一个。
整体架构
整体方案分为三层:S2I 构建器镜像负责编译打包,Lean 运行时镜像负责在生产环境中运行 JAR,Jenkins 流水线串联全流程。
第一步:构建 S2I Builder 镜像
基础镜像选择
基础镜像使用 registry.aliyuncs.com/pkyit/java:jdk17,基于 Alpine 3.21.5 + OpenJDK 17.0.17,体积小巧且适合国内网络环境。
Dockerfile 核心设计
Dockerfile 的设计有几个关键点值得关注。
首先是构建工具安装,Maven 和 Gradle 都使用了国内镜像源加海外回退的双保险策略,确保在国内外网络环境下都能稳定下载:
# Maven: 阿里云 Apache 镜像优先,archive.apache.org 回退
RUN curl -fsSL -o /tmp/maven.tar.gz \
"https://mirrors.aliyun.com/apache/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
|| curl -fsSL -o /tmp/maven.tar.gz \
"https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
&& tar xzf /tmp/maven.tar.gz -C /opt \
&& mv /opt/apache-maven-${MAVEN_VERSION} /opt/maven
# Gradle: 腾讯云镜像优先,官方源回退
RUN curl -fsSL -o /tmp/gradle.zip \
"https://mirrors.cloud.tencent.com/gradle/gradle-${GRADLE_VERSION}-bin.zip" \
|| curl -fsSL -o /tmp/gradle.zip \
"https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
&& unzip -qo /tmp/gradle.zip -d /opt \
&& mv /opt/gradle-${GRADLE_VERSION} /opt/gradle
其次是 OpenShift 兼容性。OpenShift 会以随机 UID 运行容器(属于 root 组,GID=0),所以需要确保目录权限正确:
RUN mkdir -p /deployments /tmp/src /tmp/artifacts /home/s2i \
&& chmod -R g+rwX /deployments /tmp/src /tmp/artifacts /home/s2i
RUN adduser -D -u 1001 -h /home/s2i -s /bin/bash s2i 2>/dev/null || true \
&& addgroup s2i root 2>/dev/null || true
最后是 S2I 标准标签,这些标签告诉 OpenShift 如何找到构建脚本:
LABEL io.openshift.s2i.scripts-url="image:///usr/libexec/s2i" \
io.openshift.s2i.destination="/tmp" \
io.openshift.tags="builder,java,java17"
S2I 核心脚本
S2I 镜像的灵魂在于四个脚本:assemble(构建)、run(运行)、usage(帮助)、save-artifacts(增量构建)。
assemble — 自动检测 + 智能构建
assemble 脚本是整个 S2I 流程的核心。它会自动检测项目类型并按优先级选择构建策略:
关键代码片段展示了自动检测逻辑:
# 自动检测构建类型
[ -f "${S2I_SOURCE_DIR}/pom.xml" ] && HAS_POM=true
{ [ -f "${S2I_SOURCE_DIR}/build.gradle" ] || \
[ -f "${S2I_SOURCE_DIR}/build.gradle.kts" ]; } && HAS_GRADLE=true
[ -f "${S2I_SOURCE_DIR}/mvnw" ] && HAS_MVNW=true
[ -f "${S2I_SOURCE_DIR}/gradlew" ] && HAS_GRADLEW=true
# 按优先级执行
if [ "${HAS_POM}" = "true" ]; then
if [ "${HAS_MVNW}" = "true" ]; then
build_maven_wrapper # 优先使用项目自带的 Maven Wrapper
else
build_maven # 使用内置 Maven
fi
elif [ "${HAS_GRADLE}" = "true" ]; then
# ... 同理
fi
run — 容器感知 JVM 调优
run 脚本负责在容器环境中智能启动 Java 应用。它会自动检测容器内存限制并计算 JVM 堆大小,同时支持 cgroup v1 和 v2 两种格式:
max_memory() {
local ratio="${JAVA_MAX_MEM_RATIO:-80}"
local max_mem=""
# cgroup v2
if [ -f /sys/fs/cgroup/memory.max ]; then
max_mem=$(cat /sys/fs/cgroup/memory.max 2>/dev/null)
# cgroup v1
elif [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
max_mem=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null)
fi
if [ -n "${max_mem}" ] && [ "${max_mem}" != "max" ]; then
echo "-XX:MaxRAMPercentage=${ratio}.0"
fi
}
此外还支持远程调试(JAVA_DEBUG)、堆转储(JAVA_DIAGNOSTICS)、自定义 GC 策略等高级功能。
第二步:解决国内网络痛点 — 阿里云镜像
在国内使用 Maven/Gradle 构建,最大的痛点就是从 Maven Central 下载依赖极慢。解决方案是在构建器中默认配置阿里云镜像。
Maven 镜像 — assemble 脚本在构建时自动生成 settings.xml:
generate_maven_settings() {
cat > "${settings_file}" <<SETTINGS_XML
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<mirrors>
<mirror>
<id>aliyun-mirror</id>
<url>${MAVEN_MIRROR_URL}</url>
<mirrorOf>${MAVEN_MIRROR_OF}</mirrorOf>
</mirror>
</mirrors>
</settings>
SETTINGS_XML
}
默认值通过环境变量设置,用户可以随时覆盖:
MAVEN_MIRROR_URL="${MAVEN_MIRROR_URL:-https://maven.aliyun.com/repository/public}"
Gradle 镜像 — 通过 init.d 全局初始化脚本实现:
// /opt/gradle/init.d/aliyun-mirror.gradle
allprojects {
buildscript {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}
}
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}
}
这个脚本在 Dockerfile 构建阶段就预置到了 /opt/gradle/init.d/ 目录中,Gradle 启动时会自动加载,无需项目做任何修改。
第三步:解决镜像体积痛点 — 多阶段构建
初版构建出的 Spring Boot 应用镜像高达 534MB。问题出在哪里?S2I Builder 镜像本身包含 Maven、Gradle、构建缓存,这些东西全部被"继承"到了最终的应用镜像中。
解决方案是多阶段构建(Dockerfile.lean):第一阶段使用完整的 S2I Builder 编译源码,第二阶段只把 JAR 文件复制到一个纯净的 JDK 运行时镜像中。
# ========== Stage 1: 构建阶段 ==========
FROM registry.cn-hangzhou.aliyuncs.com/pkyit/s2i:java17 AS builder
USER root
COPY . /tmp/src/
RUN /usr/libexec/s2i/assemble
# ========== Stage 2: 运行阶段 ==========
FROM registry.aliyuncs.com/pkyit/java:jdk17
COPY --from=builder /deployments/ /deployments/
COPY --from=builder /usr/libexec/s2i/run /usr/libexec/s2i/run
USER 1001
CMD ["/usr/libexec/s2i/run"]
同时,assemble 脚本中加入了激进的清理逻辑,确保构建阶段不留下任何多余文件:
cleanup_maven_build_artifacts() {
# 删除 target 目录(编译产物)
rm -rf "${build_dir}/target" 2>/dev/null || true
}
cleanup_build_cache() {
# 清理 Maven 本地仓库
rm -rf "${MAVEN_LOCAL_REPO}" 2>/dev/null || true
# 清理 Gradle 缓存
rm -rf "${GRADLE_USER_HOME}/caches" 2>/dev/null || true
rm -rf "${GRADLE_USER_HOME}/daemon" 2>/dev/null || true
}
cleanup_source() {
# 删除源代码
rm -rf "${S2I_SOURCE_DIR:?}"/* 2>/dev/null || true
}
效果对比非常显著:
| 构建方式 | 镜像体积 | 包含内容 |
|---|---|---|
| 标准模式 | 534 MB | Maven + Gradle + 缓存 + 源码 + JAR |
| Lean 多阶段 | 340 MB | JDK + JAR(生产运行所需的最小集) |
| 缩减幅度 | 36% | 构建工具完全隔离在 Builder 阶段 |
第四步:多架构构建(amd64 + arm64)
现代基础设施越来越多地混合使用 x86 和 ARM 服务器。为了让 S2I 镜像同时支持两种架构,使用了 Docker buildx + QEMU 方案。
# 注册 QEMU 用户态模拟(支持 arm64 构建)
docker run --privileged --rm tonistiigi/binfmt --install arm64
# 创建多架构 builder
docker buildx create --name s2i-builder --use
# 构建并推送双架构镜像
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.cn-hangzhou.aliyuncs.com/pkyit/s2i:java17 \
--push .
这里踩了一个坑:arm64 构建在 QEMU 模拟环境下从 archive.apache.org 下载 Maven 超时(连接耗时超过 128 秒)。解决方案就是前面提到的国内镜像优先策略,让 QEMU 环境也能快速下载构建工具。
第五步:接入 Jenkins 流水线 — 若依微服务实战
为了验证 S2I 镜像的实际可用性,选择了一个真实的微服务项目来实战检验:若依(RuoYi-Cloud),一个基于 Spring Cloud Alibaba 的微服务框架,包含 7 个后端服务和 1 个前端。
改造前的流水线
原始 Jenkins 流水线的 Java 构建阶段直接使用一个包含 Maven 的 JDK 镜像编译,然后为每个服务单独 docker build,每个服务都有自己的 Dockerfile(基于 eclipse-temurin:17-jre):
// 改造前
stage("Build Java") {
steps {
sh "docker run --rm -v ${HOST_BUILD_DIR}:/workspace ... mvn clean package ..."
}
}
stage("Build Images") {
steps {
sh '''
cp ruoyi-gateway/target/ruoyi-gateway.jar docker/ruoyi/gateway/
docker build -t ${REGISTRY}/ruoyi-gateway:test docker/ruoyi/gateway/
# ... 每个服务都要手动复制 JAR + docker build
'''
}
}
每个服务还需要维护一份独立的 Dockerfile,增加了维护成本。
改造后的流水线
改造后的流水线使用 S2I 镜像作为统一的构建和运行时基础:
pipeline {
agent any
environment {
REGISTRY = "registry.cn-hangzhou.aliyuncs.com/pkydocker"
S2I_BUILDER = "registry.cn-hangzhou.aliyuncs.com/pkyit/s2i:java17"
S2I_RUNTIME = "registry.cn-hangzhou.aliyuncs.com/pkyit/s2i:java17"
}
stages {
stage("Build Backend (S2I)") {
steps {
sh '''
docker run --rm --user root \
-v ${HOST_BUILD_DIR}:/workspace -w /workspace \
-v ${HOST_M2}:/root/.m2 \
${S2I_BUILDER} \
sh -c '
cat > /tmp/settings.xml << SETXML
<?xml version="1.0" encoding="UTF-8"?>
<settings><mirrors><mirror><id>aliyun</id>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>central</mirrorOf></mirror></mirrors></settings>
SETXML
mvn clean package -P test -DskipTests -T 1C \
-Dmaven.repo.local=/root/.m2 \
-s /tmp/settings.xml -e --batch-mode
'
'''
}
}
stage("Build Backend Images (S2I Runtime)") {
steps {
sh '''
for pair in \
"ruoyi-gateway:ruoyi-gateway/target/ruoyi-gateway.jar" \
"ruoyi-auth:ruoyi-auth/target/ruoyi-auth.jar" \
"ruoyi-system:ruoyi-modules/ruoyi-system/target/ruoyi-modules-system.jar" \
"ruoyi-gen:ruoyi-modules/ruoyi-gen/target/ruoyi-modules-gen.jar" \
"ruoyi-job:ruoyi-modules/ruoyi-job/target/ruoyi-modules-job.jar" \
"ruoyi-file:ruoyi-modules/ruoyi-file/target/ruoyi-modules-file.jar" \
"ruoyi-monitor:ruoyi-visual/ruoyi-monitor/target/ruoyi-visual-monitor.jar"; do
name="${pair%%:*}"
jarfile="${pair##*:}"
dir=$(dirname "${jarfile}")
# 内联生成 Dockerfile,无需为每个服务维护单独文件
cat > ${dir}/Dockerfile.s2i << DEOF
FROM ${S2I_RUNTIME}
USER root
COPY $(basename ${jarfile}) /deployments/$(basename ${jarfile})
RUN chown 1001:0 /deployments/$(basename ${jarfile}) && chmod g+rw /deployments/$(basename ${jarfile})
USER 1001
ENV SPRING_PROFILES_ACTIVE=test \\
JAVA_MAX_MEM_RATIO=75
CMD ["/usr/libexec/s2i/run"]
DEOF
docker build -t ${REGISTRY}/${name}:test \
-f ${dir}/Dockerfile.s2i ${dir}/
done
'''
}
}
}
}
改造后的核心变化有三点:
第一,构建阶段统一使用 S2I Builder 镜像,Maven/Gradle 版本、阿里云镜像配置都内置在镜像中,实现了构建环境的标准化。
第二,运行时镜像统一基于 S2I Runtime,每个服务不再需要维护独立的 Dockerfile,只需要内联生成一个简单的 Dockerfile(只有 8 行),把 JAR 复制到 /deployments/ 即可。
第三,JVM 调优由 S2I 的 run 脚本自动处理,容器内存感知、GC 策略、堆转储等功能开箱即用,无需在每个服务的启动脚本中重复配置。
验证结果
Jenkins 构建 #14 执行成功,耗时 486 秒。7 个后端服务全部使用 S2I 镜像重新构建并部署到 Kubernetes 集群。


K8s Pod 状态
NAME READY STATUS RESTARTS AGE
ruoyi-gateway-6fcb6b45f8-zk5hj 1/1 Running 0 77s
ruoyi-auth-85bb4c6774-dbcnh 1/1 Running 0 5m
ruoyi-system-555dfbdc48-vhwrh 1/1 Running 0 5m
ruoyi-gen-7d9bc77559-fmxlr 1/1 Running 0 5m
ruoyi-job-57d7c659f8-w7kb4 1/1 Running 0 5m
ruoyi-file-84d9cb775d-55hvv 1/1 Running 0 5m
ruoyi-monitor-784ff4bb7b-rg2cf 1/1 Running 0 5m
所有服务 7/7 Ready,0 次重启。
端到端 API 验证
通过 Gateway NodePort(30080)验证微服务链路:
| 测试接口 | 预期结果 | 实际结果 |
|---|---|---|
GET /code(验证码) |
200 + uuid + 图片 | 200 OK |
POST /auth/login |
Gateway 路由到 Auth | 返回"验证码不能为空" |
GET /auth/logout |
Gateway 路由到 Auth | 200 OK(提示需要 POST) |
| Monitor :30090 | 401 未认证 | 401 Unauthorized |
各服务启动日志也确认了 Spring Boot 应用正常初始化:
(♥◠‿◠)ノ゙ 认证授权中心启动成功 ლ(´ڡ`ლ)゙
(♥◠‿◠)ノ゙ 系统模块启动成功 ლ(´ڡ`ლ)゙
(♥◠‿◠)ノ゙ 若依网关启动成功 ლ(´ڡ`ლ)゙
踩坑记录
整个过程中遇到了几个典型问题,记录下来供参考。
坑 1:Bash 函数定义顺序
assemble 脚本最初版本中,函数定义在主执行逻辑之后,导致执行时报 command not found。Bash 不像 JavaScript 有函数提升(hoisting),所有函数必须在使用之前定义。解决方案是将所有函数定义移到脚本顶部的 # Functions 区域。
坑 2:CRLF 换行符
在 Windows 上创建的文件上传到 Linux 服务器后,/bin/bash^M 无法执行。解决方法是在服务器上用 sed -i 's/\r$//' file 转换。建议 CI/CD 项目中始终配置 .gitattributes 强制 LF 换行。
坑 3:Dockerfile 不支持 Shell heredoc
Docker 的 RUN 指令不支持 Shell 的 <<'EOF' heredoc 语法。最初尝试在 Dockerfile 中用 heredoc 内联生成 Gradle init 脚本,结果报 unknown instruction 错误。解决方案是将内容提取为独立文件,用 COPY 指令复制到镜像中。
坑 4:QEMU arm64 构建网络超时
在 QEMU 模拟的 arm64 环境中,curl 连接 archive.apache.org 超时(>128秒)。这是因为 QEMU 用户态模拟的网络性能远低于原生环境。解决方案是使用国内镜像源(阿里云、腾讯云)作为首选,海外源作为回退。
坑 5:Gateway Pod 未自动更新
由于所有服务都使用 :test 固定 tag,当新镜像推送后 K8s 的 Deployment spec 没有变化,不会触发滚动更新。对 ruoyi-gateway 手动执行了 kubectl rollout restart 解决。生产环境建议使用镜像 digest 或带时间戳的 tag。
项目文件清单
https://github.com/pkyit/s2i
https://gitee.com/pengkaiyan/RuoYi-Vue (jenkins脚本在这里,git提交记录第一次是用传统流水线实现的,第二次是用s2i构建容器)
s2i-build/
├── Dockerfile # S2I Builder 镜像定义
├── Dockerfile.lean # 多阶段 Lean 构建模板
└── s2i/
├── aliyun-mirror.gradle # Gradle 阿里云镜像初始化脚本
└── bin/
├── assemble # 构建脚本(核心,~350行)
├── run # 运行时启动脚本(~150行)
├── save-artifacts # 增量构建支持
├── usage # 帮助入口
└── usage.txt # 帮助文档
总结与展望
通过这次实践,我们实现了一套完整的 Java 17 S2I 解决方案:
- 构建器镜像:内置 Maven 3.9.6 + Gradle 8.7,默认阿里云镜像,支持 Maven/Gradle/Wrapper/二进制四种构建模式自动检测
- 多架构支持:amd64 + arm64 双架构,适配 x86 服务器和 ARM 服务器
- Lean 运行时:多阶段构建将镜像体积从 534MB 降至 340MB(减少 36%)
- 容器化 JVM 调优:自动感知 cgroup 内存限制,开箱即用的 GC 策略、堆转储、远程调试
- CI/CD 集成:成功接入 Jenkins 流水线,部署若依微服务 7 个后端服务全部正常运行
下一步可以考虑的方向包括:引入 Kaniko 实现 Jenkins 中无 Docker daemon 构建;集成 Trivy 进行镜像安全扫描;以及探索 distroless 运行时镜像进一步减小体积。
如果你也在寻找 Java 17 的 S2I 方案,或者希望统一团队的构建运行时标准,相信这篇文章能给你一些参考。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)