node全栈(二):Express 接口自动化部署实战 —— Jenkins + PM2 上线指南

72 阅读4分钟

文章前言

永远把别人对你的批评记在心里,别人的表扬,就把它忘了。Hello 大家好~!我是淮桑榆

本文主要是记录如何在阿里云 Ubuntu 上使用 Jenkins + PM2 部署 Express 接口,特地记录下与诸位分享,如有阐述不对的地方欢迎在评论区指正。

观看到文章最后的话,如果觉得不错,可以点个关注或者点个赞哦!感谢~❤️

前系文章

node全栈(一):在阿里云 Ubuntu 上部署 Jenkins(Docker 方式)实战指南

文章主体

感谢各位观者的耐心观看,阿里云 Ubuntu 上使用 Jenkins + PM2 部署 Express 接口的正片即将开始,且听淮桑榆娓娓道来

image.png

本地开发Express接口

在本地开发一个基础的Express接口,暂时返回固定的测试数据

初始化项目

mkdir node-api && cd node-api //创建文件夹
npm init -y //初始化 npm 项目 
npm install express //安装

image.png

编写服务代码

在文件中创建index.js文件

const express = require('express')
const app = express()
const port = 3000

/* ------------------------------- 允许跨域,方面前端调用 ------------------------------ */
app.use((req, res, next)=>{
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    next()
})

/* ------------------------------- 用户接口,返回用户数据 ------------------------------ */
app.get('/api/user', (req, res)=>{
    const result = {
        status: 200,
        message: '成功返回用户数据',
        data: {
            age: 18,
            name: '淮桑榆',
            bobbies: ["编程", "服务器部署"]
        }
    }

    res.json(result)
})

app.listen(port, () => {
    console.log(`服务已启动,访问:http://localhost:${port}/api/user`);
})

本地测试

运行服务,在浏览器中访问 http://localhost:3000/api/user, 查看JSON数据是否正常返回

node app.js

image.png

代码托管到GitHub上

git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:huaisangyu/node-api.git
git push -u origin main

配置 SSH

在服务器终端生成SSH Key

在Docker容器中进入Jenkins运行环境

docker exec -it -u jenkins jenkins bash

创建 .ssh 目录并设置权限

mkdir -p /var/jenkins_home/.ssh
chmod 700 /var/jenkins_home/.ssh

在 Jenkins 容器中生成 SSH Key

ssh-keygen -t ed25519 -C "jenkins@huaisangyu" -f /var/jenkins_home/.ssh/id_ed25519 -N ""

设置私钥权限

chmod 600 /var/jenkins_home/.ssh/id_ed25519
chmod 644 /var/jenkins_home/.ssh/id_ed25519.pub

查看公钥(Jenkins中生成的)

cat /var/jenkins_home/.ssh/id_ed25519.pub

将公钥(Jenkins中生成的)添加到GitHub

  • 打开项目仓库 → 点击 Settings → 点击左侧 Deploy keys → 点击 Add deploy ke

  • 填写:

    • Title:Jenkins
    • Key:粘贴刚才的公钥
  • 保存

将公钥(Jenkins中生成的)复制到宿主机上

  • 在宿主机执行
sudo mkdir -p /root/.ssh
sudo chmod 700 /root/.ssh
  • 将公钥(Jenkins中生成的)追加到宿主机的
echo "公钥" | sudo tee -a /root/.ssh/authorized_keys
  • 设置权限
sudo chmod 600 /root/.ssh/authorized_keys
sudo chown root:root /root/.ssh/authorized_keys
  • 添加 GitHub SSH 主机到 known_hosts
# 在Jenkins容器里执行
ssh-keyscan -t ed25519 github.com >> /var/jenkins_home/.ssh/known_hosts
chmod 644 /var/jenkins_home/.ssh/known_hosts

测试免密登录

因为到时Jenkins执行Pipeline Script时要免密进入服务器

# 在Jenkins容器里执行
ssh -i /var/jenkins_home/.ssh/id_ed25519 -v root@宿主机IP

查看私钥(Jenkins中生成的)

cat /var/jenkins_home/.ssh/id_ed25519

在Jenkins直凭证

将上面的私钥填入下面的密闻里面

image.png

在宿主机中生成 SSH Key

