Node.js + Jenkins 自动化部署示例
·
一、项目结构与基础代码
创建一个极简的 Node.js + Express Web 项目,包含基础页面和启动脚本,后续对接 Jenkins 实现自动化部署。
1. 项目目录结构
nodejs-jenkins-demo/
├── public/ # 静态资源目录
│ ├── css/ # 样式文件目录
│ │ └── style.css # 页面样式
│ └── index.html # 前端页面
├── Jenkinsfile # Jenkins流水线配置
├── README.md # 项目说明文档
├── app.js # Express服务入口
├── app.test.js # 测试文件(如单元测试)
├── package.json # 依赖与脚本配置
└── package-lock.json # 依赖版本锁定文件
2. 核心代码编写
(1) package.json(依赖配置)
{
"name": "nodejs-jenkins-demo",
"version": "1.0.0",
"description": "Node.js + Jenkins 自动化部署示例",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"stop": "pkill -f node app.js || echo '服务未运行'",
"test": "jest --runInBand --no-cache"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"jest": "^27.5.1",
"nodemon": "^3.0.1"
}
}
(2) app.js(Express 服务)
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3300;
// 静态资源目录
app.use(express.static(path.join(__dirname, 'public')));
// 根路由返回首页
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// 关键修复:仅当直接运行 app.js 时才启动服务(测试导入时跳过)
if (require.main === module) {
// 启动服务
app.listen(PORT, () => {
console.log(`服务运行在 http://localhost:${PORT}`);
});
}
// 导出 app 实例(供测试使用)
module.exports = app;
(3) public/index.html(前端页面)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Node.js + Jenkins 自动化部署示例</title>
<style>
body {
text-align: center;
margin-top: 100px;
font-family: Arial, sans-serif;
}
.container {
color: #333;
}
.version {
color: #666;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎉 Node.js 自动化部署成功!</h1>
<p>该页面由 Jenkins 自动部署</p>
<div class="version">部署时间: <span id="deployTime"></span></div>
</div>
<script>
// 显示当前时间(模拟部署版本)
document.getElementById('deployTime').textContent = new Date().toLocaleString();
</script>
</body>
</html>
(4) Jenkinsfile(Jenkins 流水线脚本)
pipeline {
agent any
environment {
DEPLOY_DIR = '/u01/nodejs-jenkins-demo'
NODEJS_NAME = 'NodeJs 16.20.2'
PORT = 3300
APP_ENTRY = 'app.js'
SERVICE_NAME = 'nodejs-jenkins-demo' // 系统服务名
TEST_PORT = 3301 // 测试专用端口(避免和生产端口冲突)
}
stages {
stage('拉取代码') {
steps {
echo '📥 开始拉取代码...'
git url: 'github的仓库地址',
branch: 'main',
credentialsId: 'github的Personal access tokens (classic)'
}
}
stage('安装依赖') {
steps {
echo '📦 安装项目依赖(含测试依赖)...'
nodejs(nodeJSInstallationName: env.NODEJS_NAME) {
// 注意:移除 --production,安装所有依赖(含devDependencies中的Jest)
sh 'npm install'
}
}
}
// ========== 新增:自动化测试阶段 ==========
stage('自动化测试') {
steps {
echo '🧪 执行 Node.js 服务自动化测试...'
nodejs(nodeJSInstallationName: env.NODEJS_NAME) {
sh '''
echo "📌 开始执行 Jest 测试(Node.js 版本:$(node -v))"
echo "📌 测试专用端口:${TEST_PORT}"
# 1. 执行 Jest 测试(适配 Node 16,单进程运行)
npm test -- --runInBand --no-cache
# 2. 测试失败则直接终止部署(Jest 非0退出码会触发Pipeline失败)
if [ $? -ne 0 ]; then
echo "❌ 自动化测试失败,终止部署流程!"
exit 1
fi
echo "✅ 所有自动化测试用例执行通过!"
'''
}
}
// 测试失败时直接终止Pipeline,不进入后续部署阶段
post {
failure {
echo '❌ 自动化测试失败,部署流程终止!'
sh 'echo "测试失败时间:$(date)" >> test-fail.log'
}
}
}
// ========== 新增结束 ==========
stage('清理旧服务') {
steps {
echo '🛑 停止旧的 Node.js 服务并释放端口...'
sh '''
# 1. 停止 systemd 管理的旧服务
echo "停止 ${SERVICE_NAME} 系统服务..."
systemctl stop ''' + env.SERVICE_NAME + ''' 2>/dev/null || echo "系统服务未运行"
# 2. 杀死占用端口的残留进程(兜底)
PID=$(lsof -ti:''' + env.PORT + ''' || echo "none")
if [ "$PID" != "none" ] && [ -n "$PID" ]; then
echo "杀死占用 ''' + env.PORT + ''' 端口的进程:$PID"
kill -9 $PID
sleep 2
else
echo "''' + env.PORT + ''' 端口未被占用"
fi
# 3. 杀死旧服务进程(兜底)
OLD_PID=$(ps -ef | grep -v grep | grep "node ''' + env.APP_ENTRY + '''" | awk \'{print $2}\' | tr -d \' \' || echo "none")
if [ "$OLD_PID" != "none" ] && [ -n "$OLD_PID" ] && [[ "$OLD_PID" =~ ^[0-9]+$ ]]; then
echo "杀死旧服务进程:$OLD_PID"
kill -9 $OLD_PID
sleep 2
else
echo "无旧的 node ''' + env.APP_ENTRY + ''' 服务进程需要停止"
fi
'''
}
}
stage('部署代码') {
steps {
echo '🚀 部署新代码到服务器...'
sh '''
mkdir -p ''' + env.DEPLOY_DIR + '''
# 备份旧日志
cp ''' + env.DEPLOY_DIR + '''/app.log ''' + env.DEPLOY_DIR + '''/app.log.bak 2>/dev/null || true
# 清空新日志
> ''' + env.DEPLOY_DIR + '''/app.log
# 复制代码(覆盖原有文件)
cp -r ./* ''' + env.DEPLOY_DIR + '''/
# 设置权限(适配 systemd 中 root 用户运行)
chmod -R 755 ''' + env.DEPLOY_DIR + '''
chown -R root:root ''' + env.DEPLOY_DIR + '''
'''
}
}
stage('启动新服务') {
steps {
echo '🔄 通过 systemctl 启动新的 Node.js 服务...'
nodejs(nodeJSInstallationName: env.NODEJS_NAME) {
sh '''
echo "📋 当前执行用户信息:"
whoami
echo "当前用户 ID 和所属组:"
id
echo "当前工作目录:"
pwd
echo "=============================================="
# 1. 进入部署目录
cd ''' + env.DEPLOY_DIR + '''
# 2. 重载 systemd 配置(防止服务文件修改)
echo "重载 systemd 配置..."
systemctl daemon-reload
# 3. 启动系统服务
echo "启动 ${SERVICE_NAME} 服务..."
systemctl start ''' + env.SERVICE_NAME + '''
# 4. 检查服务启动状态
if ! systemctl is-active --quiet ''' + env.SERVICE_NAME + '''; then
echo "❌ 服务启动失败!查看 systemd 日志:"
journalctl -u ''' + env.SERVICE_NAME + ''' -n 50
exit 1
fi
echo "✅ 服务启动成功,状态:active(running)"
# 5. 检测端口是否监听
sleep 3 # 等待服务完全启动
PORT_LISTEN=$(ss -tulpn | grep ":''' + env.PORT + '''" | grep "node" || echo "none")
if [ "$PORT_LISTEN" = "none" ]; then
echo "❌ 服务未监听 ''' + env.PORT + ''' 端口!查看应用日志:"
cat ''' + env.DEPLOY_DIR + '''/app.log
exit 1
fi
echo "✅ ''' + env.PORT + ''' 端口已被 node 进程监听:"
ss -tulpn | grep ":''' + env.PORT + '''"
# 6. 检测服务是否可访问
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:''' + env.PORT + ''')
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "301" ] || [ "$HTTP_CODE" = "302" ]; then
echo "✅ 服务可正常访问,HTTP 状态码:$HTTP_CODE"
else
echo "⚠️ 服务进程启动但访问失败,状态码:$HTTP_CODE"
echo "应用日志:"
cat ''' + env.DEPLOY_DIR + '''/app.log
# exit 1 // 可选:非关键服务可注释
fi
'''
}
}
}
}
post {
success {
echo "✅ 部署成功!访问地址: http://服务器IP:${env.PORT}"
sh '''
echo "当前服务状态:"
systemctl status ''' + env.SERVICE_NAME + ''' --no-pager
echo "当前进程列表:"
ps -ef | grep -v grep | grep "node ''' + env.APP_ENTRY + '''"
'''
}
failure {
echo '❌ 部署失败,请检查日志!'
sh '''
echo "systemd 服务日志:"
journalctl -u ''' + env.SERVICE_NAME + ''' -n 50 --no-pager
echo "应用日志:"
cat ''' + env.DEPLOY_DIR + '''/app.log || echo "日志文件不存在"
'''
}
}
}
(5) app.test.js(测试文件)
const http = require('http');
const fs = require('fs');
const path = require('path');
const app = require('./app');
// 1. 全局增加 Jest 超时时间(解决钩子函数超时)
jest.setTimeout(15000); // 延长到15秒,覆盖服务器启动/关闭耗时
const TEST_PORT = 3301;
let server;
// 2. 优化前置启动逻辑:确保服务器真正启动后再执行测试
beforeAll((done) => {
// 先杀死可能占用测试端口的残留进程(关键:避免端口占用导致启动失败)
const killPortProcess = () => {
const { exec } = require('child_process');
exec(`lsof -ti:${TEST_PORT} | xargs kill -9 2>/dev/null`, (err) => {
if (err) console.log(`测试端口 ${TEST_PORT} 无残留进程`);
// 启动服务器
server = http.createServer(app);
// 监听错误,避免端口占用导致阻塞
server.on('error', (err) => {
console.error('服务器启动失败:', err);
done(err);
});
// 确认服务器监听成功后执行done
server.listen(TEST_PORT, () => {
console.log(`测试服务器启动在端口 ${TEST_PORT}`);
done();
});
});
};
killPortProcess();
});
// 3. 优化后置关闭逻辑:强制关闭+容错处理
afterAll((done) => {
if (!server) {
console.log('服务器未启动,直接结束');
return done();
}
// 强制关闭服务器,忽略错误
server.close((err) => {
if (err) {
console.warn('服务器关闭警告:', err);
}
// 兜底:再次杀死测试端口进程
const { exec } = require('child_process');
exec(`lsof -ti:${TEST_PORT} | xargs kill -9 2>/dev/null`, () => {
console.log(`测试端口 ${TEST_PORT} 已释放`);
done();
});
});
});
// 封装请求方法(增加超时)
const sendRequest = (path, method = 'GET') => {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost',
port: TEST_PORT,
path: path,
method: method,
timeout: 5000 // 请求超时5秒
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: data
});
});
});
// 请求超时处理
req.on('timeout', () => {
req.destroy();
reject(new Error(`请求 ${path} 超时`));
});
req.on('error', (err) => reject(err));
req.end();
});
};
// 测试用例(保持不变,或增加单例超时)
describe('Express 服务器测试(原生 http 模块)', () => {
// 单个测试用例超时(可选)
test('GET / 应返回 200 状态码和 HTML 内容', async () => {
const response = await sendRequest('/');
expect(response.statusCode).toBe(200);
expect(response.headers['content-type']).toMatch(/text\/html/);
const indexPath = path.join(__dirname, 'public', 'index.html');
const expectedHtml = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '';
expect(response.body).toBe(expectedHtml);
}, 10000); // 单个用例超时10秒
test('静态资源 /css/style.css 可访问', async () => {
const cssPath = path.join(__dirname, 'public/css/style.css');
if (!fs.existsSync(cssPath)) {
fs.mkdirSync(path.dirname(cssPath), { recursive: true });
fs.writeFileSync(cssPath, 'body { margin: 0; }');
}
const response = await sendRequest('/css/style.css');
expect(response.statusCode).toBe(200);
expect(response.headers['content-type']).toMatch(/text\/css/);
expect(response.body).toBe('body { margin: 0; }');
}, 10000);
test('GET /non-existent 应返回 404', async () => {
const response = await sendRequest('/non-existent');
expect(response.statusCode).toBe(404);
}, 10000);
});
其他文件非必须就不做展示了!
二、Jenkins 环境准备
1. 安装必要插件
在 Jenkins 插件管理中安装:
- Git Plugin(代码拉取)
- NodeJS Plugin(Node.js 环境管理)
- Pipeline Plugin(流水线支持)
- Plain Credentials Plugin(token认证)
#### 2. 配置 Node.js 环境 - 进入 Jenkins → 全局工具配置 → Node.js → 新增 Node.js
- 填写名称(如
Node.js 16.x),选择自动安装,保存
3. 配置 Git 凭证

