《企业级后端部署方案:Jenkins+MinIO+SSH+Gitee+Jenkinsfile自动化实践》

企业级后端部署方案: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 目标机目录结构

text 复制代码
{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 后旧进程停不掉。

bash 复制代码
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)

bash 复制代码
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 uploadToMiniopruneMinioArtifacts,保留最新 N 个 JAR
应用服务器 部署时删除 {destPath}/{projectName}-*.jar 超额旧文件

N 由 artifactRetainCount 控制(默认 10,见 config.DEFAULT_ARTIFACT_RETAIN_COUNT)。

5.3 凭据与环境变量

同前端文档:gitee_registry_sshminio-credentials、SSH 凭据;可选 MINIO_ENDPOINTMINIO_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(内部) 模板/自定义 + 占位符

tools.groovy

  • 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

十、快速检查清单

  • 共享库含 backend-launch-template.sh
  • 目标机已创建 runUser,sudo 权限正确
  • publishMode、artifactRetainCount 已配置
  • projectName 与 MinIO 路径一致
  • Apollo 地址在目标机可达(模板模式)