在ubuntu上使用jenkins部署.net8程序

目标

我有一台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
                """
            }
        }
    }
} 

说明:

  • 本项目有两个部署环境:prodtest环境

  • 两个环境有如下差异:

    • 发布后,生成文件到文件夹不一样,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:ENVENV要和如下的对应:

提交Jenkinsfile到git仓库

编写之后的Jenkinsfile放这里:

bash 复制代码
git add Jenkinsfile
git commit -m "添加Jenkinsfile"
git push

构建

回到Jenkins的网页主界面:

这里随便选一个,然后点击Build,即可创建。

相关推荐
CodeHackerBhx5 小时前
Jenkins
java·运维·jenkins
会飞的灰大狼12 小时前
MySQL主从复制部署
linux·mysql·ubuntu·centos7
__Smile°15 小时前
Gitlab+Jenkins+K8S+Registry 建立 CI/CD 流水线
linux·ci/cd·docker·kubernetes·gitlab·jenkins
慕y27415 小时前
Java学习第一百零九部分——Jenkins(一)
java·学习·jenkins
Allenliu _Andy15 小时前
基于 Jenkins Pipeline 实现 DITA 文档自动化构建与发布(开源方案)
自动化·jenkins·技术文档·dita·git actions
东东今天敲代码了吗17 小时前
Ubuntu20.04 离线安装 FFmpeg 静态编译包
linux·运维·服务器·ubuntu·ffmpeg
kobe_OKOK_17 小时前
查看ubuntu server 的基本信息
数据库·ubuntu·postgresql
云中小生18 小时前
ASP.NET Core MVC修仙指南
.net
白云coy1 天前
如何在 Ubuntu 24.04 LTS 上安装 Docker
ubuntu·docker·eureka