[特殊字符] CI/CD 流水线搭建实战指南:Spring Boot + GitHub Actions → 服务器自动部署

⚡ 已部署上线! 截止本文更新时,流水线已成功运行,服务正在服务器上正常运行。


📋 概述

本文详细记录了一套完整的 CI/CD 自动化部署流水线的搭建过程,实现:

  1. 本地提交代码到 GitHub dev 分支
  2. GitHub Actions 自动触发构建
  3. Maven 编译打包 Spring Boot JAR
  4. 通过 SCP 上传 JAR 到服务器
  5. 自动重启 systemd 服务完成部署

1. 环境说明

1.1 项目信息

项目 值
项目地址 https://github.com/xxxxx/projectXXXX.git
分支策略 dev → 自动部署
项目类型 Spring Boot 2.6.13
JDK 版本 Java 8
构建工具 Maven
项目端口 9092

1.2 服务器信息

项目 值
服务器 IP 111.11.222.111
系统 OpenCloudOS (Linux 5.4.119)
用户 xxxxx (UID 1005, sudo 权限)
工作目录 /home/xxxxx
Shell /bin/bash

1.3 架构图

复制代码
┌───────────────────────────────────────────────────────────┐
│                       开发者本地                            │
│  ┌────────────────────────────────────────────────────┐   │
│  │  IDE (IntelliJ) → Git commit → git push origin dev │   │
│  └──────────────────────┬─────────────────────────────┘   │
└─────────────────────────┼─────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                  GitHub (云端)                           │
│  ┌──────────────────────────────────────────────────┐   │
│  │  dev 分支收到 push → 触发 GitHub Actions           │   │
│  │  ┌─────────────────────────────────────────┐     │   │
│  │  │ 1. Checkout 代码                         │     │   │
│  │  │ 2. 设置 JDK 8 + Maven 缓存               │     │   │
│  │  │ 3. mvn clean package 编译打包            │     │   │
│  │  │ 4. SCP 上传 JAR 到服务器                  │     │   │
│  │  │ 5. SSH 执行 deploy.sh 部署               │     │   │
│  │  └─────────────────────────────────────────┘     │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────┼───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                  服务器                                  │
│  ┌──────────────────────────────────────────────────┐   │
│  │ /opt/projectXXXX/                         │   │
│  │  ├── releases/           ← 存放历史 JAR 包         │   │
│  │  ├── current.jar         ← 当前运行版本(软链接)     │   │
│  │  └── deploy.sh           ← 部署脚本                │   │
│  │                                                   │   │
│  │ systemd: projectXXXX.service               │   │
│  │  → 自动重启, 失败回滚, 保留最近5个版本                 │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

2. 服务器端环境准备

2.1 登录服务器检查环境

bash 复制代码
# 通过 SSH 密码登录
ssh xxxxx@111.11.222.111

# 检查系统信息
uname -a
# Linux VM-8-13-opencloudos 5.4.119-20.0009.20 #1 SMP x86_64 GNU/Linux

# 检查 Java
java -version
# openjdk version "1.8.0_492"

服务器已经预装了 Java 8,无需额外安装。

2.2 创建应用目录结构

bash 复制代码
# 创建应用目录和 releases 子目录
sudo mkdir -p /opt/projectXXXX/releases

# 将目录所有权交给部署用户
sudo chown -R xxxxx:xxxxx /opt/projectXXXX

目录结构说明:

路径 用途
/opt/projectXXXX/ 应用根目录
/opt/projectXXXX/releases/ 存放历史版本 JAR 包
/opt/projectXXXX/current.jar 当前运行版本的软链接
/opt/projectXXXX/deploy.sh 部署脚本

3. SSH 密钥认证配置

🔑 为什么要配密钥?CI/CD 流水线需要自动登录服务器,使用 SSH 密钥可以实现免密登录,更安全也更可靠。

3.1 本地生成部署专用密钥对

⚠️ 注意 :GitHub Actions 的 Ubuntu runner 对密钥格式兼容性有限,必须使用 RSA PEM 格式。不要使用 ed25519。

bash 复制代码
# 在本地 Windows 电脑上生成 RSA 4096 PEM 格式密钥对
ssh-keygen -t rsa -b 4096 -m PEM -f ~/.ssh/github_actions_deploy_pem -N "" -C "github-actions-deploy@projectXXXX"

生成的文件:

  • ~/.ssh/github_actions_deploy_pem --- 私钥(⚠️ 绝不要泄露,待会要添加到 GitHub Secret)
  • ~/.ssh/github_actions_deploy_pem.pub --- 公钥(需要添加到服务器 authorized_keys)

