Jenkins 部署
本小节,讲解如何使用项目自带的 script/jenkins/Jenkinsfile (opens new window),通过 Jenkins Pipeline 将后端打包成 Jar 包,再使用 SSH 部署到一台 Linux 服务器上。整体流程如下:
Git 仓库(master-jdk17 分支)
│ ① checkout scm 拉取代码
↓
Jenkins Pipeline
│ ② mvn package 打包出 yudao-server.jar(JDK 17),并归档到 Jenkins
│ ③ ssh 检查服务器 => scp 上传 Jar 包 => ssh 备份、停止、替换、启动,并检查启动结果
↓
Linux 服务器的 /work/projects/yudao-server 目录
├── yudao-server.jar # 正在运行的 Jar 包
├── app.env # 运行时配置(数据库、Redis 的地址与密码),只保存在服务器上
├── backup/ # 每次部署前的备份,保留最近 5 个
├── upload/ # 上传中的新 Jar 包
└── nohup.out # 控制台日志
注意:服务器的 IP 地址。
- 外网 IP:139.9.196.247
下属所有涉及到 IP 的配置,需要替换成你自己的。
友情提示:
- 本文部署出来的结果,和 《Linux 部署》 一样,都是
java -jar+ Nginx 的方式,只是把“编译、上传、重启”交给 Jenkins 自动完成。 - 前端使用独立的 Jenkins Job 部署,见「7. 部署前端」小节。
- 如果希望使用 Docker 部署,可参考 《Docker Compose 部署》 文档。
注意:目前只有 `master-jdk17` 分支提供本文使用的 `script/jenkins/Jenkinsfile`!
其它分支的 script/jenkins 目录尚未同步,请以对应分支的实际内容为准,不要直接套用本文。
# 1. 环境准备
# 1.1 安装 Jenkins
参考 Jenkins 官方的 《Installing Jenkins》 (opens new window) 文档,安装 LTS 版本的 Jenkins。需要特别注意 Java 版本:
| 用途 | Java 版本 | 说明 |
|---|---|---|
| 运行 Jenkins 本身(Controller、Agent) | 21 或 25 | Jenkins 已经不再支持 Java 17,使用不支持的 Java 版本,Jenkins 无法启动 |
编译项目(Jenkinsfile 中的 mvn 命令) | 17 | 与 Jenkins 自身的 Java 版本无关,在「1.3 配置构建工具」中单独指定 |
具体可见 Jenkins 官方的 《Java Support Policy》 (opens new window) 文档。
注意:请使用「Pipeline(流水线)」类型的 Job,不要使用「构建一个 Maven 项目」(Maven Integration 插件)类型!
Maven Integration 插件可能要求编译使用的 JDK 不低于 Jenkins 自身的 Java 版本(21 或 25),与项目的 JDK 17 冲突。本文在 Pipeline 的 sh 步骤中直接执行 mvn 命令,没有这个限制。
# 1.2 安装插件
在 [Manage Jenkins -> Plugins] 菜单中,确认已经安装如下插件:
| 插件 | 用途 |
|---|---|
| Pipeline (opens new window) | 执行 Jenkinsfile |
| Git (opens new window) | 拉取代码 |
| Credentials Binding (opens new window) | 读取 known_hosts 文件凭据 |
| SSH Agent (opens new window) | 使用 SSH 私钥凭据,执行 ssh、scp 命令 |
前 3 个属于安装 Jenkins 时的“推荐插件”,一般已经安装;SSH Agent 插件需要手动安装。
# 1.3 配置构建工具
在 [Manage Jenkins -> Tools] 菜单中,新增如下两个工具,名字必须与 Jenkinsfile 中 tools 的配置一致:
| 类型 | 名字 | 说明 |
|---|---|---|
| JDK | jdk17 | JDK 17 的安装目录,即 JAVA_HOME |
| Maven | maven3 | Maven 3.9.x 的安装目录 |
执行构建的机器(Jenkins 内置节点或 Agent),需要是 Linux 系统,并安装 git,以及 ssh、scp、ssh-agent 命令(一般在 openssh-clients 或 openssh-client 软件包中)。生产环境建议使用独立的 Agent 执行构建,而不是 Jenkins 内置节点。
# 1.4 准备服务器
① 参考 《Linux 部署》 的「1. 环境准备」小节,安装 MySQL、Redis、Nginx,并导入 sql/mysql/ruoyi-vue-pro.sql 脚本。另外:
- 还需要导入
sql/mysql/quartz.sql脚本:application-dev.yaml开启了 Quartz 定时任务,并使用数据库存储,缺少QRTZ_开头的表时,后端会启动失败。 - JDK 需要 17 或以上的版本,例如说执行
yum install -y java-17-openjdk命令安装,而不是《Linux 部署》中的 JDK 8。 - 需要有
bash、curl、pgrep、ss命令,大部分 Linux 发行版默认自带。其中,ss用于检查本机所有地址上的端口监听情况,没有时,CentOS / RHEL 执行yum install -y iproute,Debian / Ubuntu 执行apt-get install -y iproute2安装。部署脚本还会读取/proc/<进程号>/cmdline识别进程,所以只支持 Linux 服务器。
② 执行如下命令,创建部署目录与运行时配置 app.env:
mkdir -p /work/projects/yudao-server
vi /work/projects/yudao-server/app.env
chmod 600 /work/projects/yudao-server/app.env
app.env 的内容如下,按照你的环境修改:
## 必填:Spring Profile,对应 Jar 包中的 application-dev.yaml
SPRING_PROFILES_ACTIVE=dev
## 可选:JVM 参数,不填时为 -Xms512m -Xmx512m
JAVA_OPTS='-Xms512m -Xmx512m'
## 可选:JDK 的安装目录,不填时使用 PATH 中的 java 命令
#JAVA_HOME=/usr/lib/jvm/jre-17
## MySQL:主库 master、从库 slave 都要设置,否则会使用 application-dev.yaml 中的地址
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL='jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true'
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME=root
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD='请修改为你的密码'
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_SLAVE_URL='jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true'
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_SLAVE_USERNAME=root
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_SLAVE_PASSWORD='请修改为你的密码'
## Redis:Redis 库索引沿用 application-dev.yaml 中的 1
SPRING_DATA_REDIS_HOST=127.0.0.1
SPRING_DATA_REDIS_PORT=6379
## Redis 设置了密码时,才取消下面这行的注释,并填写真实的密码。Redis 没有密码时,不要保留空值,否则会以空密码执行 AUTH,导致连接失败
#SPRING_DATA_REDIS_PASSWORD='请修改为你的密码'
- 部署时,会以
set -a; . app.env; set +a的方式加载它,所以只能写KEY=VALUE格式;值包含&、空格等特殊字符时,使用单引号包裹。 - 这些环境变量,会覆盖 Jar 包中
application-dev.yaml的同名配置,例如说SPRING_DATA_REDIS_HOST对应spring.data.redis.host。所以不需要为了修改地址、密码而修改源码。其它配置项同理,例如说YUDAO_CAPTCHA_ENABLE=false关闭登录验证码。 - 如需修改后端端口,增加
SERVER_PORT=xxx即可,启动检查也会使用它。 app.env只保存在服务器上,不要提交到 Git 仓库,也不要放到 Jenkins 中。
③ 建议使用专用的部署用户(例如说 yudao),而不是 root。此时,需要执行 chown -R yudao:yudao /work/projects/yudao-server 命令,保证该用户对部署目录有读写权限。
# 2. 配置凭据
在 [Manage Jenkins -> Credentials] 菜单中,新建如下两个凭据。它们的 ID,需要与 Jenkinsfile 中的 SSH_CREDENTIAL_ID、KNOWN_HOSTS_CREDENTIAL_ID 一致。
# 2.1 SSH 私钥
① 在一台可信的机器上,执行如下命令,生成专门用于部署的密钥对:
ssh-keygen -t ed25519 -C jenkins-deploy -f ./jenkins-deploy
② 将公钥 jenkins-deploy.pub 的内容,追加到服务器上部署用户的 ~/.ssh/authorized_keys 文件中。
③ 在 Jenkins 中新建凭据:
- Kind:
SSH Username with private key - ID:
yudao-deploy-ssh - Username:部署用户,例如说
root。仅作为说明,实际登录的用户以构建参数DEPLOY_USER为准 - Private Key:选择 [Enter directly],粘贴私钥
jenkins-deploy的内容。生成密钥时设置了密码的话,填写 Passphrase
④ 导入完成后,删除本地的私钥文件。
# 2.2 known_hosts
Jenkinsfile 使用 StrictHostKeyChecking=yes 连接服务器,只信任这个凭据中记录的服务器公钥,从而避免连接到伪造的服务器。
① 在 Jenkins 所在的机器上,执行如下命令,获取服务器的公钥,并查看它的指纹:
## SSH 端口不是 22 时,需要修改 -p 参数
ssh-keyscan -p 22 -t ed25519 139.9.196.247 > known_hosts
ssh-keygen -lf known_hosts
② 通过云厂商的控制台等可信方式登录服务器,执行如下命令,查看服务器真实的公钥指纹:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
③ 确认两个 SHA256:xxx 指纹完全一致后,在 Jenkins 中新建凭据:
- Kind:
Secret file - ID:
yudao-deploy-known-hosts - File:上传第 ① 步生成的
known_hosts文件
注意:不要使用 `StrictHostKeyChecking=no` 跳过服务器公钥的校验!
ssh-keyscan 不会校验获取到的公钥是否可信,所以一定要执行第 ② 步比对指纹。
服务器重装系统、更换 SSH 主机密钥后,部署会报 Host key verification failed. 错误,需要重新执行上述步骤,更新凭据。部署到多台服务器时,把它们的 ssh-keyscan 结果合并到同一个 known_hosts 文件即可。
友情提示:
能修改 Jenkinsfile 的人,就能使用这两个凭据登录服务器。请限制 Git 仓库的写权限,以及 Jenkins 中 Job 的配置权限。
# 3. 新建 Job
① 在 Jenkins 首页,点击 [New Item] 按钮,名字填写 yudao-server(不要包含空格),类型选择 [Pipeline]。
② 在 [Pipeline] 部分,填写如下配置,然后保存:
| 配置项 | 值 |
|---|---|
| Definition | Pipeline script from SCM |
| SCM | Git |
| Repository URL | 你的后端仓库地址,例如说 https://github.com/YunaiV/ruoyi-vue-pro.git。私有仓库需要选择 Credentials |
| Branch Specifier | */master-jdk17 |
| Script Path | script/jenkins/Jenkinsfile |
③ 点击 [Build Now] 按钮,执行一次构建。第一次构建会在「检查参数」阶段失败,提示 DEPLOY_HOST 必填,这是正常的:Jenkins 需要先执行一次 Jenkinsfile,才能读取到其中的参数定义。之后,[Build Now] 按钮会变成 [Build with Parameters]。
# 3.1 构建参数
| 参数 | 默认值 | 说明 |
|---|---|---|
DEPLOY_HOST | 空 | 必填,服务器的 IPv4 地址或域名,例如说 139.9.196.247 |
DEPLOY_USER | root | SSH 登录的用户 |
DEPLOY_SSH_PORT | 22 | SSH 端口 |
DEPLOY_BASE_DIR | /work/projects | 部署根目录,实际部署到 /work/projects/yudao-server |
参数会拼接到 ssh、scp 命令中,所以「检查参数」阶段会严格校验格式,不允许包含空格、引号、; 等字符,避免命令注入。
# 3.2 固定配置
如下配置写在 Jenkinsfile 中,如需修改,修改 Jenkinsfile 后提交到 Git 仓库即可:
| 配置 | 值 | 说明 |
|---|---|---|
APP_NAME | yudao-server | 服务器上的目录名、Jar 包名 |
APP_MODULE | yudao-server | Maven 模块 |
APP_JAR | yudao-server/target/yudao-server.jar | 打包出的 Jar 包 |
APP_PORT | 48080 | 启动检查使用的端口。app.env 中设置了 SERVER_PORT 时,以它为准 |
SSH_CREDENTIAL_ID | yudao-deploy-ssh | 见「2.1 SSH 私钥」 |
KNOWN_HOSTS_CREDENTIAL_ID | yudao-deploy-known-hosts | 见「2.2 known_hosts」 |
tools | jdk17、maven3 | 见「1.3 配置构建工具」 |
stop_timeout、start_timeout、backup_keep | 60、180、5 | 在 REMOTE_SCRIPT 的开头:停止的最长等待秒数、启动检查的最长等待秒数、保留的备份个数 |
# 4. 执行部署
点击 [Build with Parameters] 按钮,填写 DEPLOY_HOST 等参数,再点击 [Build] 按钮。Pipeline 会依次执行如下阶段:
| 阶段 | 说明 |
|---|---|
| 拉取代码 | 执行 checkout scm,拉取 Job 中配置的仓库与分支 |
| 检查参数 | 校验构建参数的格式 |
| 编译 | 执行 mvn -B -ntp clean package -pl yudao-server -am -Dmaven.test.skip=true 命令,只编译 yudao-server 及其依赖的模块 |
| 归档 | 将 yudao-server.jar 归档到 Jenkins,保留最近 5 次构建的 Jar 包 |
| 部署 | 通过 SSH 部署到服务器,见下文 |
「部署」阶段,会依次执行:
- ① 检查服务器:部署目录、
app.env、Java、curl、pgrep是否就绪,并创建upload、backup目录。如果yudao-server没有运行,但 48080 端口已经被其它进程占用,直接失败。 - ② 上传:使用
scp命令,将 Jar 包上传到upload/yudao-server.jar。 - ③ 部署:备份当前的 Jar 包到
backup目录 =>kill -15停止旧进程(60 秒未停止时kill -9),确认进程已经退出 => 确认端口已经空闲 => 替换 Jar 包 => 加载app.env后,使用nohup java -jar启动 => 启动检查。
① 和 ③ 在服务器上执行的脚本,直接写在 Jenkinsfile 的 REMOTE_SCRIPT 变量中,每次部署时,通过 ssh ... bash -s 传给服务器执行,所以服务器上不需要额外放置 deploy.sh 脚本。
疑问:旧进程是怎么找到的?
只停止 java 进程中,-jar 后面的参数与 /work/projects/yudao-server/yudao-server.jar 完全一致的进程:逐个比较 /proc/<进程号>/cmdline 中的参数,而不是使用 pgrep -f 的正则匹配,所以不会误停同目录下其它 Jar 包的进程。
进程在 kill -9 后仍然没有退出时(例如说属于其它用户,没有权限停止),部署会直接失败,不会替换 Jar 包。
疑问:启动检查是怎么判断的?
在启动新进程之前,先使用 ss 确认端口在本机所有地址上都没有被任何进程监听,避免把其它进程的响应误认为启动成功。启动后,在 180 秒内,新进程没有退出,并且 http://127.0.0.1:48080/ 返回了 HTTP 状态码(小于 500),就认为启动完成。
项目默认没有引入 spring-boot-starter-actuator 依赖,所以没有使用 /actuator/health 接口。它只能说明服务已经启动、端口可以访问,不代表业务健康,业务是否正常,还需要参考「5. 验证」小节。
疑问:-Dmaven.test.skip=true 是什么意思?
跳过单元测试的执行。如果你项目的单元测试写的不错,可以修改 Jenkinsfile,去掉该参数,保证交付的质量。
# 4.1 首次部署
首次部署时,服务器上还没有 Jar 包,不需要备份和停止。日志如下(Java 版本以实际为准):
[check] Java:openjdk version "17.0.12" 2024-07-16
[check] Profile:dev,端口:48080
[check] 首次部署:/work/projects/yudao-server/yudao-server.jar 不存在,无需备份
[backup] 首次部署,无需备份
[stop] yudao-server 未运行,无需停止
[start] 启动 /work/projects/yudao-server/yudao-server.jar,Profile:dev
[start] 进程号:12345
[start] http://127.0.0.1:48080/ 已响应,启动完成
[deploy] 部署成功:yudao-server
如果之前按照 《Linux 部署》 在 /work/projects/yudao-server 目录启动过 yudao-server.jar,并且启动命令中使用的是 -jar /work/projects/yudao-server/yudao-server.jar 这个完整路径,Jenkins 会找到并停止它。注意,部署用户需要有权限停止该进程。
# 4.2 再次部署
再次部署时,会先备份当前的 Jar 包,再停止旧进程。日志如下:
[check] 已存在 /work/projects/yudao-server/yudao-server.jar,部署前会先备份
[backup] 已备份到 /work/projects/yudao-server/backup/yudao-server-20261025103000.jar
[stop] 停止进程:12345
[stop] 已停止
[start] 启动 /work/projects/yudao-server/yudao-server.jar,Profile:dev
[start] 进程号:12346
[start] http://127.0.0.1:48080/ 已响应,启动完成
[deploy] 部署成功:yudao-server
部署成功后,backup 目录只保留最近 5 个备份(按文件名中的时间戳判断新旧)。旧备份没有权限删除时,会输出警告并继续,此时备份可能超过 5 个,需要检查权限后手动清理。
# 4.3 部署失败
任一步骤失败时,构建会被标记为失败。常见的错误如下:
| 日志 | 原因与解决 |
|---|---|
DEPLOY_HOST 必填,只能是 IPv4 地址或域名 | 构建参数没有填写,或者格式不正确 |
does not have an install of "jdk17" | 没有完成「1.3 配置构建工具」,或者名字不一致 |
找不到 yudao-deploy-ssh、yudao-deploy-known-hosts 凭据 | 没有完成「2. 配置凭据」,或者 ID 不一致 |
Host key verification failed. | known_hosts 中没有该服务器(注意 IP 与端口,要和 DEPLOY_HOST、DEPLOY_SSH_PORT 一致),或者服务器公钥发生了变化。按照「2.2 known_hosts」重新生成 |
Permission denied (publickey...) | 公钥没有加入 DEPLOY_USER 用户的 ~/.ssh/authorized_keys 文件 |
[deploy] 失败:目录 ... 不存在、[deploy] 失败:缺少运行时配置 .../app.env | 首次部署前,没有完成「1.4 准备服务器」 |
[deploy] 失败:找不到 Java:java | 服务器 PATH 中没有 java 命令,在 app.env 中设置 JAVA_HOME 即可 |
[deploy] 失败:端口 48080 已被其它进程占用、[deploy] 失败:停止 yudao-server 后,端口 48080 仍被其它进程占用 | 有其它进程监听该端口,此时新 Jar 包没有被替换。注意,第二种情况下,旧的 yudao-server 进程已经被停止(例如说 app.env 中修改了 SERVER_PORT,而新端口被占用),并且不会自动恢复。执行 ss -ltnp 找到并处理占用端口的进程(或者改回 SERVER_PORT)后,重新构建;也可以先执行「6. 回滚」代码块开头的 cd 与 JAR=... 两行,再执行第 ④、⑥、⑦ 步,手动启动当前的 Jar 包 |
[deploy] 失败:进程 ... 无法停止 | 旧进程在 kill -9 后仍然存在,常见原因是它属于其它用户,此时 Jar 包没有被替换。使用该进程所属的用户停止它后,重新构建 |
[start] 进程已退出、[start] 等待 180 秒后,... 仍未响应 | 后面会打印 nohup.out 的最后 100 行,常见原因是数据库、Redis 的地址或密码错误,内存不足等。修改 app.env 后,重新构建即可 |
注意:部署中途失败时,旧版本可能已经被停止!
「停止后端口仍被占用」与「启动检查失败」这两种失败,发生在旧进程停止之后,服务会中断。Jenkins 不会自动回滚,也不会自动恢复旧进程:
- 停止后端口仍被占用:新 Jar 包没有被替换。处理端口占用后重新构建,或者手动启动当前的 Jar 包即可。
- 启动检查失败:新 Jar 包已经替换,日志中会打印备份 Jar 包的路径。由你确认原因后,参考「6. 回滚」小节手动回滚。
# 5. 验证
① 在服务器上,执行如下命令,确认后端可以访问:
curl http://127.0.0.1:48080/admin-api/
返回 {"code":401,"data":null,"msg":"账号未登录"} 即可。
② 执行如下命令,验证 后端 => MySQL 正常,并且 SQL 已经初始化:
curl -G --data-urlencode "name=芋道源码" http://127.0.0.1:48080/admin-api/system/tenant/get-id-by-name
返回 {"code":0,"data":1,"msg":""},说明从数据库中查询到了默认租户。
③ 参考 《Linux 部署》 的「4. 配置 Nginx 转发」小节,配置 Nginx。只需要配置一次,Jenkins 部署不会修改 Nginx 的配置。
④ 部署前端后(见「7. 部署前端」小节),请求 http://139.9.196.247 (opens new window) 地址(“需要换成你的外网 IP”),使用默认账号 admin、密码 admin123 登录。
⑤ 查看日志:
tail -f /work/projects/yudao-server/nohup.out ## 控制台日志
tail -f ~/logs/yudao-server.log ## 应用日志文件,位于部署用户的 home 目录下
# 6. 回滚
在服务器上,使用 bash 执行如下命令,回滚到某个备份的 Jar 包:
cd /work/projects/yudao-server
JAR=/work/projects/yudao-server/yudao-server.jar
## ① 查看备份,选择要回滚的版本。文件名中的时间戳就是备份的时间(文件的修改时间是原 Jar 包的时间,不要以它为准)
ls -1 backup/
## ② 找到 java 进程中,-jar 后面的参数与 Jar 路径完全一致的进程,并确认输出的进程号
PIDS=$(for p in $(pgrep -x java); do tr '\0' '\n' < /proc/$p/cmdline 2> /dev/null | grep -A1 -xF -- -jar | grep -qxF -- "$JAR" && echo $p; done)
echo "$PIDS"
## ③ 停止这些进程,并等待它们退出。超过 60 秒仍未退出时,按 Ctrl+C 中断,执行 kill -9 $PIDS 后,再重新执行 for 循环
kill -15 $PIDS
for p in $PIDS; do while [ -e /proc/$p ]; do sleep 1; done; done
## ④ 确认端口已经空闲:它会列出本机所有地址上监听该端口的 TCP 套接字,只输出表头、没有 LISTEN 行时,才能继续。app.env 中设置了 SERVER_PORT 时,替换为该端口
ss -ltn 'sport = :48080'
## ⑤ 恢复备份的 Jar 包。文件名,需要换成第 ① 步选择的
cp -p backup/yudao-server-20261025103000.jar "$JAR"
## ⑥ 加载 app.env 后启动,与 Jenkins 的启动方式一致
(set -a; . ./app.env; set +a; nohup "${JAVA_HOME:+$JAVA_HOME/bin/}java" ${JAVA_OPTS:--Xms512m -Xmx512m} -jar "$JAR" > nohup.out 2>&1 < /dev/null &)
## ⑦ 查看启动日志
tail -f nohup.out
- 第 ② 步没有输出进程号时,说明服务没有运行,跳过第 ③ 步即可。
- 备份被清理时,也可以在 Jenkins 历史构建的 [Build Artifacts] 中,下载对应版本的
yudao-server.jar,上传到服务器的backup目录,并按照备份的格式命名,时间戳使用上传时的时间,例如说/work/projects/yudao-server/backup/yudao-server-20261025120000.jar。不要直接覆盖正在运行的yudao-server.jar。然后,从第 ② 步开始执行,第 ⑤ 步的源文件使用这个文件名。 - 回滚只会替换 Jar 包,不会回滚数据库。如果新版本执行过升级 SQL,请先确认旧版本能否兼容。
# 7. 部署前端
前端与后端分开构建、分开部署:在 Jenkins 中为前端单独新建一个 Job,它的 Jenkinsfile 放在你自己的前端仓库中(后端仓库不包含前端代码)。最终,前端打包出的静态文件,部署到服务器的 /work/projects/yudao-ui-admin 目录,与 《Linux 部署》 一致。
# 7.1 修改配置
前端和后端通过同一个 Nginx 同源访问,所以“后端地址”要填写 Nginx 的访问地址(例如说 http://139.9.196.247),修改后提交到你的前端仓库:
| 前端项目 | 配置文件 | 修改的配置项 | 打包命令 => 产物目录 |
|---|---|---|---|
yudao-ui-admin-vue3 | .env.prod | VITE_BASE_URL='http://139.9.196.247',VITE_API_URL=/admin-api 保持不变 | pnpm build:prod => dist-prod |
yudao-ui-admin-vben(ant-design-vue) | apps/web-antd/.env.production | VITE_BASE_URL=http://139.9.196.247,VITE_GLOB_API_URL=/admin-api | pnpm build:antd => apps/web-antd/dist |
yudao-ui-admin-vben(element-plus) | apps/web-ele/.env.production | 同上 | pnpm build:ele => apps/web-ele/dist |
- 前端的
VITE_APP_CAPTCHA_ENABLE,要与后端的验证码开关一致,默认都是true。 - 执行构建的机器,需要安装 Node.js 与 pnpm:
yudao-ui-admin-vue3:Node.js 20.19.0 及以上,pnpm 9(已验证 9.15.9)。虽然它package.json的engines写的是 pnpm 8.6.0 及以上,但pnpm-lock.yaml是 v9 格式,使用 pnpm 8 执行pnpm install --frozen-lockfile会报ERR_PNPM_LOCKFILE_BREAKING_CHANGE错误。yudao-ui-admin-vben:Node.js 22.18.0+ 或 24.12.0+,pnpm 11 及以上(packageManager为pnpm@11.16.0)。- 可以执行
corepack enable、corepack prepare pnpm@9.15.9 --activate命令:没有声明packageManager的yudao-ui-admin-vue3使用 pnpm 9.15.9;yudao-ui-admin-vben会按照自己声明的packageManager,使用 pnpm 11.16.0。
# 7.2 新增 Jenkinsfile
在你的前端仓库根目录下,新建 Jenkinsfile 文件,内容如下:
#!groovy
// 前端的 Jenkins 部署流水线:pnpm 打包,再通过 SSH 部署到 Nginx 的静态文件目录
// 在服务器上执行的脚本:解压到新目录 => 备份旧目录 => 切换到新目录
def REMOTE_SCRIPT = '''
set -euo pipefail
dir="$1"
backup="$dir-backup/$(date +%Y%m%d%H%M%S)"
[ ! -e "$backup" ] || { echo "[deploy] 失败:$backup 已存在,请稍后重试" >&2; exit 1; }
rm -rf "$dir.new"
mkdir "$dir.new"
tar -xzf "$dir.tar.gz" -C "$dir.new"
[ -f "$dir.new/index.html" ] || { echo "[deploy] 失败:产物中没有 index.html" >&2; exit 1; }
mkdir -p "$dir-backup"
if [ -d "$dir" ]; then
mv "$dir" "$backup"
echo "[backup] 已备份到 $backup"
else
echo "[backup] 首次部署,无需备份"
fi
mv "$dir.new" "$dir"
rm -f "$dir.tar.gz"
# 只保留最近 5 个备份:目录名是时间戳,按名称排序(C 语言环境)即为时间顺序。不使用修改时间,因为 mv 会保留原目录的修改时间
LC_ALL=C
backups=("$dir-backup"/[0-9]*)
if [ -e "${backups[0]}" ]; then
for ((i = 0; i < ${#backups[@]} - 5; i++)); do
rm -rf -- "${backups[i]}" || echo "[backup] 警告:无法删除旧备份 ${backups[i]},请检查权限后手动清理" >&2
done
fi
echo "[deploy] 部署成功:$dir"
'''
pipeline {
agent any
options {
skipDefaultCheckout()
disableConcurrentBuilds()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
parameters {
string(name: 'DEPLOY_HOST', defaultValue: '', trim: true, description: '目标服务器的 IP 或域名(必填)')
string(name: 'DEPLOY_USER', defaultValue: 'root', trim: true, description: 'SSH 登录用户')
string(name: 'DEPLOY_SSH_PORT', defaultValue: '22', trim: true, description: 'SSH 端口')
string(name: 'DEPLOY_DIR', defaultValue: '/work/projects/yudao-ui-admin', trim: true, description: '前端部署目录,与 Nginx 的 root 一致')
}
environment {
// 打包命令与产物目录,见「7.1 修改配置」的表格。例如说 yudao-ui-admin-vben 为 build:antd、apps/web-antd/dist
BUILD_SCRIPT = 'build:prod'
DIST_DIR = 'dist-prod'
// Jenkins 凭据 ID,与后端共用
SSH_CREDENTIAL_ID = 'yudao-deploy-ssh'
KNOWN_HOSTS_CREDENTIAL_ID = 'yudao-deploy-known-hosts'
}
stages {
stage('拉取代码') {
steps {
checkout scm
}
}
stage('检查参数') {
steps {
script {
// 参数会拼接到 ssh、scp 命令中,必须严格校验,避免命令注入
if (!(params.DEPLOY_HOST ?: '').matches('[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?')) {
error('DEPLOY_HOST 必填,只能是 IPv4 地址或域名')
}
if (!(params.DEPLOY_USER ?: '').matches('[a-z_][a-z0-9_-]{0,31}')) {
error('DEPLOY_USER 不合法,只能包含小写字母、数字、下划线、中划线')
}
def port = params.DEPLOY_SSH_PORT ?: ''
if (!port.matches('[0-9]{1,5}') || port.toInteger() < 1 || port.toInteger() > 65535) {
error('DEPLOY_SSH_PORT 必须是 1 ~ 65535 的数字')
}
def dir = params.DEPLOY_DIR ?: ''
if (!dir.matches('/[A-Za-z0-9._/-]*[A-Za-z0-9_-]') || dir.contains('..') || dir.contains('//')) {
error('DEPLOY_DIR 必须是绝对路径,只能包含字母、数字和 . _ - /,不能以 / 结尾')
}
}
}
}
stage('编译') {
steps {
sh 'node -v && pnpm -v'
sh 'pnpm install --frozen-lockfile'
sh 'pnpm run "$BUILD_SCRIPT"'
sh 'test -f "$DIST_DIR/index.html"'
sh 'tar -czf dist.tar.gz -C "$DIST_DIR" .'
}
}
stage('部署') {
steps {
writeFile file: '.jenkins-remote-deploy.sh', text: REMOTE_SCRIPT
withCredentials([file(credentialsId: env.KNOWN_HOSTS_CREDENTIAL_ID, variable: 'KNOWN_HOSTS_FILE')]) {
sshagent(credentials: [env.SSH_CREDENTIAL_ID]) {
sh '''
set -eu
case "$KNOWN_HOSTS_FILE" in
*[[:space:]]*) echo "known_hosts 文件路径不能包含空白字符,请修改 Job 名称" >&2; exit 1 ;;
esac
scp -P "$DEPLOY_SSH_PORT" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=yes -o GlobalKnownHostsFile=/dev/null -o "UserKnownHostsFile=$KNOWN_HOSTS_FILE" dist.tar.gz "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR.tar.gz"
ssh -p "$DEPLOY_SSH_PORT" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=yes -o GlobalKnownHostsFile=/dev/null -o "UserKnownHostsFile=$KNOWN_HOSTS_FILE" "$DEPLOY_USER@$DEPLOY_HOST" bash -s -- "$DEPLOY_DIR" < .jenkins-remote-deploy.sh
'''
}
}
}
}
}
}
如果使用的是 yudao-ui-admin-vben,把 BUILD_SCRIPT、DIST_DIR 修改为 build:antd、apps/web-antd/dist(或者 build:ele、apps/web-ele/dist)即可。
# 7.3 新建 Job
① 首次部署前,在服务器上执行 mkdir -p /work/projects 命令,保证部署目录的上级目录存在,并且部署用户有写权限。
② 在 Jenkins 中新建一个 [Pipeline] 类型的 Job,名字例如说 yudao-ui-admin。配置与「3. 新建 Job」相同,只是 Repository URL 填写你的前端仓库地址,Branch Specifier 填写前端的分支(例如说 */master),Script Path 填写 Jenkinsfile。
③ 与后端相同,第一次构建会失败,之后使用 [Build with Parameters] 构建。构建参数为 DEPLOY_HOST、DEPLOY_USER、DEPLOY_SSH_PORT,以及 DEPLOY_DIR(默认 /work/projects/yudao-ui-admin)。
部署成功后,旧的前端文件会备份到 /work/projects/yudao-ui-admin-backup 目录,只保留最近 5 个。旧备份没有权限删除时,会输出警告并继续,此时备份可能超过 5 个,需要检查权限后手动清理。回滚时,执行如下命令即可(目录名需要换成你要回滚的版本):
cd /work/projects
ls -1 yudao-ui-admin-backup/ ## 目录名就是备份的时间,选择要回滚的版本
mv yudao-ui-admin "yudao-ui-admin-failed-$(date +%Y%m%d%H%M%S)"
mv yudao-ui-admin-backup/20261025103000 yudao-ui-admin
# 666. 更多说明
① Jenkins Pipeline 的更多用法,可见 Jenkins 官方的 《Using a Jenkinsfile》 (opens new window)、《Pipeline Syntax》 (opens new window) 文档,以及 SSH Agent 插件 (opens new window) 的说明。
② 部署到多台服务器时,每台服务器各执行一次构建(修改 DEPLOY_HOST 参数)即可,Jenkinsfile 一次只部署一台服务器。
③ 如果 Jenkins 构建结束后,后端进程也跟着被停止,检查服务器 /etc/systemd/logind.conf 中的 KillUserProcesses 配置,或者对部署用户执行 loginctl enable-linger 命令。
④ 如果需要 HTTPS 访问,可参考 《HTTPS 证书》 文档。