⚡ 已部署上线! 截止本文更新时,流水线已成功运行,服务正在服务器上正常运行。
📋 概述
本文详细记录了一套完整的 CI/CD 自动化部署流水线的搭建过程,实现:
- 本地提交代码到 GitHub
dev分支 - GitHub Actions 自动触发构建
- Maven 编译打包 Spring Boot JAR
- 通过 SCP 上传 JAR 到服务器
- 自动重启 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 配置步骤截图指引
-
打开 GitHub 仓库 → Settings
-
左侧导航 → Secrets and variables → Actions
-
点击 New repository secret
-
分别添加上述 3 个 Secret
-
添加完成后应看到:
✅ 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 查看流水线执行
-
打开 GitHub 仓库 → Actions 标签页

-
可以看到
Build and Deploy projectXXXX工作流正在运行
-
点击进入可查看每个步骤的实时日志

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 服务