企业级后端部署方案:Jenkins + MinIO + SSH + Gitee + Jenkinsfile 自动化实践
本文档基于 jenkinsfileTest/backend.jenkinsfile 及共享库 jenkinslib 编写,描述 Java 后端从 Maven/Gradle 编译、JAR 制品入库(含保留策略)、多机 SSH 部署(启动脚本模板/自定义、指定启动用户)到回滚的完整流程。
一、方案架构
1.1 组件说明
| 组件 |
角色 |
说明 |
| Gitee |
代码仓库 |
SSH 拉取指定分支 |
| Jenkins (master) |
CI/CD |
编译、上传 MinIO、SSH 部署、launch.sh 重启 |
| MinIO |
制品仓库 |
版本化 JAR,自动清理超额历史包 |
| 应用服务器 |
运行节点 |
SSH 拉 JAR + bin/launch.sh restart |
| Apollo |
配置中心 |
脚本模板模式注入 JVM 参数 |
1.2 目标机目录结构
{destPath}/
├── order-service-20250618_143022-a1b2c3d.jar # 当前 JAR(保留 N 个历史 JAR)
└── bin/
└── launch.sh # start / stop / restart
1.3 流水线与共享库
| 文件 |
说明 |
backend.jenkinsfile |
后端流水线入口 |
pipeline.groovy |
checkout、发布门禁、回滚选版 |
build.groovy |
compile() mvn/gradle |
deploy.groovy |
MinIO 上传/清理、SSH 部署、launch 脚本生成 |
resources/backend-launch-template.sh |
默认 bash 启动脚本 |
tools.groovy |
publishMode、startScriptType 归一化 |
config.groovy |
凭据、MinIO、默认保留数 |
二、发布模式(publishMode)
| publishMode |
行为 |
| 自动发布(默认) |
构建上传后自动部署 |
| 手动发布 |
input 确认后部署 |
| 仅构建 |
仅上传 MinIO,不 SSH 部署 |
| 回滚 |
选历史 JAR 部署 |
#mermaid-svg-Rpt7ec9XvC7xBTl2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .error-icon{fill:#552222;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .marker.cross{stroke:#333333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 p{margin:0;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .cluster-label text{fill:#333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .cluster-label span{color:#333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .cluster-label span p{background-color:transparent;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .label text,#mermaid-svg-Rpt7ec9XvC7xBTl2 span{fill:#333;color:#333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .node rect,#mermaid-svg-Rpt7ec9XvC7xBTl2 .node circle,#mermaid-svg-Rpt7ec9XvC7xBTl2 .node ellipse,#mermaid-svg-Rpt7ec9XvC7xBTl2 .node polygon,#mermaid-svg-Rpt7ec9XvC7xBTl2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .rough-node .label text,#mermaid-svg-Rpt7ec9XvC7xBTl2 .node .label text,#mermaid-svg-Rpt7ec9XvC7xBTl2 .image-shape .label,#mermaid-svg-Rpt7ec9XvC7xBTl2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .rough-node .label,#mermaid-svg-Rpt7ec9XvC7xBTl2 .node .label,#mermaid-svg-Rpt7ec9XvC7xBTl2 .image-shape .label,#mermaid-svg-Rpt7ec9XvC7xBTl2 .icon-shape .label{text-align:center;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .node.clickable{cursor:pointer;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .arrowheadPath{fill:#333333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Rpt7ec9XvC7xBTl2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Rpt7ec9XvC7xBTl2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Rpt7ec9XvC7xBTl2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .cluster text{fill:#333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .cluster span{color:#333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Rpt7ec9XvC7xBTl2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .icon-shape,#mermaid-svg-Rpt7ec9XvC7xBTl2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .icon-shape p,#mermaid-svg-Rpt7ec9XvC7xBTl2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .icon-shape .label rect,#mermaid-svg-Rpt7ec9XvC7xBTl2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Rpt7ec9XvC7xBTl2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Rpt7ec9XvC7xBTl2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Rpt7ec9XvC7xBTl2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 回滚
其他
自动/手动
仅构建
publishMode
选 JAR 版本 + SSH 部署
CheckOut → 编译 → 上传 MinIO
publishMode
发布:launch.sh restart
跳过发布
post 邮件
三、时序图(自动发布)
应用服务器 MinIO deploy.groovy Jenkins 应用服务器 MinIO deploy.groovy Jenkins #mermaid-svg-PAgbjE63nN2YTuHp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PAgbjE63nN2YTuHp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PAgbjE63nN2YTuHp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PAgbjE63nN2YTuHp .error-icon{fill:#552222;}#mermaid-svg-PAgbjE63nN2YTuHp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PAgbjE63nN2YTuHp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PAgbjE63nN2YTuHp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PAgbjE63nN2YTuHp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PAgbjE63nN2YTuHp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PAgbjE63nN2YTuHp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PAgbjE63nN2YTuHp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PAgbjE63nN2YTuHp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PAgbjE63nN2YTuHp .marker.cross{stroke:#333333;}#mermaid-svg-PAgbjE63nN2YTuHp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PAgbjE63nN2YTuHp p{margin:0;}#mermaid-svg-PAgbjE63nN2YTuHp .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PAgbjE63nN2YTuHp text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-PAgbjE63nN2YTuHp .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PAgbjE63nN2YTuHp .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-PAgbjE63nN2YTuHp .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-PAgbjE63nN2YTuHp .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-PAgbjE63nN2YTuHp #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-PAgbjE63nN2YTuHp .sequenceNumber{fill:white;}#mermaid-svg-PAgbjE63nN2YTuHp #sequencenumber{fill:#333;}#mermaid-svg-PAgbjE63nN2YTuHp #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-PAgbjE63nN2YTuHp .messageText{fill:#333;stroke:none;}#mermaid-svg-PAgbjE63nN2YTuHp .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PAgbjE63nN2YTuHp .labelText,#mermaid-svg-PAgbjE63nN2YTuHp .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-PAgbjE63nN2YTuHp .loopText,#mermaid-svg-PAgbjE63nN2YTuHp .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-PAgbjE63nN2YTuHp .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PAgbjE63nN2YTuHp .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-PAgbjE63nN2YTuHp .noteText,#mermaid-svg-PAgbjE63nN2YTuHp .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-PAgbjE63nN2YTuHp .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PAgbjE63nN2YTuHp .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PAgbjE63nN2YTuHp .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PAgbjE63nN2YTuHp .actorPopupMenu{position:absolute;}#mermaid-svg-PAgbjE63nN2YTuHp .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-PAgbjE63nN2YTuHp .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PAgbjE63nN2YTuHp .actor-man circle,#mermaid-svg-PAgbjE63nN2YTuHp line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-PAgbjE63nN2YTuHp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} compile(mvn/gradle) → 查找 JAR 1 uploadToMinio + prune(保留 N 个) 2 deployBackendJar 3 buildLaunchScript(模板或自定义) 4 SSH 下载 JAR 5 清理超额旧 JAR(服务器本地) 6 写入 bin/launch.sh 7 sudo -u runUser launch.sh restart 8
四、启动脚本
4.1 脚本模板模式(startScriptType=脚本模板)
共享库 backend-launch-template.sh,支持 start / stop / restart。
停服逻辑(已修复) :按应用目录 ${app_dir} 匹配 Java 进程,而非按带时间戳的 JAR 文件名,避免换 JAR 后旧进程停不掉。
find_pid() {
ps -ef | grep java | grep "${app_dir}/" | grep -v grep | awk '{print $2}' | head -1
}
占位符:{``{PRONAME}} {``{JARNAME}} {``{JVM_OPTS}} {``{APOLLO_*}}
4.2 自定义脚本模式
- 上传
launchScriptFile 或填写 customScriptContent(至少一种)
- 同样支持占位符替换
4.3 启动用户(runUser)
chown -R ${runUser} ${destPath}
sudo -u ${runUser} bash bin/launch.sh restart
五、Jenkins 配置
5.1 参数化构建
基础参数
| 参数名 |
类型 |
说明 |
Tenv |
Choice |
dev/test/prod |
publishMode |
Choice |
必配:自动发布/手动发布/仅构建/回滚 |
projectName |
String |
应用名,JAR 命名与 MinIO 路径 |
buildType |
Choice |
mvn / gradle |
buildshell |
String |
如 clean package -DskipTests |
buildPath |
String |
JAR 搜索目录,默认 target |
destPath |
String |
目标机应用根目录 |
destIp |
String |
部署服务器,逗号分隔 |
artifactRetainCount |
String |
MinIO + 目标机保留 JAR 数,默认 10 |
waitMins / emailUser |
String |
手动超时 / 邮件 |
发布配置
| 参数名 |
说明 |
runUser |
启动用户,默认 app |
startScriptType |
脚本模板 / 自定义脚本 |
customScriptContent |
自定义脚本文本 |
launchScriptFile |
File Parameter 上传脚本 |
JVM / Apollo(模板模式)
| 参数名 |
默认值 |
JVM_OPTS |
-server -Xmx1024m ... |
APOLLO_ENV |
PRO |
APOLLO_META |
http://apollo-eurka-service/ |
APOLLO_NAMESPACES |
bigdata.configuration,application,... |
5.2 制品保留策略
| 位置 |
机制 |
| MinIO |
uploadToMinio 后 pruneMinioArtifacts,保留最新 N 个 JAR |
| 应用服务器 |
部署时删除 {destPath}/{projectName}-*.jar 超额旧文件 |
N 由 artifactRetainCount 控制(默认 10,见 config.DEFAULT_ARTIFACT_RETAIN_COUNT)。
5.3 凭据与环境变量
同前端文档:gitee_registry_ssh、minio-credentials、SSH 凭据;可选 MINIO_ENDPOINT、MINIO_BUCKET(默认 backend-artifacts)、SSH_KEY_CREDENTIAL_ID。
六、流水线阶段
| 阶段 |
条件 |
说明 |
| CheckOut |
≠ 回滚 |
pipeline.checkoutCode |
| 代码编译 |
同上 |
build.compile,find JAR,命名 {projectName}-{BUILD_TIME}-{GIT_COMMIT}.jar |
| 打包并上传 MinIO |
同上 |
cp + uploadToMinio + prune |
| 发布 |
auto/manual |
runPublish → deployBackendJar |
| 回滚 |
rollback |
selectRollbackVersion → deployBackendJar |
| post |
always |
sendPost 合并邮件 |
七、共享库 API
deploy.groovy
| 方法 |
说明 |
uploadToMinio(..., retainCount) |
上传 JAR 并清理 MinIO |
deployBackendJar(...) |
SSH 部署 + launch.sh + 本地 JAR 清理 |
resolveCustomScriptFile(script) |
解析上传的脚本文件 |
buildLaunchScript(内部) |
模板/自定义 + 占位符 |
normalizePublishMode() --- 发布模式
normalizeStartScriptType() --- 脚本模板/自定义
parseRetainCount() --- 解析保留个数
八、典型场景
| 场景 |
publishMode |
备注 |
| 日常 Apollo 发版 |
自动发布 |
startScriptType=脚本模板 |
| 构建不上线 |
仅构建 |
制品仍上传 MinIO |
| 自定义启动脚本 |
自动发布 |
上传 launchScriptFile |
| 回滚 |
回滚 |
使用当前 runUser/脚本配置生成 launch.sh |
| 磁盘控制 |
任意 |
artifactRetainCount=5 |
九、排错
| 现象 |
处理 |
| 未找到 JAR |
检查 buildPath、buildshell |
| 双实例/端口占用 |
确认 launch.sh 已更新(find_pid 按 app_dir) |
| 自定义脚本报错 |
提供 launchScriptFile 或 customScriptContent |
| 回滚版本少 |
retain 过小导致 MinIO 旧包被删,调大保留数 |
| sudo 失败 |
配置 sudoers 或 SSH 用户=runUser |
十、快速检查清单