chmod 700 .ssh
cd .ssh
ssh-keygen -t ed25519 -C "root@huaisangyu" -f /root/.ssh/id_ed25519 -N ""
ssh-keyscan github.com >> /root/.ssh/known_hosts
chmod 644 /root/.ssh/known_hosts

查看公钥(宿主机中生成的)

cat /root/.ssh/id_ed25519.pub

将公钥(宿主机中生成的)添加到GitHub

  • 打开项目仓库 → 点击 Settings → 点击左侧 Deploy keys → 点击 Add deploy ke

  • 填写:

    • Title:root
    • Key:粘贴刚才的公钥
  • 保存

配置 PM2

安装n(Node.js版本管理工具)

n 依赖 npm,所以我们需要先用 apt 装一个基础版 Node.js,仅用于跑 n,后续真正的 Node 版本由 n 接管

  • 用 apt 安装基础 Node(版本无所谓)
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt install -y nodejs

image.png

  • 使用npm安装n(全局)
npm install -g n

image.png

用n安装和管理Node.js

  • 安装你需要的Node版本
n lts

image.png

安装完成后,n会把Node装到:/usr/local/bin/node

  • 切换并确认Node版本
node -v
which node

image.png

我们发现node -v输出的还是老版本,这个时候我们要修复path

执行

export PATH="/usr/local/bin:$PATH"

并写入 shell 配置

bash 用户

echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

zsh 用户

echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

image.png

移除apt安装的Node(让n完全接管)

这一步非常重要,否则以后容易出现版本混乱

apt remove -y nodejs npm
node -v
npm -v
which node

如果能正常输出版本,并且路径是:/usr/local/bin/node,说明n接管成功了

全局安装 PM2

npm install -g pm2

image.png

Jenkins配置

新建 Jenkins 任务

  • Jenkins Dashboard → 新建Item(New Item)

  • 名称:node-api

  • 类型:流水线(Pipeline)

image.png

Pipeline Script

pipeline {
    agent any

    environment {
        DEPLOY_HOST = "公网ip"
        DEPLOY_USER = "root"
        DEPLOY_PATH = "/var/project/node-api"
        SSH_KEY = "/var/jenkins_home/.ssh/id_ed25519"
    }

    parameters {
        gitParameter(
            name: 'Branch',
            type: 'PT_BRANCH',
            defaultValue: 'main',
            description: '选择要部署的分支',
            branchFilter: 'origin/(.*)',
            selectedValue: 'DEFAULT',
            sortMode: 'NONE',
            quickFilterEnabled: true
        )

        string(name: 'Repository', defaultValue: 'git@github.com:huaisangyu/node-api.git', description: 'Git仓库地址')

        credentials(name: 'Creadential', defaultValue: 'github-ssh', description: '用于访问Git仓库的SSH私钥')
    }

    stages {

        stage('准备参数') {
            steps {
                script {
                    repo = params.Repository.trim()
                    branch = params.Branch
                    cred = params.Creadential

                    echo "使用分支:${branch}"
                    echo "使用仓库:${repo}"
                    echo "使用凭据ID:${cred}"
                }
            }
        }

        stage('刷新Git分支信息') {
            steps {
                echo "刷新Git Parameter分支列表"
                git changelog: true,   
                    branch: "${branch}",
                    url: "${repo}",
                    credentialsId: "${cred}" 
            }
        }

        stage('拉取代码') {
            steps {
                echo "在宿主机拉取最新代码,分支:${branch}"
                sh """
                    ssh -i ${SSH_KEY} -o StrictHostKeyChecking=yes ${DEPLOY_USER}@${DEPLOY_HOST} '
                        if [ ! -d ${DEPLOY_PATH} ]; then
                            git clone -b ${branch} ${repo} ${DEPLOY_PATH}
                        else
                            cd ${DEPLOY_PATH} && git fetch --all && git checkout ${branch} && git reset --hard origin/${branch}
                        fi
                    '
                """
            }
        }

        stage('安装依赖') {
            steps {
                echo "在宿主机安装依赖"
                sh """
                    ssh -i ${SSH_KEY} -o StrictHostKeyChecking=yes ${DEPLOY_USER}@${DEPLOY_HOST} 'cd ${DEPLOY_PATH} && npm install'
                """
            }
        }

        stage('启动/重启服务') {
            steps {
                echo "使用 PM2 启动或重启 Node 服务"
                sh """
                    ssh -i ${SSH_KEY} -o StrictHostKeyChecking=yes ${DEPLOY_USER}@${DEPLOY_HOST} '
                        cd ${DEPLOY_PATH} && pm2 start index.js --name node-api --update-env || pm2 restart node-api
                        pm2 save
                    '
                """
            }
        }

        stage('查看服务状态') {
            steps {
                echo "检查服务状态"
                sh """
                    ssh -i ${SSH_KEY} -o StrictHostKeyChecking=yes ${DEPLOY_USER}@${DEPLOY_HOST} 'pm2 status node-api'
                """
            }
        }
    }

    post {
        success {
            echo "部署成功 ✅ 分支:${branch}"
        }
        failure {
            echo "部署失败 ❌"
        }
    }
}

