做后端开发的人,应该都经历过手动部署的痛:本地打包、上传服务器、登录后kill掉旧进程、再启动、盯着日志确认……一套下来十几分钟,遇到测试催着要版本,手忙脚乱还容易把旧包忘备份。后来我把这套流程整理成了基于Jenkins + SpringBoot + GitHub + Maven的自动化部署方案,从那以后提交代码到GitHub,剩下的全交给流水线去处理——拉代码、编译、打包、停进程、备份、发布、自检,一气呵成。
这篇文章适合正在被重复部署折磨的Java后端开发者,也适合团队里想搭建一套简单CI/CD的同学。你不需要懂太多DevOps概念,只要有一台能联网的Linux服务器和一个GitHub账号,按下面的思路一步步来,就能搭出一个稳定、可复用的自动化部署流程。我会尽量讲清楚每一步背后的原因,而不只是给命令,踩过的坑也会一并说出来。
1. 方案总览:一次提交,服务器自动部署
1.1 这套流程里的四个主角
很多人一听到“自动化部署”就以为要上Kubernetes、上容器编排,其实对于大多数中小项目和单机部署场景来说,一套由 Jenkins、GitHub、Maven、SpringBoot 组成的轻量方案已经完全够用。
四个组件的分工很清晰:
- GitHub负责代码托管,是所有构建动作的源头。每次开发者 push 代码,GitHub 都能通过 Webhook 通知 Jenkins 去干活。
- Jenkins是整个流程的调度中心,负责监听代码变化、拉取代码、调用 Maven 构建、执行发布脚本。你可以把它理解成一个“监工”。
- Maven负责把 SpringBoot 项目编译并打成可执行的 jar 包,同时管理第三方依赖的版本和下载。
- SpringBoot本身是内嵌了 Tomcat 的 Java 应用,打成 jar 包后能直接通过
java -jar启动,这正好让部署变的特别简单——不用再在服务器上单独装 Tomcat 或 WebLogic。
这四者的组合天然契合:SpringBoot 项目的产物是一个自包含的 jar,Maven 能一键产出这个 jar,GitHub 能托管代码,Jenkins 则把“拉代码→构建→部署”串联起来。相比起在服务器上手动下载分发包、配置容器,这种方式最大的价值是“可重复”和“可回溯”——哪次构建成功、哪个版本在跑,都能一目了然。
1.2 为什么坚持用这套组合
有人可能会问:我自己写个 Shell 脚本在服务器上git pull再重启服务,不是也能实现自动化吗?是的,小规模场景下完全没问题,这也是不少团队最初的做法。但一旦涉及多人协作、多分支开发、版本回滚、构建历史追溯,纯 Shell 脚本的短板就暴露了:
- 没有统一的构建记录,哪天发布过什么版本全靠“备忘录”。
- 谁触发的构建、发生在哪个分支,事后说不清楚。
- 当项目从单机扩展到多台服务器时,脚本的维护成本会直线上升。
Jenkins 恰好补上了这些短板。它的任务历史、控制台输出、凭据管理、插件生态都是现成的,而且部署一个单机 Jenkins 并不复杂。我后来在多个项目里实践下来,这种“单机 Jenkins + SpringBoot jar”的组合反而是中小团队里性价比最高、最难出错的一套模式。
当然,这套方案也不是没有前提:服务器需要能在网络层面访问 GitHub,同时构建机上最好预留足够的内存给 Maven 和 Jenkins 同时运行。如果是台 1G 内存的小机器,建议给 Maven 的 JVM 参数做一下限制,并在 Jenkins 里关掉不用的插件。
2. 服务器环境准备:先把地基打牢
2.1 安装JDK和Maven,版本匹配很关键
自动化部署的底座是 JDK 和 Maven。注意,这里有一个新手容易忽略的问题:Jenkins 本身是 Java 程序,它需要一套 JRE/JDK 来运行,而 SpringBoot 项目构建又需要另一套 JDK。如果 Jenkins 是用系统自带的 OpenJDK 8 跑起来的,而项目编译要求 JDK 11 或 17,后续就需要在 Jenkins 里额外配置 JDK 路径。
我建议直接在服务器上装 JDK 8,因为目前大多数生产环境 SpringBoot 2.x 项目依然基于 JDK 8,而且 Jenkins 用 JDK 8 运行也完全没有问题。如果以后项目要升级到 JDK 17,再单独下载一个新的 JDK 目录,在 Jenkins 的 “Global Tool Configuration” 里配置多个 JDK 版本即可。
具体安装步骤,以常见的 CentOS 7 为例:
# 安装 OpenJDK 8 yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel # 查看安装路径 echo $JAVA_HOME which javayum装完以后,JAVA_HOME 默认可能在/usr/lib/jvm/java-1.8.0-openjdk,可以把它写进/etc/profile,保证所有用户都能读到。
Maven 的安装更像“解压即用”,去官网下载二进制包后解压到指定目录,然后配置环境变量:
# 下载 apache-maven-3.8.x-bin.tar.gz 并解压到 /usr/local tar -zxf apache-maven-3.8.8-bin.tar.gz -C /usr/local mv /usr/local/apache-maven-3.8.8 /usr/local/maven # 配置环境变量 vi /etc/profile export MAVEN_HOME=/usr/local/maven export PATH=$PATH:$MAVEN_HOME/bin # 重新加载并验证 source /etc/profile mvn -version这里有个建议:如果服务器访问官方 Maven 中央仓库速度不太理想,一定要尽早修改 Maven 的settings.xml,配置一个国内镜像源。不然每次构建光下载依赖就能等上几分钟,体验会很差。
<mirrors> <mirror> <id>nexus-aliyun</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/central</url> </mirror> </mirrors>注意:这里只是示例配置,实际使用时请根据你所在环境选择可用的镜像仓库地址。配置镜像的目的是提升依赖下载速度,但如果企业内网已经有自己的私有仓库,优先使用内部源。
2.2 安装Git和Jenkins,并完成初始化
Git 是拉取代码的基础工具,安装很简单:
yum install -y git git --versionJenkins 的安装方式有两种:一种是用官方 yum 源安装,另一种是直接下载 war 包后用java -jar启动。我个人的经验是:war 包方式更可控,解压到一个固定目录、用nohup或 systemd 管理进程,升级时替换文件就行。yum 方式虽然省事,但是版本升级和目录位置都比较“黑盒”,出了问题反而不容易排查。
war 包方式的启动命令大概是这样的:
# 下载 jenkins.war 到 /opt/jenkins nohup java -jar /opt/jenkins/jenkins.war --httpPort=8080 > /opt/jenkins/jenkins.log 2>&1 &首次启动后,Jenkins 会生成一个初始密码,日志里或者/root/.jenkins/secrets/initialAdminPassword文件里能看到。浏览器访问http://服务器IP:8080,填入密码,再去安装推荐插件,然后创建管理员账号。
这里提醒一下:如果你用的是云服务器,第一次访问不了 Jenkins,十有八九是安全组/防火墙没放行 8080 端口。另外,Jenkins 默认会把所有数据存放在/root/.jenkins,如果你的系统盘空间不大,最好是启动前先设置JENKINS_HOME环境变量,把它指向一个数据盘目录,否则插件装多之后磁盘很容易被打满。
export JENKINS_HOME=/data/jenkins_home初始化完成后,进入 Jenkins 的插件管理页面,确认下面几个插件已经安装:Git Plugin、Maven Integration Plugin、Pipeline Plugin、Credentials Plugin。如果你打算用 Webhook 触发构建,还需要GitHub Integration Plugin。这些都是核心插件,装好之后才能开始配置任务。
3. 打通GitHub与Jenkins:凭据管理决定构建能不能跑
3.1 项目仓库的分支规划
在写 Jenkins 任务之前,先把 GitHub 上的仓库和组织方式想清楚。建议一个简单但实用的分支策略:
main分支:只用来发布,稳定版本都打 tag。develop分支:日常开发集成分支。- 功能分支:例如
feature/xxx、fix/xxx,按需创建。
Jenkins 任务可以针对不同分支建不同任务,比如deploy-dev对应develop分支的自动部署,deploy-prod对应main分支的发布部署。这样做有个好处:开发环境可以随便触发构建,生产环境则通过手动点击构建来严格控制发布时机,避免开发提交直接污染线上。
不建议为了省事把多个环境塞进同一个任务反复切换分支,后期日志会非常乱,而且开发同学很容易误点生产部署。
3.2 用SSH Key配置凭据,而不是账号密码
Git 拉取 GitHub 仓库时,最常见的方式是 HTTPS + 用户名密码,或者 SSH + 密钥。但在 Jenkins 里,强烈建议用 SSH Key。
原因有两点:
- GitHub 已经不支持密码直接拉取仓库,需要生成个人访问令牌(Personal Access Token),令牌一旦过期忘记更换,构建就莫名失败。
- SSH Key 不绑定具体账号密码,部署更稳定,也更好权限隔离。
生成密钥的操作在服务器上执行:
ssh-keygen -t rsa -b 4096 -C "jenkins@your-server" -f /root/.ssh/jenkins_github_key然后把公钥内容复制到 GitHub 账号的 SSH keys 配置里:
cat /root/.ssh/jenkins_github_key.pub再去 Jenkins 的凭据管理里添加一个 SSH 类型的凭据,把生成的私钥jenkins_github_key内容粘贴进去,ID 可以命名为github-ssh。以后 Jenkins 构建时,Git 插件会自动使用这份私钥去连接 GitHub。
实际操作中要注意:私钥的权限不能太松散,建议
chmod 600 /root/.ssh/jenkins_github_key,否则 SSH 会直接拒绝使用这个密钥,导致连接失败。
如果是公司内网自己搭建的 Git 服务(比如 GitLab),原理也是一样的,在 Jenkins 里添加相应的凭据即可,流程完全可以迁移。
3.3 Credentials在Job里的用法
配置完凭据后,在 Pipeline 脚本里可以通过credentials('git-credentials')的方式引用。比如使用 Snippet Generator 生成的 checkout 代码大致是:
git branch: 'develop', credentialsId: 'github-ssh', url: 'git@github.com:your-name/your-project.git'这段代码告诉 Jenkins:从哪个仓库、哪个分支拉代码,使用哪个凭据。凭据 ID 不是 GitHub 上的名字,而是你在 Jenkins 里添加凭据时自己起的 ID,别搞混了。
我见过不少人栽在这个细节上:在 Jenkins 任务里填了项目地址,却忘了选凭据,结果构建报Permission denied (publickey)。这时候不要急着怀疑网络,先回凭据管理页面核对私钥是否复制完整、GitHub 上公钥是否正确。
4. Jenkins流水线全流程:从git pull到jar发布
4.1 构建前的Maven配置
要让 Maven 正确打出可执行 jar,SpringBoot 项目的pom.xml里必须引入 SpringBoot Maven 插件。版本号由spring-boot-starter-parent统一管理,通常不需要单独声明,但要注意packaging必须是jar:
<groupId>com.example</groupId> <artifactId>demo</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <build> <finalName>demo-app</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>这里finalName用来指定最终 jar 的名字,设为demo-app后,构建出来的产物就是demo-app.jar,发布脚本里直接引用这个固定文件名,比带着版本号demo-app-1.0.0.jar方便得多。
在 Jenkins 的构建步骤里,我通常跳过单元测试来缩短构建时间:
mvn clean package -DskipTests注意,-DskipTests会编译测试代码但不执行,如果连测试代码都不想编译,可以用-Dmaven.test.skip=true。日常体验下来,-DskipTests更合理,至少能尽早暴露测试代码本身编译错误。
4.2 编写发布脚本,把“手动操作”变成“一条命令”
Jenkins 构建完成后,产物是一个demo-app.jar。接下来要做的其实是三个动作:停掉旧进程、备份旧包、启动新包。
这段逻辑很适合写成一个独立脚本,例如/opt/deploy/deploy.sh:
#!/bin/bash APP_NAME=demo-app JAR_PATH=/opt/app LOG_PATH=/opt/app/logs BAK_PATH=/opt/app/backup echo "==> 停止旧进程: ${APP_NAME}" PID=$(ps -ef | grep ${APP_NAME}.jar | grep -v grep | awk '{print $2}') if [ -n "${PID}" ]; then kill -9 ${PID} sleep 3 fi echo "==> 备份旧包" if [ -f "${JAR_PATH}/${APP_NAME}.jar" ]; then DATETIME=$(date +%Y%m%d%H%M%S) cp ${JAR_PATH}/${APP_NAME}.jar ${BAK_PATH}/${APP_NAME}_${DATETIME}.jar fi echo "==> 拷贝新包" cp /var/lib/jenkins/workspace/demo-project/target/${APP_NAME}.jar ${JAR_PATH}/ chmod +x ${JAR_PATH}/${APP_NAME}.jar echo "==> 启动新包" nohup java -jar ${JAR_PATH}/${APP_NAME}.jar \ --spring.profiles.active=prod \ --server.port=8081 \ -Xms512m -Xmx512m \ >> ${LOG_PATH}/${APP_NAME}.log 2>&1 & echo "==> 启动完成,等待健康检查" sleep 15 HEALTH_URL="http://127.0.0.1:8081/actuator/health" STATUS=$(curl -s -o /dev/null -w "%{http_code}" ${HEALTH_URL}) if [ "${STATUS}" == "200" ]; then echo "==> 应用启动成功" else echo "==> 应用启动失败,请查看日志" exit 1 fi这段脚本有几个细节值得展开:
- PID 获取方式:
ps -ef | grep ${APP_NAME}.jar能精准匹配进程名,避免误杀其他 Java 进程。如果项目里有多个 SpringBoot 服务,一定不要用一句话java -jar去匹配,否则会全部杀掉。 - 备份策略:每次都按时间戳备份旧包,是为了保留快速回滚的现场。如果线上出问题了,直接把
backup目录下最近的 jar 重新用java -jar启动就行。 - 健康检查:脚本里调用 SpringBoot 的
/actuator/health接口,前提是项目引入了 actuator 依赖。这个检查非常有用,能避免“Jenkins 显示构建成功,但应用其实没有起来”的假象。
如果项目没有引入 actuator,也可以退而求其次用端口探测:
ss -ltn | grep 8081或者curl -I首页地址。但 /actuator/health 能更真实地反映应用内部状态,建议开发阶段就加上。
发布脚本在执行前必须赋予可执行权限:
chmod +x /opt/deploy/deploy.shJenkins 在 Pipeline 里直接调用这个脚本即可:
stage('Deploy') { steps { sh '/opt/deploy/deploy.sh' } }4.3 在Jenkins中配置触发方式
发布脚本写好了,接下来要想的是“什么时候触发构建”。我通常配置两种触发方式:
- 手动触发:主要是生产环境,每次上线前由负责人在 Jenkins 网页上点击“立即构建”,自己确认当前提交就是要发布的内容。
- Webhook 自动触发:开发环境最常见,开发 push 代码到
develop分支后,GitHub 通知 Jenkins 自动执行构建。
Webhook 的配置分两边:
- 在 GitHub 仓库的 Settings → Webhooks 里添加一个 webhook:
- Payload URL:
http://你的Jenkins地址/github-webhook/ - Content type:
application/json - 触发事件:
Just the push event
- Payload URL:
- 在 Jenkins 的 Job 配置里勾选 “GitHub hook trigger for GITScm polling”。
需要注意,Webhook 能否成功触发,和一个前提条件强相关:Jenkins 必须能被 GitHub 在公网上访问到。如果你的 Jenkins 只在内网,GitHub 的请求进不来,Webhook 就不起作用。这种情况下可以退而求其次:在 Jenkins 里配置定时轮询,比如每 5 分钟检查一次远程仓库是否有新提交:
H/5 * * * *定时轮询虽然不如 Webhook 实时,但胜在稳定,不受网络通不通的限制。很多团队在实际部署时都遇到过 Webhook 收不到请求的问题,于是在用 Webhook 的同时也保留一个 5 分钟的轮询兜底,实测效果很好。
完整的 Pipeline 脚本可以参考这样一个结构:
pipeline { agent any tools { maven 'maven-3.8' } stages { stage('Checkout') { steps { git branch: 'develop', credentialsId: 'github-ssh', url: 'git@github.com:your-name/your-project.git' } } stage('Build') { steps { sh 'mvn clean package -DskipTests' } } stage('Deploy') { steps { sh '/opt/deploy/deploy.sh' } } } }如果不想写 Pipeline,也可以用 Jenkins 的“自由风格项目”界面来配置,本质是一样的:源码管理选 Git,构建触发器选轮询或 Webhook,构建步骤里写mvn clean package和sh /opt/deploy/deploy.sh。两种方式我都有过长时间使用,Pipeline 的优势是脚本化、方便入库走版本管理,自由风格的优点是界面直观、新手友好。建议从自由风格入手,跑通之后再迁移到 Pipeline 不迟。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
这套流程上线之后,长时间运行会冒出一堆莫名其妙的问题。我把遇到过的、以及身边同事经常问的问题整理成一张表,方便对照排查。
| 现象 | 最常见原因 | 处理建议 |
|---|---|---|
构建时拉取代码失败Permission denied (publickey) | Jenkins 凭据里私钥没配对,或私钥权限过大 | 重新生成 SSH Key,检查 GitHub 公钥和 Jenkins 私钥是否匹配,私钥权限设为 600 |
| Maven 构建卡在下载依赖 | 默认中央仓库访问太慢 | 在 settings.xml 配置国内镜像源 |
| 构建成功但应用没有起来 | 启动脚本里的健康检查未通过,或日志中端口冲突 | 查看 nohup 日志,重点确认端口是否被占,SpringBoot 配置的 profiling 是否正确 |
| 端口被占用导致应用启动失败 | 旧进程没被完全停止 | 在发布脚本里先精确杀掉指定 jar 的进程,等 3 秒后再启动新包 |
| Webhook 触发了但 Jenkins 没反应 | Jenkins 地址公网不可达,或没有安装对应插件 | 改为轮询触发,或者使用定时构建兜底 |
| Jenkins 重启后任务或插件丢失 | JENKINS_HOME 指向了系统盘临时目录,重启后数据被清空 | 设置 JENKINS_HOME 到数据盘,定期备份该目录 |
| 构建时的 JDK 版本不符合项目要求 | Jenkins 默认用了自身运行时 JDK | 在 Global Tool Configuration 里添加 JDK 安装,并在 Job 中指定 |
5.2 几个值得一试的优化方向
第一,把发布脚本纳入 Git 版本管理。很多人习惯把部署脚本放到服务器上就不管了,一旦服务器重装,脚本丢了就得重写。建议把deploy.sh放在项目仓库的scripts/目录下,Jenkins 构建时可以直接使用工作区里的脚本,这样发布逻辑也跟着代码走,天然有版本记录。
第二,构建历史虽然能看出谁触发了构建,但看不出“这次构建对应哪个 commit 改了什么”。建议在 Pipeline 里加上echo "构建版本: ${GIT_COMMIT}",把 commit 号打到控制台输出里。如果还要更直观,可以调用 GitHub API 换取对应的提交信息。
第三,日志的保留策略。Jenkins 构建日志默认会一直保留,几个月下来磁盘很容易被撑满。可以在 Jenkins 全局配置里设置构建日志保留天数,或者在 Jenkins 系统配置里调整 “Discard Old Builds” 策略。例如保留最近 30 天或最近 50 次构建。
第四,如果项目开始容器化,这套方案也可以平滑演进。只要把“启动 jar 包”换成“构建 Docker 镜像”,把“部署”步骤换成docker run,最外层的 Jenkins 编排逻辑可以基本不变。这也是为什么我始终坚持“先让构建过程沉淀到 Jenkins Pipeline”的原因,它本质上是一层统一的入口,具体把产物扔到哪、用什么方式启动,都可以在脚本里替换。
第五,关于失败通知。我后来在实际使用中给 Pipeline 加了一个post块,当构建失败时直接把失败日志里的关键错误发送到团队消息群,省得同事盯着 Jenkins 页面等结果。类似这样:
post { failure { echo "构建失败,请检查:${BUILD_URL}" // 调用消息通知的接口 } }这个功能看似简单,但能明显减少团队的沟通成本。尤其是开发环境频繁提交时,谁也不想一直盯着构建任务列表看灯亮没亮。
最后说几句
这套 Jenkins + SpringBoot + GitHub + Maven 的自动化部署方案,我前前后后在好几个项目里搭过,最明显的感受是:一旦跑顺了,你几乎不会再想回到手动部署的状态。每次在 GitHub 上 push 完代码,过几分钟打开服务器地址就能看到新功能生效,这种“眼睛一闭一睁,部署完成了”的体验,才是自动化应该有的样子。
如果你正在准备第一次搭建,我建议不要一开始就把所有功能都堆上去。先把“拉代码 → 打包 → 重启服务”这条最小链路跑通,再逐步加上健康检查、备份、Webhook、失败通知。链路越短,出问题时越好定位,跑顺之后再往里面加东西也更有底气。
最后分享一个小技巧:写发布脚本的时候,尽量让每一步都有明确的输出和退出码。Jenkins 判断构建成功与否,最看重的就是最后一条命令的返回值。如果你在脚本某一步用了exit 0强行返回成功,那以后问题排查会非常痛苦。宁可让脚本在应用启动失败时返回非零退出码,让 Jenkins 明确标记这次构建失败,也好过默默吞掉错误,把坏版本发布上去。