3.2 将公钥添加到服务器

bash 复制代码
# 查看公钥内容
cat ~/.ssh/github_actions_deploy_pem.pub
# ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCz... github-actions-deploy@projectXXXX

# 登录服务器
ssh xxxxx@111.11.222.111

# 将公钥追加到 authorized_keys
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "ssh-rsa AAAAB3NzaC1y...完整公钥..." >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

3.3 验证免密登录

bash 复制代码
# 使用部署密钥测试 SSH 连接
ssh -i ~/.ssh/github_actions_deploy_pem xxxxx@111.11.222.111 "echo SSH_LOGIN_OK"
# 输出: SSH_LOGIN_OK ✅

4. 部署脚本编写

部署脚本是整个流水线的核心执行者,负责替换 JAR 包、重启服务、健康检查、失败回滚。

4.1 deploy.sh 完整内容

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

APP_NAME="projectXXXX"
APP_DIR="/opt/${APP_NAME}"
RELEASES_DIR="${APP_DIR}/releases"
CURRENT_JAR="${APP_DIR}/current.jar"
SERVICE_NAME="${APP_NAME}"
NEW_RELEASE="${1:-}"

if [[ -z "${NEW_RELEASE}" ]]; then
  echo "Usage: $0 <release-jar-name>"
  exit 1
fi

NEW_JAR="${RELEASES_DIR}/${NEW_RELEASE}"
if [[ ! -f "${NEW_JAR}" ]]; then
  echo "Release jar not found: ${NEW_JAR}"
  exit 1
fi

# 保存当前版本以便回滚
PREVIOUS_TARGET=""
if [[ -L "${CURRENT_JAR}" ]]; then
  PREVIOUS_TARGET="$(readlink -f "${CURRENT_JAR}")"
elif [[ -f "${CURRENT_JAR}" ]]; then
  PREVIOUS_TARGET="${CURRENT_JAR}"
fi

echo "Deploying ${NEW_JAR}"
ln -sfn "${NEW_JAR}" "${CURRENT_JAR}"

sudo /usr/bin/systemctl daemon-reload
sudo /usr/bin/systemctl restart "${SERVICE_NAME}"
sleep 8