Jenkins控制台输出

image.png

网页访问

成功运行起项目并返回数据

image.png

部署其他分支

切换到test分支,并将数据返回调整下,然后提交到远程仓库

/*
 * @Description  : 
 * @Author       : Sherlock
 * @Date         : 2026-01-20 14:23:29
 * @LastEditors  : Sherlock
 * @LastEditTime : 2026-01-20 14:23:31
 * @FilePath     : /index.js
 */
const express = require('express')
const app = express()
const port = 3000

/* ------------------------------- 允许跨域,方面前端调用 ------------------------------ */
app.use((req, res, next)=>{
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    next()
})

/* ------------------------------- 用户接口,返回用户数据 ------------------------------ */
app.get('/api/user', (req, res)=>{
    const result = {
        status: 200,
        message: '成功返回用户数据',
        data: {
            age: 18,
            name: '测试家的淮桑榆',
            bobbies: ["编程", "服务器部署"]
        }
    }

    res.json(result)
})

app.listen(port, () => {
    console.log(`服务已启动,访问:http://localhost:${port}/api/user`);
})

现在回到Jenkins界面,然后选择test分支后点击Build

image.png

然后我们再次进行网页访问,发现数据已经产生变化了

image.png

配置HTTPS(加密访问)

在已经实现HTTP反向代理的基础上,通过 Let's Encrypt 申请免费 SSL 证书,将访问地址从 http:// 升级为 https://(如 https://huaisangyu.top/api/user),并自动将 HTTP 请求重定向到 HTTPS

安装Cerbot

sudo apt update
sudo apt install certbot python3-certbot-nginx -y

申请 SSL 证书并自动配置 Nginx

替换为你的域名(与 Nginx 配置中的 server_name 一致)
sudo certbot --nginx -d 你的域名
执行过程中的交互选项:
  1. 输入邮箱(用于证书过期提醒)→ 回车
  2. 同意服务条款(输入 Y)→ 回车
  3. 是否共享邮箱(输入 N 拒绝)→ 回车
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): jianpoli0214@gmail.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.6-August-18-2025.pdf. You must agree
in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Account registered.
Requesting a certificate for huaisangyu.top
成功标志:

输出包含以下内容,说明证书申请并配置成功:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/huaisangyu.top/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/huaisangyu.top/privkey.pem
This certificate expires on 2026-04-29.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for huaisangyu.top to /etc/nginx/sites-enabled/jenkins
Congratulations! You have successfully enabled HTTPS on https://huaisangyu.top

验证HTTPS配置

  • 访问测试

    在浏览器中打开 huaisangyu.top/api/user ,地址栏会显示小锁图标(表示加密连接),并返回正常数据

image.png

  • 重定向测试

    访问 huaisangyu.top/api/user (HTTP),会自动跳转到 https:// 版本

  • 证书信息查看

    点击浏览器地址栏的小锁 →「证书」,可查看证书有效期(默认 90 天)

image.png

配置证书自动续期

Let's Encrypt 证书有效期为 90 天,需设置自动续期:

# 测试自动续期功能(无错误输出即为正常) 
sudo certbot renew --dry-run
# 添加定时任务(每天自动检查续期) 
sudo crontab -e

在打开的文件中添加一行(每天凌晨 2 点执行续期检查):

0 2 * * * /usr/bin/certbot renew --quiet

保存退出:Ctrl+O → 回车 → Ctrl+X

结尾营业

看官都看到这了,如果觉得不错,可不可以不吝啬你的小手手帮忙点个关注或者点个赞

711115f2517eae5e.gif