Jenkins CI/CD 实战:Vite 前端发布、Koa + PM2 后端部署、权限隔离与远程发布
本文以 Jenkins Freestyle Project 为基础,完整实现一套前后端项目的持续集成与持续部署流程。
前端项目采用 Vite,Jenkins 负责拉取代码、安装依赖和执行构建,生成的 dist 通过 rsync 发布到 Nginx 静态目录;后端项目采用 Koa + TypeScript,通过 npm run build 编译到 dist,再以 npm run start 作为统一启动入口,并交由 PM2 管理。
在此基础上,进一步使用 Role-based Authorization Strategy 按 Job 前缀实现项目级权限隔离,并补充 Publish Over SSH 远程部署方案,使 Jenkins 与业务服务器分离时仍能保持清晰的构建与发布边界。
示例环境如下:
text
Jenkins 服务器:192.168.31.88
远程业务服务器:192.168.31.90
Jenkins 端口:8080
Git 仓库:https://gitee.com/linhao-dev/side-blog.git
Jenkins 初始化管理员:linhao
前端 Job:web_test
后端 Job:api_test
前端发布目录:/home/html/web_test
后端发布目录:/home/apps/api_test
Koa 端口:3000
示例仓库采用前后端同仓结构:
text
side-blog/
├── web/
│ ├── src/
│ ├── package.json
│ ├── pnpm-lock.yaml
│ └── vite.config.ts
│
└── api/
├── src/
├── package.json
├── package-lock.json
└── tsconfig.json
如果前端与后端分别位于独立仓库,只需去掉后续构建脚本中的 cd web 或 cd api。
整体流程如下:
text
Git 仓库
│
▼
Jenkins
├── web_test
│ │
│ ├─ pnpm install
│ ├─ npm run build
│ ▼
│ web/dist
│ │
│ ├─ 同机:rsync
│ └─ 远程:Publish Over SSH
│ │
│ ▼
│ Nginx
│
└── api_test
│
├─ npm ci
├─ npm run build
▼
api/dist
│
├─ 同机:同步到运行目录
└─ 远程:Publish Over SSH
│
▼
npm run start
│
▼
PM2
│
▼
Nginx
本文涉及的核心职责边界如下:
| 层次 | 主要职责 | 使用组件 |
|---|---|---|
| 源码 | 版本管理、触发构建 | Gitee / Git |
| CI | 拉取代码、安装依赖、执行构建 | Jenkins |
| 前端发布 | 将 dist 同步到静态目录 |
rsync / Publish Over SSH |
| 后端发布 | 安装生产依赖、启动或重启服务 | PM2 |
| 入口层 | 静态资源与 API 反向代理 | Nginx |
| 权限 | 控制用户可访问的 Job 范围 | Role-Based Strategy |
1. 安装 Jenkins
Jenkins 运行依赖 Java。本文使用 Java 21:
bash
sudo dnf install -y fontconfig java-21-openjdk
java -version
添加 Jenkins LTS 仓库:
bash
sudo wget -O /etc/yum.repos.d/jenkins.repo \
https://pkg.jenkins.io/rpm-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/rpm-stable/jenkins.io-2026.key
安装 Jenkins:
bash
sudo dnf install -y jenkins
sudo systemctl daemon-reload
sudo systemctl enable --now jenkins
确认是否已经开机启动:
bash
systemctl is-enabled jenkins
systemctl status jenkins
看到:
text
enabled
Active: active (running)
返回 enabled 且服务状态为 active (running),即表示 Jenkins 已正常启动并设置为开机自启。
内网或测试环境可开放 Jenkins 默认端口 8080:
bash
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
浏览器打开:
text
http://192.168.31.88:8080
首次访问 Jenkins 时需要读取初始化密码:
bash
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
初始化阶段可选择 Install suggested plugins 安装 Jenkins 推荐插件。
初始化向导中创建管理员账号:
text
linhao
该账号作为 Jenkins 全局管理员。后续配置项目级权限时,仅新增 api 和 web 两个普通账号。
初始化完成后进入 Jenkins 首页:
初始化完成后,Jenkins 首页应能够看到以下核心入口:
text
新建 Item
构建历史
Manage Jenkins
构建队列
构建执行状态
后续所有 Job、插件、工具链和权限配置都从这些入口完成。
2. 安装 Jenkins 所需插件
进入:
text
Manage Jenkins
→ Plugins
在 Manage Jenkins 中,本篇主要会用到以下配置入口:
text
Plugins → 安装 NodeJS Plugin、Role-based Authorization Strategy、Publish Over SSH
Tools → 配置 Node.js 24
System → 配置 Publish Over SSH 远程服务器
Security → 启用 Role-Based Strategy
Users → 创建 Jenkins 普通用户
本文涉及以下三个插件:
text
NodeJS Plugin
Role-based Authorization Strategy
Publish Over SSH
Publish Over SSH 仅用于远程发布;同机部署阶段无需配置。
NodeJS Plugin
搜索 NodeJS,安装 NodeJS Plugin:
插件安装完成后,在 Installed plugins 中应能看到:
text
NodeJS Plugin
Status:Enabled
如果插件已安装但未启用,需要先启用后再进入 Tools 配置 Node.js。
需要注意:服务器上即使已经通过 NVM 安装 Node,Jenkins Job 也未必能够直接使用。
Jenkins Job 默认以 Linux 用户 jenkins 执行,而 NVM 通常安装在 root 或其他用户目录。为避免构建过程依赖用户级 NVM 环境,本文通过 NodeJS Plugin 为 Jenkins 独立配置 Node 运行环境。
3. Jenkins 配置 Node 24
进入:
text
Manage Jenkins
→ Tools
→ NodeJS installations
新增一套:
text
Name:node24
Install automatically:勾选
NodeJS Version:24.x
Global npm packages to install:pnpm pm2
前端构建使用 pnpm,后端进程管理使用 PM2,因此可在该工具配置中统一安装。
如果希望 CI 环境更稳定,也可以固定主版本:
text
pnpm@10 pm2@6
NodeJS 工具建议配置为:
text
Name:node24
Install automatically:√
NodeJS Version:24.x
Global npm packages to install:pnpm pm2
如果希望 CI 环境更稳定,可以固定主版本:
text
pnpm@10 pm2@6
保存以后,每个 Job 在"构建环境"里勾上:
text
Provide Node & npm bin/ folder to PATH
然后选择:
text
node24
完成配置后,Execute shell 中可直接使用:
bash
node -v
npm -v
pnpm -v
pm2 -v
4. 配置 Git 私有仓库凭据
新建 Job 后,在:
text
源码管理
→ Git
仓库填:
text
https://gitee.com/linhao-dev/side-blog.git
如果是私有仓库,Credentials 不能留空。
HTTPS 仓库我一般添加:
text
Kind:Username with password
Username:linhao-dev
Password:Gitee 私人令牌
Password 建议填写 Personal Access Token,不建议直接保存 Git 平台登录密码。
分支根据项目实际情况填:
text
*/main
如果仓库还是 master:
text
*/master
配置时如果出现:
text
Incorrect username or password (access token)
该错误通常由 Credentials 配置错误导致,应优先检查用户名、Token 与仓库访问权限。
5. Jenkins Job 命名规范
为便于后续通过 Role Pattern 进行权限隔离,Job 统一采用前缀命名,不再沿用临时测试名称 vite-web:
text
web_xxx 前端
api_xxx 后端
示例创建两个 Freestyle Project:
text
web_test
api_test
对应的权限正则可统一定义为:
text
^web_.*$
^api_.*$
这样新增同类 Job 时,无需为每个项目单独维护一套角色。
6. 前端 web_test:Vite 构建与同机发布
创建一个 Freestyle Project:
text
新建 Item
→ web_test
→ Freestyle project
Git、Credentials 与 Node 24 按前述方式配置。
同机部署场景如下:
text
Jenkins 和 Nginx 在同一台服务器
最终就是:
text
Git
↓
Jenkins
↓
cd web
↓
pnpm install
↓
npm run build
↓
web/dist
↓
rsync
↓
/home/html/web_test
↓
Nginx
text
Jenkins 拉取代码
│
▼
进入 web 目录
│
▼
pnpm install
│
▼
npm run build
│
▼
生成 web/dist
│
▼
rsync --delete
│
▼
/home/html/web_test
│
▼
Nginx
6.1 配置发布目录权限
服务器执行一次:
bash
sudo mkdir -p /home/html/web_test
sudo chown -R jenkins:jenkins /home/html/web_test
sudo chmod 755 /home
sudo chmod 755 /home/html
sudo chmod -R u+rwX,go+rX /home/html/web_test
建议使用 jenkins 用户执行一次写入测试:
bash
sudo -u jenkins touch /home/html/web_test/test.txt
sudo rm -f /home/html/web_test/test.txt
命令成功执行即表示 jenkins 用户具备目标目录写权限。
6.2 web_test 构建脚本
在 Build Steps → Execute shell 中配置以下脚本:
bash
#!/usr/bin/env bash
set -Eeuo pipefail
readonly DEPLOY_DIR="/home/html/web_test"
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=2048}"
log() {
printf '
[%s] %s
' "$(date '+%F %T')" "$*"
}
log "检查构建环境"
node -v
npm -v
pnpm -v
log "进入前端目录"
cd "${WORKSPACE}/web"
log "安装依赖"
pnpm install \
--frozen-lockfile \
--dangerously-allow-all-builds
log "执行 Vite 构建"
npm run build
if [[ ! -d "dist" ]]; then
echo "ERROR: dist 目录不存在,终止发布。" >&2
exit 1
fi
log "同步静态资源到 Nginx 目录"
rsync -rv --delete \
dist/ \
"${DEPLOY_DIR}/"
log "web_test 发布完成"
脚本中几个关键参数的作用:
| 配置 | 作用 |
|---|---|
set -Eeuo pipefail |
任一关键命令失败时立即终止,避免失败后继续发布 |
NODE_OPTIONS |
提高 Node 构建阶段可用堆内存,避免 vue-tsc / Vite OOM |
--frozen-lockfile |
CI 中严格按照 lock 文件安装依赖 |
--delete |
删除目标目录中已经不属于新版本的旧静态资源 |
${WORKSPACE} |
Jenkins 当前 Job 的工作空间目录 |
发布阶段采用:
bash
rsync -rv --delete
相比简单的 cp,rsync 更适合前端静态资源发布,因为构建文件通常包含 hash:
text
index-a83d.js
index-27fd.js
新版本已经没有的旧文件,--delete 会一起清掉。
如果使用:
bash
rsync -av --delete
在部分权限配置下可能出现:
text
chgrp failed: Operation not permitted
静态资源发布通常无需保留源文件的 owner/group,因此使用 -rv 可以避免不必要的权限继承。
6.3 配置 Nginx 静态目录
Nginx root 指向发布目录:
nginx
server {
listen 80;
server_name web-test.example.com;
root /home/html/web_test;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
检查:
bash
sudo nginx -t
sudo systemctl reload nginx
后续 Jenkins 仅替换静态文件,无需在每次构建后 reload Nginx。
如果使用 Vue Router history 模式,需要保留以下配置:
nginx
try_files $uri $uri/ /index.html;
7. 前端构建与发布常见问题
以下问题均发生在构建或发布阶段,容易被误认为 Jenkins 本身异常。
7.1 pnpm 10:Ignored build scripts
安装依赖时如果看到:
text
ERR_PNPM_IGNORED_BUILDS
Ignored build scripts: esbuild ...
典型日志类似:
text
[ERR_PNPM_IGNORED_BUILDS]
Ignored build scripts:
@parcel/watcher
esbuild
vue-demi
Run "pnpm approve-builds" to pick which dependencies
should be allowed to run scripts.
看到这个错误时,说明依赖已经解析完成,但部分依赖的安装脚本被 pnpm 安全策略阻止。
这是新版 pnpm 对依赖生命周期脚本做了限制。
长期维护建议在仓库中显式配置允许执行 build script 的依赖;对于完全可信的内部项目,也可以使用:
bash
pnpm install --dangerously-allow-all-builds
因此示例构建脚本采用该参数。
7.2 vue-tsc 构建 OOM
类型检查阶段如果出现:
text
FATAL ERROR: Allocation failed
JavaScript heap out of memory
典型日志:
text
FATAL ERROR:
Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
ELIFECYCLE Command failed with exit code 134
如果错误出现在 vue-tsc、TypeScript 类型检查或 Vite 构建阶段,优先检查 Node 堆内存限制。
通常发生在 vue-tsc 类型检查阶段。
Execute shell 开头加:
bash
export NODE_OPTIONS="--max-old-space-size=2048"
项目更大可以给 4096:
bash
export NODE_OPTIONS="--max-old-space-size=4096"
同时需要保证服务器具有足够的可用内存。
7.3 构建成功但 rsync Permission denied
如果前面已经 build 完成,最后报:
text
Permission denied
mkdir failed
mkstemp failed
典型日志:
text
rsync: mkdir ".../static" failed: Permission denied (13)
rsync: mkstemp ".../index.html.xxx" failed: Permission denied (13)
rsync error: some files/attrs were not transferred (code 23)
此时说明构建已经完成,失败点在"发布目录写入权限",排查重点应转向 Linux 用户与目录权限。
该问题与 Vite 构建无关,通常是 jenkins 用户缺少目标目录写权限。
重新确认:
bash
sudo chown -R jenkins:jenkins /home/html/web_test
sudo chmod 755 /home/html
sudo chmod -R u+rwX,go+rX /home/html/web_test
sudo -u jenkins touch /home/html/web_test/test.txt
touch 测试通过后,再重新执行 Jenkins 构建。
8. 后端 api_test:Koa 构建与 PM2 进程管理
后端示例创建 Job:
text
api_test
下面使用一个最小 Koa + TypeScript 项目说明完整构建与运行流程。
目录:
text
api/
├── src/
│ └── index.ts
├── package.json
├── package-lock.json
└── tsconfig.json
8.1 package.json
json
{
"name": "api-test",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js"
},
"dependencies": {
"koa": "^2.16.0"
},
"devDependencies": {
"@types/koa": "^2.15.0",
"@types/node": "^24.0.0",
"typescript": "^5.9.0"
}
}
项目统一通过以下脚本启动:
bash
npm run start
PM2 同样托管 npm run start,避免在 Jenkins 中写死具体入口文件。
8.2 tsconfig.json
json
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
8.3 src/index.ts
ts
import Koa from 'koa';
const app = new Koa();
const port = Number(process.env.PORT || 3000);
app.use(async (ctx) => {
if (ctx.path === '/health') {
ctx.body = {
code: 0,
message: 'ok',
service: 'api_test'
};
return;
}
ctx.body = {
code: 0,
message: 'Hello from Koa'
};
});
app.listen(port, '0.0.0.0', () => {
console.log(`api_test started on ${port}`);
});
提交 Jenkins 构建前可先在本地验证:
bash
cd api
npm install
npm run build
npm run start
构建完成后应生成:
text
api/dist/index.js
访问:
text
http://127.0.0.1:3000/health
返回:
json
{
"code": 0,
"message": "ok",
"service": "api_test"
}
9. 配置后端 Jenkins Job api_test
新建:
text
api_test
→ Freestyle project
Git、Credentials 与 Node 24 配置与 web_test 保持一致。
api_test 的构建、发布与运行关系如下:
text
Jenkins 拉取代码
│
▼
进入 api 目录
│
▼
npm ci
│
▼
npm run build
│
▼
生成 api/dist
│
▼
同步到 /home/apps/api_test
│
▼
npm ci --omit=dev
│
▼
npm run start
│
▼
PM2 托管进程
│
▼
Nginx 反向代理
不建议直接将 Jenkins Workspace 作为长期运行目录,因为 Workspace 可能在构建或清理过程中被覆盖。
创建独立运行目录:
bash
sudo mkdir -p /home/apps/api_test
sudo chown -R jenkins:jenkins /home/apps/api_test
sudo chmod -R u+rwX,go+rX /home/apps/api_test
9.1 api_test Execute shell
在 Build Steps → Execute shell 中配置:
bash
#!/usr/bin/env bash
set -Eeuo pipefail
readonly APP_NAME="api_test"
readonly DEPLOY_DIR="/home/apps/api_test"
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=2048}"
log() {
printf '
[%s] %s
' "$(date '+%F %T')" "$*"
}
log "检查构建环境"
node -v
npm -v
pm2 -v
log "进入后端目录"
cd "${WORKSPACE}/api"
log "安装完整构建依赖"
npm ci
log "编译 TypeScript"
npm run build
if [[ ! -d "dist" ]]; then
echo "ERROR: dist 目录不存在,终止发布。" >&2
exit 1
fi
log "同步构建产物"
mkdir -p "${DEPLOY_DIR}/dist"
rsync -rv --delete \
dist/ \
"${DEPLOY_DIR}/dist/"
cp package.json "${DEPLOY_DIR}/"
cp package-lock.json "${DEPLOY_DIR}/"
log "安装生产依赖"
cd "${DEPLOY_DIR}"
npm ci --omit=dev
log "启动或重启 PM2 进程"
if pm2 describe "${APP_NAME}" >/dev/null 2>&1; then
pm2 restart "${APP_NAME}" --update-env
else
pm2 start npm \
--name "${APP_NAME}" \
-- run start
fi
pm2 save
pm2 list
log "api_test 发布完成"
这里将"构建目录"和"运行目录"分离:
text
Jenkins Workspace
↓ npm run build
api/dist
↓ rsync
/home/apps/api_test/dist
↓ npm run start
PM2
这样即使 Jenkins 后续清理 Workspace,也不会直接影响当前正在运行的 Node 服务。
该脚本执行流程如下:
text
npm ci
↓
npm run build
↓
dist
↓
同步到 /home/apps/api_test/dist
↓
npm ci --omit=dev
↓
pm2 start npm --name api_test -- run start
首次启动:
bash
pm2 start npm --name api_test -- run start
后续部署检测到同名进程后执行:
bash
pm2 restart api_test --update-env
这样项目真正的启动入口一直是:
bash
npm run start
后续即使调整 start 对应的 Node 入口,Jenkins 中的 PM2 命令也无需修改。
9.2 PM2 开机恢复
同机部署场景下,如果 PM2 由 jenkins 用户管理,则 pm2 startup 也应基于同一 Linux 用户完成配置。
生产环境更建议采用后文的远程部署模式:Jenkins 负责构建与制品传输,业务服务器由独立 deploy 用户负责 PM2 运行管理。
10. Nginx 反向代理 api_test
假设 Koa 在本机:
text
127.0.0.1:3000
Nginx 可以这样写:
nginx
server {
listen 80;
server_name api-test.example.com;
location /api/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Rocky Linux 如果 SELinux 阻止 Nginx 连 Node,可以:
bash
sudo setsebool -P httpd_can_network_connect 1
然后:
bash
sudo nginx -t
sudo systemctl reload nginx
测试:
text
http://api-test.example.com/api/health
不建议将 Node 服务端口 3000 直接暴露到公网。
11. Jenkins 项目级权限隔离
完成构建与部署后,可以进一步配置 Jenkins 项目级权限。
权限目标如下:
text
linhao 管理全部 Jenkins
api 只能看到 api_ 开头的 Job
web 只能看到 web_ 开头的 Job
发布阶段采用:
text
Role-based Authorization Strategy
插件安装以后,到:
text
Manage Jenkins
→ Security
→ Authorization
选择:
text
Role-Based Strategy
权限模型如下:
text
linhao
└── Global Role:admin
└── Overall → Administer
└── 可管理全部 Jenkins 资源
api 用户
├── Global Role:loginSys
│ └── Overall → Read
└── Item Role:api
└── Pattern:^api_.*$
└── 只能访问 api_ 开头的 Job
web 用户
├── Global Role:loginSys
│ └── Overall → Read
└── Item Role:web
└── Pattern:^web_.*$
└── 只能访问 web_ 开头的 Job
Role-Based Strategy 中需要区分两类权限:
text
Global roles:整个 Jenkins 范围的权限
Item roles:只对 Pattern 匹配到的 Job 生效
进行项目隔离时,普通用户不应在 Global Role 中获得 Job Read、Job Build 等全局 Job 权限。
11.1 Global roles
Global roles 定义两个角色:
text
admin
→ Overall / Administer
loginSys
→ Overall / Read
loginSys 仅授予 Overall → Read,用于提供 Jenkins 基础访问权限;具体 Job 权限由 Item roles 控制。
初始化管理员 linhao 分配:
text
admin
11.2 Item roles
Item roles 定义两个角色:
text
Role:api
Pattern:^api_.*$
和:
text
Role:web
Pattern:^web_.*$
普通开发账号建议授予:
text
Job → Read
Job → Build
Job → Cancel
Job → Workspace
View → Read
不授予 Configure、Delete 与 Credentials 相关权限,以遵循最小权限原则。
Item Role 建议按前缀设计:
text
Role:api
Pattern:^api_.*$
Role:web
Pattern:^web_.*$
权限建议仅授予:
text
Job → Read
Job → Build
Job → Cancel
Job → Workspace
View → Read
不授予 Configure、Delete 和 Credentials 相关权限。
Pattern ^api_.*$ 的匹配结果如下:
text
^api_.*$
会匹配:
text
api_test
api_prod
api_user
不会匹配:
text
web_test
test_api
api
web 角色使用 ^web_.*$,规则一致。
11.3 创建普通 Jenkins 用户
进入:
text
Manage Jenkins
→ Users
新增:
text
api
web
管理员 linhao 已在初始化阶段创建,此处仅新增普通账号。
11.4 Assign Roles
Global roles:
text
linhao → admin
api → loginSys
web → loginSys
Item roles:
text
api → api
web → web
角色分配关系如下:
| 用户 | Global Role | Item Role | 可见项目 |
|---|---|---|---|
linhao |
admin |
--- | 全部 Job |
api |
loginSys |
api |
api_ 开头的 Job |
web |
loginSys |
web |
web_ 开头的 Job |
最后效果:
text
linhao 登录
→ 能看到 web_test、api_test 和其他全部 Job
api 登录
→ 只看到 api_test / api_xxx
web 登录
→ 只看到 web_test / web_xxx
如果普通账号登录后提示"没有权限",首先检查是否已经分配:
text
Global → loginSys
该角色对应 Overall → Read。
Item Role 不能替代 Jenkins 的基础访问权限。
12. 同机部署流程汇总
目前前端:
text
Gitee
↓
Jenkins web_test
↓
pnpm install
↓
npm run build
↓
web/dist
↓
rsync
↓
/home/html/web_test
↓
Nginx
后端:
text
Gitee
↓
Jenkins api_test
↓
npm ci
↓
npm run build
↓
api/dist
↓
/home/apps/api_test
↓
npm run start
↓
PM2
↓
Nginx
权限也已经能做到:
text
api → api_.*
web → web_.*
该方案适用于 Jenkins 与 Nginx/Node 服务部署在同一台开发或测试服务器的场景。
在生产环境中,Jenkins 通常与业务服务器分离。此时不再使用本地 rsync /home/... 目标路径,而改用 Publish Over SSH。
13. 远程发布方案:Publish Over SSH
假设现在变成:
text
Jenkins:192.168.31.88
业务服务器:192.168.31.90
推荐将 Jenkins 的职责限定为:
text
1. 构建
2. 把构建产物传过去
业务服务器上的文件切换、PM2 重启等部署动作由远程脚本负责。
text
Git
│
▼
Jenkins
│
├─ 安装依赖
├─ 执行 build
▼
生成构建产物
│
▼
Publish Over SSH
│
▼
远程服务器 /tmp/jenkins/...
│
▼
执行 deploy 脚本
│
├─ 前端:替换 Nginx 静态目录
│
└─ 后端:替换应用目录 + npm ci --omit=dev + PM2 restart
流程变成:
text
Git
↓
Jenkins build
↓
Publish Over SSH
↓
192.168.31.90:/tmp/jenkins/xxx
↓
远程 deploy 脚本
↓
正式目录
备份、健康检查、回滚等逻辑可以集中维护在业务服务器部署脚本中,避免 Jenkins Execute shell 过度膨胀。
14. 目标服务器 deploy 用户与目录
业务服务器 192.168.31.90 创建一个专门发布账号:
bash
sudo useradd -m deploy
准备目录:
bash
sudo mkdir -p /tmp/jenkins
sudo mkdir -p /home/html/web_test
sudo mkdir -p /home/apps/api_test
sudo mkdir -p /home/deploy/scripts
sudo chown -R deploy:deploy /tmp/jenkins
sudo chown -R deploy:deploy /home/html/web_test
sudo chown -R deploy:deploy /home/apps/api_test
sudo chown -R deploy:deploy /home/deploy/scripts
远程服务器如果要运行后端,还要保证 deploy 这个用户能用:
bash
node -v
npm -v
pm2 -v
如未安装 PM2:
bash
npm install -g pm2
SSH 认证建议使用 Key,并避免使用 root 账号直接登录业务服务器。
15. 配置 Publish Over SSH
插件装好后进入:
text
Manage Jenkins
→ System
→ Publish over SSH
添加服务器:
text
Name:prod-server
Hostname:192.168.31.90
Username:deploy
Remote Directory:/tmp/jenkins
认证配置 SSH Private Key。
点:
text
Test Configuration
测试成功后保存配置。
Publish Over SSH 支持在文件传输完成后执行远程命令,因此可以将实际部署逻辑交由目标服务器脚本处理。
16. 远程发布前端 web_test
远程模式下,Jenkins Execute shell 只负责生成构建产物,不直接操作业务服务器目录:
bash
#!/usr/bin/env bash
set -Eeuo pipefail
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=2048}"
cd "${WORKSPACE}/web"
pnpm install \
--frozen-lockfile \
--dangerously-allow-all-builds
npm run build
[[ -d "dist" ]] || {
echo "ERROR: dist 目录不存在。" >&2
exit 1
}
echo "web_test build success"
然后在:
text
Post-build Actions
→ Send build artifacts over SSH
选择:
text
prod-server
Transfer Set:
text
Source files:web/dist/**
Remove prefix:web/dist
Remote directory:web_test
由于全局 Remote Directory 是:
text
/tmp/jenkins
最终文件会到:
text
/tmp/jenkins/web_test
目标服务器写:
bash
vim /home/deploy/scripts/deploy-web_test.sh
bash
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SOURCE_DIR="/tmp/jenkins/web_test"
readonly TARGET_DIR="/home/html/web_test"
printf '
[%s] deploy web_test
' "$(date '+%F %T')"
mkdir -p "${TARGET_DIR}"
find "${TARGET_DIR}" \
-mindepth 1 \
-maxdepth 1 \
-exec rm -rf {} +
cp -a "${SOURCE_DIR}/." "${TARGET_DIR}/"
echo "web_test deploy success"
授权:
bash
chmod +x /home/deploy/scripts/deploy-web_test.sh
Publish Over SSH 的 Exec command 填:
bash
bash /home/deploy/scripts/deploy-web_test.sh
前端远程发布流程如下:
text
Jenkins build
→ SSH 上传 dist
→ 执行 deploy-web_test.sh
→ 更新 Nginx 目录
17. 远程发布后端 api_test
后端远程发布还需要在目标服务器安装生产依赖并重启 PM2。
Jenkins 先完成依赖安装、编译并生成发布包:
bash
#!/usr/bin/env bash
set -Eeuo pipefail
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=2048}"
cd "${WORKSPACE}/api"
npm ci
npm run build
[[ -d "dist" ]] || {
echo "ERROR: dist 目录不存在。" >&2
exit 1
}
rm -f api_test.tar.gz
tar -czf api_test.tar.gz \
dist \
package.json \
package-lock.json
echo "api_test build success"
Publish Over SSH:
text
Source files:api/api_test.tar.gz
Remove prefix:api
Remote directory:api_test
最终:
text
/tmp/jenkins/api_test/api_test.tar.gz
远程服务器脚本:
bash
vim /home/deploy/scripts/deploy-api_test.sh
bash
#!/usr/bin/env bash
set -Eeuo pipefail
readonly APP_NAME="api_test"
readonly PACKAGE="/tmp/jenkins/api_test/api_test.tar.gz"
readonly APP_DIR="/home/apps/api_test"
readonly RELEASE_DIR="/tmp/api_test_release"
printf '
[%s] deploy api_test
' "$(date '+%F %T')"
rm -rf "${RELEASE_DIR}"
mkdir -p "${RELEASE_DIR}" "${APP_DIR}"
tar -xzf "${PACKAGE}" -C "${RELEASE_DIR}"
rm -rf "${APP_DIR}/dist"
cp -a "${RELEASE_DIR}/dist" "${APP_DIR}/dist"
cp "${RELEASE_DIR}/package.json" "${APP_DIR}/"
cp "${RELEASE_DIR}/package-lock.json" "${APP_DIR}/"
cd "${APP_DIR}"
npm ci --omit=dev
if pm2 describe "${APP_NAME}" >/dev/null 2>&1; then
pm2 restart "${APP_NAME}" --update-env
else
pm2 start npm \
--name "${APP_NAME}" \
-- run start
fi
pm2 save
pm2 list
echo "api_test deploy success"
授权:
bash
chmod +x /home/deploy/scripts/deploy-api_test.sh
Publish Over SSH 的 Exec command:
bash
bash /home/deploy/scripts/deploy-api_test.sh
该方式下 Jenkins 不直接管理远程服务器上的 PM2:
text
Jenkins:build + 上传
业务服务器:安装生产依赖 + npm run start + PM2 restart
这种职责划分能够将 CI 构建与业务运行管理解耦。
18. rsync 与 Publish Over SSH 选型
两种方案的边界可以概括为:
text
Jenkins 构建完成
│
┌─────────────┴─────────────┐
│ │
▼ ▼
同机部署 远程部署
│ │
▼ ▼
rsync --delete Publish Over SSH
│ │
▼ ▼
本机 Nginx / Node 目录 远程临时目录 + deploy 脚本
│ │
▼ ▼
Nginx / PM2 Nginx / PM2
两种部署方式可按以下场景选择:
| 场景 | 推荐方案 |
|---|---|
| Jenkins 和 Nginx 同机 | rsync |
| Jenkins 和 Node 后端同机 | rsync + PM2 |
| Jenkins 和业务服务器分开 | Publish Over SSH |
| 测试/预发/生产多台服务器 | Publish Over SSH |
| 希望部署逻辑留在业务服务器 | Publish Over SSH + deploy 脚本 |
rsync 适合同机发布,配置简单,并可通过 --delete 清理旧资源。
Publish Over SSH 更适合 Jenkins 与业务服务器分离、多环境部署以及需要远程执行部署脚本的场景。
19. 完整流程汇总
前端同机:
text
Gitee
↓
Jenkins web_test
↓
Node 24 / pnpm
↓
npm run build
↓
web/dist
↓
rsync --delete
↓
/home/html/web_test
↓
Nginx
后端同机:
text
Gitee
↓
Jenkins api_test
↓
npm ci
↓
npm run build
↓
dist
↓
/home/apps/api_test
↓
npm ci --omit=dev
↓
npm run start
↓
PM2
↓
Nginx
Jenkins 权限:
text
linhao
└── Global:admin
api
├── Global:loginSys
└── Item:api
└── ^api_.*$
web
├── Global:loginSys
└── Item:web
└── ^web_.*$
远程发布:
text
Git
↓
Jenkins build
↓
Publish Over SSH
↓
192.168.31.90:/tmp/jenkins
↓
deploy-xxx.sh
↓
前端:Nginx 静态目录
后端:应用目录 + PM2
至此,基础 Jenkins CI/CD 已覆盖代码拉取、构建、部署、运行与权限隔离。
Webhook、Pipeline/Jenkinsfile、Docker、蓝绿发布等能力可在此基础上继续扩展。首次搭建时,建议优先保证以下主链路稳定:
text
拉代码 → 构建 → 发布 → 运行 → 权限隔离
在基础链路稳定后,可以接入 Gitee Webhook 实现自动触发,或进一步将 Freestyle Project 迁移为 Jenkinsfile/Pipeline。
20. 验收检查
建议按以下顺序进行验收:
text
Jenkins
[ ] systemctl status jenkins 正常
[ ] NodeJS Plugin 的 node24 能用
[ ] Git Credentials 可以拉私有仓库
web_test
[ ] pnpm install 正常
[ ] npm run build 正常
[ ] dist 存在
[ ] rsync 发布成功
[ ] 页面可以通过 Nginx 打开
api_test
[ ] npm ci 正常
[ ] npm run build 生成 dist
[ ] npm run start 能运行 dist
[ ] pm2 list 显示 api_test online
[ ] /health 可以访问
权限
[ ] linhao 能看到全部 Job
[ ] api 只能看到 api_ 开头 Job
[ ] web 只能看到 web_ 开头 Job
[ ] api/web 都有 loginSys
远程发布
[ ] Publish Over SSH Test Configuration 成功
[ ] deploy 使用 SSH Key
[ ] 上传以后远程 deploy 脚本可以执行