[特殊字符] 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-actionappleboy/ssh-action 后,它们内部处理 SSH 连接更稳健,完美解决这个问题。

6.3 流水线步骤详解

步骤 作用 关键参数
Checkout 拉取代码 actions/checkout@v4
JDK 8 安装 Java 8 temurin 发行版, 启用 Maven 缓存
Build Maven 编译打包 mvn -B clean package -DskipTestsworking-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 variablesActions

  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 服务
相关推荐
csdn小瓯1 小时前
本周 GitHub 热门项目推荐:Headroom 和 CC Switch
人工智能·github·开源项目
CodeStats1 小时前
JavaWeb 造轮者视角:Spring Boot 启动核心思想与完整链路解析
java·spring boot·后端
Sam_Deep_Thinking15 小时前
Spring Boot 的启动原理是什么?
java·spring boot·后端
屋外雨大,惊蛰出没15 小时前
深入浅出Spring Boot
java·spring boot·ioc·aop
协享科技16 小时前
Spring Boot 与 Go 双服务架构实践:从单体拆分到通信设计
java·人工智能·spring boot·后端·架构·golang·ai编程
Jurio.17 小时前
开源 Codex Sticky:在终端 Codex CLI 长对话中始终固定底部输入框
linux·rust·github·开源软件·codex·codex cli
小林敲代码778818 小时前
记录一下IDEA中很多变量变色的方案
java·开发语言·spring boot·idea
半夜修仙18 小时前
RabbitMQ中如何保证消息的可靠性传输
java·分布式·中间件·rabbitmq·github·java-rabbitmq
Flittly18 小时前
【AgentScope Java新手村系列】(3)工具系统
java·spring boot·spring