目标
我有一台ubuntu服务器,上面运行着.net8程序,已经安装.net8,每次更新.net程序,需要做如下步骤:
- 先手动停止程序
- 复制文件到unbuntu服务器-
- 再启动服务
很繁琐,想着能不能搞个自动部署,于是就有了这篇文章。
本程序有两个部署环境,一个是测试环境,一个是生产环境,一般修改之后,部署到测试环境进行测试,测试没问题后再部署到生产环境
Jenkins安装官方文档
https://www.jenkins.io/doc/book/installing/linux/#debianubuntu
在Jenkins安装的时候,找过很多博客和各种其他网站,发现安装的有问题,后来就直接去官网,发现很方便就安装成功啦
安装JAVA
这个是为了安装Jenkin做准备的,最新的Jenkins需要java17或者java21,那直接整21吧
只需要一个命令即可安装(不需要各种下载压缩包然后解压):
bash
sudo apt install openjdk-21-jdk
运行之后,输入java -version
验证:
安装Jenkins
下载Jenkins的war包
下载地址:https://mirrors.tuna.tsinghua.edu.cn/jenkins/war-stable/2.516.1/jenkins.war
下载之后,把war包拷贝到ubuntu服务器目录
启动Jenkins
cd到war包所在的目录,然后运行:
bash
java -jar jenkins.war
或者后台运行:
bash
nohup java -jar jenkins.war &
然后在浏览器输入http://localhost:8080就可以访问了,其中localhost
替换为服务器IP地址
初次进入需要进行一些设置,插件安装就选推荐的就可以了
配置Jenkins项目
由于本项目的局域网git服务器是使用用户名和密码访问的,所以提前配置好
配置访问git项目的用户名和密码
在System域这里点击全局
点击Create
后,会多一条记录
新建项目
这里选择Pipleline script from SCM
意思是Jenkinsfile从git项目那里获取,这里不用写
编写项目的Jenkinsfile
bash
pipeline {
agent any
parameters {
choice(
name: 'ENV',
choices: ['test', 'prod'],
description: '部署环境'
)
}
environment {
// 项目配置
PROJECT_NAME = 'Test2025'
SOLUTION_FILE = 'Code/ServerTest20252025/Test20252025.sln'
STARTUP_PROJECT_PATH = 'Code/Server/Test2025/Test2025.Startup'
STARTUP_PROJECT_NAME = 'Test2025.Startup'
// .NET版本
DOTNET_VERSION = '8.0'
// 构建配置
BUILD_CONFIG = 'Release'
}
stages {
stage('Initialize') {
steps {
echo "当前部署环境: ${params.ENV}"
script {
// 根据环境设置配置
if (params.ENV == 'test') {
env.PORT = '5001'
env.PUBLISH_PATH = '/root/test2025/test2025_server/publish_test_env'
} else {
env.PORT = '5000'
env.PUBLISH_PATH = '/root/test2025/test2025_server/publish'
}
echo "端口: ${env.PORT}"
echo "发布路径: ${env.PUBLISH_PATH}"
}
}
}
stage('Checkout') {
steps {
echo '开始拉取代码...'
script {
// 使用Jenkins凭据进行完整的代码拉取(包括子模块)
withCredentials([usernamePassword(credentialsId: 'test', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
// 删除现有工作空间
sh 'rm -rf * .* 2>/dev/null || true'
// 配置git凭据助手
sh """
git config --global credential.helper '!f() { echo "username=\${GIT_USERNAME}"; echo "password=\${GIT_PASSWORD}"; }; f'
"""
// 克隆主仓库(包含子模块)
sh """
git clone --recursive http://10.70.19.29:28000/production/upper-computer/Test2025.git .
"""
// 切换到指定分支(如果需要)
sh """
git checkout master
"""
}
}
echo '代码拉取完成'
}
}
stage('Restore Dependencies') {
steps {
echo '开始还原NuGet包...'
sh """
dotnet restore "${SOLUTION_FILE}"
"""
echo 'NuGet包还原完成'
}
}
stage('Update Config') {
steps {
echo '更新配置文件...'
script {
if (params.ENV == 'test') {
echo '测试环境:设置数据库为 test2025_test'
sh """
# 备份原配置文件
cp "${STARTUP_PROJECT_PATH}/appsettings.json" "${STARTUP_PROJECT_PATH}/appsettings.json.backup"
# 更新数据库连接字符串为测试环境
sed -i 's/DATABASE=test2025;/DATABASE=test2025_test;/g' "${STARTUP_PROJECT_PATH}/appsettings.json"
# 验证更新结果
echo "当前数据库配置:"
grep "DATABASE=" "${STARTUP_PROJECT_PATH}/appsettings.json"
echo "配置文件已更新为测试环境"
"""
} else {
echo '生产环境:设置数据库为 test2025'
sh """
# 备份原配置文件
cp "${STARTUP_PROJECT_PATH}/appsettings.json" "${STARTUP_PROJECT_PATH}/appsettings.json.backup"
# 更新数据库连接字符串为生产环境
sed -i 's/DATABASE=test2025_test;/DATABASE=test2025;/g' "${STARTUP_PROJECT_PATH}/appsettings.json"
# 验证更新结果
echo "当前数据库配置:"
grep "DATABASE=" "${STARTUP_PROJECT_PATH}/appsettings.json"
echo "配置文件已更新为生产环境"
"""
}
}
}
}
stage('Build') {
steps {
echo '开始编译项目...'
sh """
dotnet build "${SOLUTION_FILE}" --configuration ${BUILD_CONFIG} --no-restore
"""
echo '项目编译完成'
}
}
stage('Stop Service') {
steps {
echo '停止现有服务...'
script {
// 检查端口是否被占用
def portCheck = sh(script: "netstat -tlnp 2>/dev/null | grep :${env.PORT} || ss -tlnp 2>/dev/null | grep :${env.PORT} || true", returnStdout: true).trim()
if (portCheck != '') {
echo "发现端口 ${env.PORT} 被占用,正在停止服务..."
sh """
# 使用fuser命令杀死占用端口的进程
fuser -k "${env.PORT}/tcp" 2>/dev/null || true
# 等待进程完全停止
sleep 3
# 再次检查端口是否释放
if netstat -tlnp 2>/dev/null | grep :${env.PORT} || ss -tlnp 2>/dev/null | grep :${env.PORT}; then
echo "端口 ${env.PORT} 仍被占用,尝试强制杀死进程..."
fuser -9 "${env.PORT}/tcp" 2>/dev/null || true
sleep 2
fi
"""
echo "端口 ${env.PORT} 的服务已停止"
} else {
echo "端口 ${env.PORT} 没有被占用,无需停止服务"
}
}
}
}
stage('Publish') {
steps {
echo '开始发布项目...'
sh """
# 创建发布目录
mkdir -p "${env.PUBLISH_PATH}"
# 发布项目
dotnet publish "${STARTUP_PROJECT_PATH}/${STARTUP_PROJECT_NAME}.csproj" --configuration ${BUILD_CONFIG} --output "${env.PUBLISH_PATH}" --no-build
# 设置执行权限
chmod +x "${env.PUBLISH_PATH}/${STARTUP_PROJECT_NAME}.dll"
"""
echo '项目发布完成'
}
}
stage('Start Service') {
steps {
echo '启动服务...'
sh """
cd "${env.PUBLISH_PATH}"
# JENKINS_NODE_COOKIE=dontKillMe 让Jenkins不杀死进程
JENKINS_NODE_COOKIE=dontKillMe nohup dotnet ${STARTUP_PROJECT_NAME}.dll --urls http://*:${env.PORT} > app.log 2>&1 &
echo \$! > app.pid
echo "服务已启动,PID: \$(cat app.pid)"
sleep 10
"""
echo '服务启动完成'
}
}
stage('Health Check') {
steps {
echo '检查服务健康状态...'
script {
def maxRetries = 10
def retryCount = 0
def isHealthy = false
while (retryCount < maxRetries && !isHealthy) {
try {
def response = sh(script: "curl -f http://localhost:${env.PORT}/api/Test 2>/dev/null", returnStdout: true).trim()
if (response != '') {
isHealthy = true
echo '服务健康检查通过'
}
} catch (Exception e) {
echo "健康检查失败,重试 ${retryCount + 1}/${maxRetries}"
}
if (!isHealthy) {
retryCount++
sleep(5)
}
}
if (!isHealthy) {
error '服务健康检查失败,部署可能有问题'
}
}
}
}
}
post {
always {
echo '恢复配置文件...'
sh """
# 恢复原始配置文件
if [ -f "${STARTUP_PROJECT_PATH}/appsettings.json.backup" ]; then
cp "${STARTUP_PROJECT_PATH}/appsettings.json.backup" "${STARTUP_PROJECT_PATH}/appsettings.json"
rm -f "${STARTUP_PROJECT_PATH}/appsettings.json.backup"
echo "配置文件已恢复"
fi
"""
echo '清理git凭据配置...'
sh """
git config --global --unset credential.helper || true
rm -f ~/.git-credentials || true
"""
echo '清理工作空间...'
// 注意:不要清理工作空间,因为服务进程依赖它
// cleanWs()
}
success {
echo '部署成功!'
script {
echo "部署时间: ${new Date().format('yyyy-MM-dd HH:mm:ss')}"
echo "部署环境: ${params.ENV}"
echo "部署路径: ${env.PUBLISH_PATH}"
echo "服务端口: ${env.PORT}"
}
}
failure {
echo '部署失败!'
script {
echo "失败时间: ${new Date().format('yyyy-MM-dd HH:mm:ss')}"
echo "部署环境: ${params.ENV}"
// 尝试重启服务
echo '尝试重启服务...'
sh """
if [ -f "${env.PUBLISH_PATH}/app.pid" ]; then
kill \$(cat "${env.PUBLISH_PATH}/app.pid") 2>/dev/null || true
rm -f "${env.PUBLISH_PATH}/app.pid"
fi
cd "${env.PUBLISH_PATH}"
nohup dotnet ${STARTUP_PROJECT_NAME}.dll > app.log 2>&1 &
echo \$! > app.pid
"""
}
}
}
}
说明:
-
本项目有两个部署环境:
prod
和test
环境 -
两个环境有如下差异:
- 发布后,生成文件到文件夹不一样,
test环境
是到publish_test_env
文件夹,prod环境
是到publish
文件夹 - 连接的数据库不一样,
test环境
连接的是test2025_test
数据库,prod环境
连接到test2025
数据库 - 监听的端口不一样,
test
环境监听5001
端口,prod
环境监听5000
端口
- 发布后,生成文件到文件夹不一样,
-
在Jenkins中使用界面Build时选择的参数:
声明:
bash
parameters {
choice(
name: 'ENV',
choices: ['test', 'prod'],
description: '部署环境'
)
}
使用:
bash
params.ENV
注意:这里的name:ENV
的ENV
要和如下的对应:
提交Jenkinsfile到git仓库
编写之后的Jenkinsfile放这里:
bash
git add Jenkinsfile
git commit -m "添加Jenkinsfile"
git push
构建
回到Jenkins的网页主界面:
这里随便选一个,然后点击Build,即可创建。