# 健康检查
if sudo /usr/bin/systemctl is-active --quiet "${SERVICE_NAME}"; then
  echo "Deploy success: ${NEW_RELEASE}"
  # 只保留最近 5 个版本
  ls -1t "${RELEASES_DIR}"/*.jar 2>/dev/null | tail -n +6 | xargs -r rm -f
  exit 0
fi

# 部署失败,执行回滚
echo "Deploy failed, attempting rollback..."
if [[ -n "${PREVIOUS_TARGET}" && -f "${PREVIOUS_TARGET}" ]]; then
  ln -sfn "${PREVIOUS_TARGET}" "${CURRENT_JAR}"
  sudo /usr/bin/systemctl restart "${SERVICE_NAME}"
  sleep 8
  if sudo /usr/bin/systemctl is-active --quiet "${SERVICE_NAME}"; then
    echo "Rollback successful"
    exit 1
  fi
fi

echo "Rollback unavailable or failed"
exit 1

4.2 部署脚本的核心特性

特性 说明
✅ 原子切换 使用软链接 current.jar 指向新版本,不中断服务文件
✅ 自动回滚 新版本启动失败时自动切回上一版本
✅ 版本保留 保留最近 5 个版本,自动清理旧包
✅ 幂等操作 每次部署参数独立,可重复执行

4.3 配置 sudo 免密(关键!)

部署脚本中使用了 sudo systemctl,但 GitHub Actions 远程执行时没有 TTY 输入密码,必须配置 NOPASSWD。

bash 复制代码
# 登录服务器
ssh xxxxx@111.11.222.111

# 创建 sudoers 规则:允许 xxxxx 免密执行 systemctl 命令
echo 'xxxxx ALL=(ALL) NOPASSWD: /usr/bin/systemctl' | sudo tee /etc/sudoers.d/xxxxx-systemctl
sudo chmod 440 /etc/sudoers.d/xxxxx-systemctl

# 验证
sudo -n /usr/bin/systemctl status projectXXXX --no-pager | head -5

4.4 上传脚本到服务器

bash 复制代码
# 将部署脚本上传到服务器
scp -i ~/.ssh/github_actions_deploy_pem deploy.sh xxxxx@111.11.222.111:/home/xxxxx/

# 设置执行权限
chmod +x /home/xxxxx/deploy.sh

# 复制到应用目录
sudo cp /home/xxxxx/deploy.sh /opt/projectXXXX/deploy.sh
sudo chmod +x /opt/projectXXXX/deploy.sh

5. Systemd 服务配置

使用 systemd 管理 Spring Boot 应用进程,实现自动重启、日志管理和开机自启。

5.1 创建服务文件

ini 复制代码
# /etc/systemd/system/projectXXXX.service
[Unit]
Description=projectXXXX Spring Boot Application
After=network.target

[Service]
User=xxxxx
WorkingDirectory=/opt/projectXXXX
ExecStart=/usr/bin/java -jar /opt/projectXXXX/current.jar
SuccessExitStatus=143
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

关键配置说明:

配置项 值 说明
User xxxxx 以普通用户运行,不暴露 root
ExecStart java -jar current.jar 通过软链接引用,方便切换版本
SuccessExitStatus 143 Spring Boot 优雅关闭(SIGTERM)的退出码
Restart always 崩溃或退出时自动重启
RestartSec 10 重启间隔 10 秒

5.2 注册并启动服务

bash 复制代码
# 复制服务文件
sudo cp projectXXXX.service /etc/systemd/system/

# 加载配置
sudo systemctl daemon-reload

# 启用开机自启
sudo systemctl enable projectXXXX

6. GitHub Actions 工作流编写

GitHub Actions 在每次 push 到 dev 分支时自动触发构建和部署。

⚠️ 重要:工作流文件位置

工作流文件必须放在仓库根目录 的 .github/workflows/ 下,GitHub 才能识别。不能放在子目录里。

复制代码
仓库根目录/               ← 这里!
├── .github/
│   └── workflows/
│       └── deploy.yml   ← 工作流文件
├── projectXXXX/  ← 项目代码
│   ├── pom.xml
│   └── src/

6.1 工作流文件

yaml 复制代码
# .github/workflows/deploy.yml
name: Build and Deploy projectXXXX

on:
  push:
    branches:
      - dev
  workflow_dispatch:

concurrency:
  group: projectXXXX-deploy
  cancel-in-progress: true

jobs:
  build-and-deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      - name: Set up JDK 8
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '8'
          cache: maven

      - name: Build Spring Boot JAR
        working-directory: ./projectXXXX
        run: mvn -B clean package -DskipTests

      - name: Get JAR filename
        id: jar
        working-directory: ./projectXXXX
        run: |
          JAR_FILE=$(ls target/*.jar | head -1)
          echo "file=projectXXXX/$JAR_FILE" >> $GITHUB_OUTPUT
          echo "name=$(basename $JAR_FILE)" >> $GITHUB_OUTPUT

      - name: Upload JAR via SCP
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          port: 22
          source: "${{ steps.jar.outputs.file }}"
          target: "/opt/projectXXXX/releases/"
          strip_components: 2
          command_timeout: 60s

      - name: Rename uploaded JAR
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          port: 22
          script: |
            RELEASE_NAME="projectXXXX-${{ github.sha }}.jar"
            UPLOADED_FILE=$(ls /opt/projectXXXX/releases/projectXXXX-*.jar 2>/dev/null | head -1)
            mv "$UPLOADED_FILE" "/opt/projectXXXX/releases/${RELEASE_NAME}"

      - name: Deploy on server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          port: 22
          script: |
            RELEASE_NAME="projectXXXX-${{ github.sha }}.jar"
            bash /opt/projectXXXX/deploy.sh ${RELEASE_NAME}

6.2 为什么使用 appleboy actions?

最初尝试直接用 scp + ssh 原生命令,但遇到了 error in libcrypto 错误------GitHub runner 的 OpenSSL 版本解析 Windows 生成的 SSH 私钥时兼容性有问题。

改用 appleboy/scp-action 和 appleboy/ssh-action 后,它们内部处理 SSH 连接更稳健,完美解决这个问题。

6.3 流水线步骤详解

步骤 作用 关键参数
Checkout 拉取代码 actions/checkout@v4
JDK 8 安装 Java 8 temurin 发行版, 启用 Maven 缓存
Build Maven 编译打包 mvn -B clean package -DskipTests,working-directory: ./projectXXXX
JAR 定位 定位生成的 JAR 包 动态获取文件名,避免硬编码
上传 JAR appleboy/scp-action 上传 strip_components: 2 剥离目录前缀
重命名 appleboy/ssh-action 重命名 按 项目名-commitSHA.jar 格式命名
部署 appleboy/ssh-action 执行 调用 deploy.sh 完成服务切换

7. GitHub Secrets 配置

GitHub Secrets 用于安全存储敏感信息,流水线运行时自动注入。

7.1 需要配置的 Secrets

在 GitHub 仓库页面 → Settings → Secrets and variables → Actions 中添加:

Secret 名称 值 说明
SERVER_SSH_KEY 部署私钥全文 GitHub Actions 用来 SSH 登录服务器
SERVER_HOST 111.11.222.111 服务器 IP 地址
SERVER_USER xxxxx 服务器登录用户名

7.2 获取私钥内容

bash 复制代码
# 在 Windows 本地执行
cat ~/.ssh/github_actions_deploy_pem

# 复制完整输出(包括 -----BEGIN RSA PRIVATE KEY----- 头尾)
# 粘贴到 GitHub 的 SERVER_SSH_KEY 字段中

⚠️ 务必使用 PEM 格式的 RSA 私钥 (github_actions_deploy_pem),不要使用 ed25519 格式。

GitHub Actions runner 不支持 OpenSSH 新版私钥格式,会报 error in libcrypto。

7.3 配置步骤截图指引

  1. 打开 GitHub 仓库 → Settings

  2. 左侧导航 → Secrets and variables → Actions

  3. 点击 New repository secret

  4. 分别添加上述 3 个 Secret

  5. 添加完成后应看到:

    ✅ SERVER_SSH_KEY (Updated)
    ✅ SERVER_HOST (Updated)
    ✅ SERVER_USER (Updated)


8. 本地提交触发部署

8.1 提交并推送代码

bash 复制代码
# 进入项目目录
cd C:\Users\xxxxx\IdeaProjects\projectXXXX

# 切换到 dev 分支
git checkout dev

# 添加修改的文件
git add .

# 提交
git commit -m "feat: 修改xxx功能"

# 推送到 GitHub
git push origin dev

⚠️ 如果推送时提示需要 GitHub 认证,建议配置 Personal Access Token:

bash 复制代码
git remote set-url origin https://<token>@github.com/xxxxx/projectXXXX.git

8.2 查看流水线执行

  1. 打开 GitHub 仓库 → Actions 标签页

  2. 可以看到 Build and Deploy projectXXXX 工作流正在运行

  3. 点击进入可查看每个步骤的实时日志

8.3 流水线执行结果示例

复制代码
✓ Checkout source code              (3s)
✓ Set up JDK 8                      (12s)
✓ Build Spring Boot JAR             (45s)
✓ Get JAR filename                  (1s)
✓ Prepare SSH key                   (2s)
✓ Upload JAR to server              (5s)
✓ Deploy on server                  (10s)

Deploy success: projectXXXX-abc123def.jar

9. 完整流水线测试

9.1 验证部署结果

bash 复制代码
# 登录服务器检查服务状态
ssh xxxxx@111.11.222.111

# 查看 systemd 服务状态
sudo systemctl status projectXXXX

# 输出示例:
● projectXXXX.service - projectXXXX Spring Boot Application
   Loaded: loaded (/etc/systemd/system/projectXXXX.service; enabled)
   Active: active (running) since Thu 2026-06-11 08:45:00 CST; 2min ago
 Main PID: 12345 (java)
    Tasks: 25 (limit: 49292)
   Memory: 256.0M

9.2 测试应用访问

bash 复制代码
# 测试应用是否正常响应
curl http://localhost:9092/actuator/health
# 或
curl http://111.11.222.111:9092/你的接口

9.3 检查版本文件

bash 复制代码
# 查看 releases 目录
ls -la /opt/projectXXXX/releases/
# 输出:
# projectXXXX-abc123def.jar
# projectXXXX-旧版本SHA.jar

# 查看 current.jar 指向
readlink -f /opt/projectXXXX/current.jar
# /opt/projectXXXX/releases/projectXXXX-abc123def.jar

10. 总结与优化建议

✅ 已完成

模块 状态
服务器目录结构 ✅
SSH 密钥认证(RSA PEM 格式) ✅
sudo NOPASSWD 配置 ✅
部署脚本 deploy.sh ✅
systemd 服务 ✅
GitHub Actions 流水线(appleboy actions) ✅
GitHub Secrets 配置 ✅ 已配置 3 个 Secret
流水线首次部署 ✅ 服务已正常运行

🔧 后续优化建议

1. 健康检查增强
yaml 复制代码
# 在 deploy.sh 中添加 HTTP 健康检查,而不仅仅是 systemctl
curl -sf http://localhost:9092/actuator/health && echo "health check passed"
2. 部署通知
yaml 复制代码
# 在 GitHub Actions 中添加钉钉/飞书/微信通知
- name: Notify deployment result
  if: always()
  run: |
    curl -X POST -H "Content-Type: application/json" \
      -d '{"msgtype":"text","text":{"content":"部署结果: ${{ job.status }}"}}' \
      https://oapi.dingtalk.com/robot/send?access_token=xxx
3. Docker 容器化(推荐)
dockerfile 复制代码
FROM openjdk:8-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 9092
ENTRYPOINT ["java", "-jar", "app.jar"]

使用 Docker 可以避免服务器环境依赖问题。

4. 多环境支持
yaml 复制代码
# 通过 GitHub Environments 区分 dev/staging/prod
environment:
  name: dev
  url: http://111.11.222.111:9092
5. 自动化测试
yaml 复制代码
# 在部署前运行测试
- name: Run tests
  run: mvn test

当前已跳过测试 (-DskipTests),建议在稳定后启用。


附录

A. 常用命令速查

bash 复制代码
# 查看服务状态
sudo systemctl status projectXXXX

# 查看服务日志(实时)
sudo journalctl -u projectXXXX -f

# 重启服务
sudo systemctl restart projectXXXX

# 查看最近 50 行日志
sudo journalctl -u projectXXXX -n 50 --no-pager

# 手动执行部署
bash /opt/projectXXXX/deploy.sh <jar文件名>

B. 排错指南

问题 可能原因 解决方式
GitHub Actions 报 error in libcrypto 私钥格式不兼容(ed25519 OpenSSH 格式) 改用 RSA PEM 格式密钥并更新 Secret
GitHub Actions 报 sudo: a terminal is required 服务器 sudo 需要 TTY 输入密码 配置 NOPASSWD sudo 规则
GitHub Actions 报 SSH 错误 Secret 未配置或私钥格式错误 检查 SERVER_SSH_KEY 是否包含完整的 PEM 内容
appleboy SCP 上传失败 目录权限不足 sudo chown -Rxxxxx:xxxxx /opt/projectXXXX/
appleboy SSH 连接失败 密钥格式不对或 Secret 未更新 检查 SERVER_SSH_KEY 是否为 PEM 格式
服务启动失败 Java 版本不对或端口占用 java -version 检查, `netstat -tlnp
回滚也失败 没有有效的前一版本 手动上传一个已知正常的 JAR 包

C. 文件清单

复制代码
本地项目目录/
├── .github/
│   └── workflows/
│       └── deploy.yml          ← GitHub Actions 工作流
├── projectXXXX/
│   ├── pom.xml                 ← Maven 构建文件
│   ├── src/                    ← 源代码
│   └── target/                 ← 构建输出 (git ignored)

服务器目录 /opt/projectXXXX/
├── deploy.sh                   ← 部署脚本
├── current.jar                 ← 当前版本软链接
├── releases/                   ← 历史版本存储
│   ├── projectXXXX-SHA1.jar
│   └── projectXXXX-SHA2.jar

系统服务 /etc/systemd/system/
└── projectXXXX.service  ← systemd 服务
相关推荐
WeiXin_DZbishe11 小时前
基于springboot宠物领养系统-计算机毕设 附源码【课程设计】94217
vue.js·spring boot·python·django·php·课程设计
vx_bisheyuange11 小时前
基于SpringBoot的体育商品推荐系统
spring boot·毕业设计
Wx-bishekaifayuan12 小时前
express皖美特色农产品网售系统53118-计算机课程设计、毕业设计
人工智能·spring boot·后端·python·课程设计·express·旅游
Wx-bishekaifayuan13 小时前
springboot汉中旅游服务平台41442-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
WeiXin_DZbishe13 小时前
基于springboot城市公交实时运营与调度系统-计算机毕设 附源码【课程设计】66173
java·vue.js·spring boot·后端·python·php·课程设计
WeiXin_DZbishe13 小时前
基于Springboot+AI的kuall音乐广场的设计与实现-计算机毕设 附源码【课程设计】12715
vue.js·人工智能·spring boot·后端·python·php·课程设计
用户2022522150614 小时前
我在 DSH 做了个 AI 交付验收台:没有证据,就不算通过
github
前端市界14 小时前
Pro React Admin: 统一工程底座,承载多种应用...
前端·github
AIGC小尼15 小时前
毕设实战|SpringBoot+Vue 实现多个系统项目
vue.js·spring boot·课程设计