- 进入 Jenkins → 凭证管理 → 全局凭证 → 添加凭证
- 类型选择「密钥文本」,填写github的Personal access tokens (classic),保存


三、Jenkins 流水线创建
- 新建「流水线」类型的任务;
- 在「流水线」配置中选择「Pipeline script from SCM」;
- 选择 Git,填写代码仓库地址、分支、凭证 ID;
- 脚本路径填写
Jenkinsfile,保存; - git代码推送时,即可触发自动化部署。


需要在git仓库里面配置Webhooks(网络钩子),关键步骤:通过钩子能及时把git的Pull信息推送给Jenkins,进行更新部署。格式:https://Jenkins用户名:APIToken@Jenkins服务器IP或域名+端口/github-webhook
而且还有Jenkins的用户授权
ApiToken配置Webhooks需要
github仓库地址以及分支
对应代码里面的文件名Jenkinsfile

四、服务器节点
服务器资源有限,就用一个节点演示!
配置服务器节点
新增节点
节点命名
节点服务器配置

配置服务器验证
保存后服务上会生成对应的包!
部署日志
五、测试
当把修改后的代码提交推送到github后,就会触发Jenekins的自动部署。
和github上的日志相同
点击Bulids下面的部署记录,可以看到每次自动化部署的日志。
六、验证部署
部署成功后,访问 http://服务器IP:3300,即可看到前端页面,表示部署生效。
- 公众号 行走之飞鱼
更多推荐
所有评论(0)