实战:VitePress 自动部署到宝塔服务器
本文记录将 VitePress 文档站点通过 GitHub Actions 自动部署到阿里云宝塔服务器的完整过程,涵盖踩坑经验、最终方案与可复用模板。项目地址:0science/vitepress,部署目标:8.138.28.212。
一、项目背景
- 静态站点:VitePress 构建产物在
docs/.vitepress/dist/ - 代码托管:GitHub 私有仓库
- 目标服务器:阿里云 ECS + 宝塔面板
- 需求:每次推送
master分支自动构建并部署,保留历史版本可回滚
二、架构设计
GitHub Push Actions Runner 阿里云 ECS
│ │ │
│ push event │ │
├──────────────────────▶│ │
│ │ npm ci + npm run build │
│ ├──────────────────────────┤
│ │ SSH + rsync 同步 │
│ ├──────────────────────────▶│
│ │ │ 备份旧文件
│ │ │ 部署新文件
│ │ │ 清理旧备份
│ │ HTTP 200 验证 │
│ │◀──────────────────────────┤
│ ✅ Deployed │ │核心设计:rsync 增量同步 + --backup 自动备份 + --exclude 保护证书文件 + 历史版本保留 N 个。
三、踩坑全过程
坑 1:package-lock.json 与 package.json 不同步
现象:Install dependencies 步骤报错
npm error `npm ci` can only install packages when your package.json
and package-lock.json or npm-shrinkwrap.json are in sync.
npm error Missing: search-insights@2.17.3 from lock file原因:本地 npm install 时缓存异常,lock 文件未正确更新;或某次合并时 lock 文件冲突解决不当。
解决:
# 仅更新 lock 文件,不实际安装依赖
npm install --package-lock-only --no-audit --no-fund
# 验证缺失的依赖已补全
grep -c "search-insights" package-lock.json
# 提交 lock 文件
git add package-lock.json
git commit -m "fix: 同步 package-lock.json,修复 npm ci 失败"
git push预防:本地开发用 npm install 后,提交前确保 package.json 和 package-lock.json 同步提交;CI 永远用 npm ci(保证可重现)。
坑 2:宝塔站点配置文件被误删
早期方案的问题:工作流用 rm -rf * 清空站点目录,会删除 .user.ini、.well-known/ 等 Let's Encrypt SSL 续签必需的文件,导致 SSL 证书无法续期。
解决方案:rsync 的 --exclude 精确保护关键文件:
rsync -avz --delete \
--backup --backup-dir="$BACKUP_DIR" \
--exclude='.well-known/' \
--exclude='.user.ini' \
--exclude='.htaccess' \
--exclude='.backup_*/' \
docs/.vitepress/dist/ \
user@ server:/path/踩坑教训:自动化部署前必须列出目标目录的所有文件,识别哪些是"基础设施文件"不能动。
四、最终成功方案
4.1 目录结构设计
/www/wwwroot/doc.dingjm.top/
├── index.html # 当前部署版本
├── assets/
├── logo.jpg
├── .user.ini # [保护] 宝塔 PHP 配置
├── .well-known/ # [保护] SSL 续签目录
├── .htaccess # [保护] Apache 配置
├── .backup_20260710_185500/ # 历史备份(保留 5 个)
├── .backup_20260709_120000/
└── ...4.2 完整 deploy.yml
name: Build and Deploy to Baota Server
on:
push:
branches: [master, main]
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
KEEP_VERSIONS: 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p ${{ secrets.SSH_PORT }} -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: Deploy with backup
run: |
BACKUP_DIR=".backup_$(date -u +%Y%m%d_%H%M%S)"
rsync -avz --delete \
--backup --backup-dir="$BACKUP_DIR" \
--exclude='.well-known/' \
--exclude='.user.ini' \
--exclude='.htaccess' \
--exclude='.backup_*/' \
-e "ssh -p ${{ secrets.SSH_PORT }} -i ~/.ssh/id_ed25519" \
docs/.vitepress/dist/ \
${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.DEPLOY_PATH }}/
- name: Cleanup old backups
if: success()
run: |
ssh -p ${{ secrets.SSH_PORT }} -i ~/.ssh/id_ed25519 \
${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
"cd ${{ secrets.DEPLOY_PATH }} && \
ls -1dt .backup_* 2>/dev/null \
| tail -n +$((${{ env.KEEP_VERSIONS }} + 1)) \
| xargs -r rm -rf"
- name: Verify site is reachable
if: success()
run: |
sleep 3
curl -sI -o /dev/null -w "HTTP Status: %{http_code}\n" \
http://${{ secrets.SERVER_HOST }}/4.3 所需 Secrets 清单
在 GitHub 仓库 → Settings → Secrets and variables → Actions 中添加:
| Secret 名 | 用途 | 示例值 |
|---|---|---|
SSH_PRIVATE_KEY | SSH 私钥完整内容(含 BEGIN/END 行) | -----BEGIN OPENSSH PRIVATE KEY-----... |
SERVER_HOST | 服务器 IP | 8.138.28.212 |
SSH_USER | SSH 用户名 | root |
SSH_PORT | SSH 端口 | 22 |
DEPLOY_PATH | 部署目标路径 | /www/wwwroot/doc.dingjm.top |
五、服务器端配置清单
5.1 宝塔创建站点
宝塔面板 → 网站 → 添加站点:
- 域名:
doc.dingjm.top(或 IP) - PHP 版本:纯静态
- 数据库:不创建
- SSL:申请 Let's Encrypt 证书
5.2 SSH 密钥配置
本地生成密钥对:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_key服务器添加公钥:
# 用密码登录服务器
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "你的公钥内容" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys本地验证免密登录:
ssh -i ~/.ssh/deploy_key root@8.138.28.2125.3 防火墙放行
阿里云安全组 + 宝塔安全 → 放行 22 端口(SSH)和 80/443 端口(HTTP/HTTPS)。
六、关键设计要点
6.1 为什么用 rsync 而不是 scp
| 方式 | 优点 | 缺点 |
|---|---|---|
scp | 简单 | 全量传输,慢;无增量;无排除功能 |
rsync | 增量同步、自动备份、排除文件、压缩传输 | 需要服务器安装 rsync |
宝塔服务器默认装了 rsync,无需额外配置。
6.2 为什么用 --backup 而不是手动备份
rsync --backup --backup-dir=DIR 在删除/覆盖旧文件时,自动把这些文件挪到 DIR 中:
- 只备份变化的文件(增量备份,磁盘占用小)
- 原子操作(备份和部署一步完成,不会有"备份了一半"的中间态)
- 自动时间戳命名(每次备份独立目录)
6.3 为什么要 --exclude 保护特定文件
宝塔环境的几个关键文件:
| 文件 | 用途 | 删除后果 |
|---|---|---|
.user.ini | 宝塔 PHP open_basedir 限制 | PHP 站点报错 |
.well-known/ | Let's Encrypt SSL 续签验证目录 | 证书到期无法续签 |
.htaccess | Apache 重写规则 | 路由失效 |
.backup_*/ | 历史备份目录 | 递归备份自身,磁盘爆炸 |
6.4 为什么要 concurrency 控制
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true- 同一分支连续推送多次时,自动取消历史运行
- 避免多个部署同时跑导致备份目录冲突
- 节省 Actions 配额
6.5 为什么用 npm ci 而不是 npm install
| 命令 | 用途 | 速度 | 安全性 |
|---|---|---|---|
npm install | 本地开发,会修改 lock | 慢 | 可能引入意外更新 |
npm ci | CI 环境,严格按 lock 安装 | 快 | 可重现构建 |
代价是:lock 文件必须与 package.json 同步,否则报错(我们踩的坑 1)。
七、故障排查清单
7.1 常见错误及解决
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Permission denied (publickey) | 私钥格式错或公钥没加到对应用户 | 检查私钥含 BEGIN/END;公钥在目标用户的 authorized_keys |
Load key: invalid format | 私钥换行被破坏 | 重新粘贴私钥到 Secret,确认换行符完整 |
Host key verification failed | known_hosts 没写入 | 工作流的 ssh-keyscan 步骤已处理 |
npm ci EUSAGE | lock 文件与 package.json 不同步 | 本地 npm install --package-lock-only 后提交 |
rsync: connection unexpectedly closed | 端口错或防火墙拦截 | 确认 SSH_PORT;阿里云安全组放行 |
| 部署成功但访问 404 | Nginx 根目录不对 | 宝塔站点设置确认根目录指向 DEPLOY_PATH |
| 部署成功但访问空白 | SSL 证书损坏 | 检查 .well-known 是否被误删 |
mkdir: cannot create directory | SSH 用户对目标路径无写权限 | chown -R user:user /www/wwwroot/your-site |
7.2 本地调试技巧
在本地复现 CI 行为,避免反复推送触发 Actions:
# 模拟 CI 构建
npm ci && npm run build
# 模拟 SSH 连接
ssh -i ~/.ssh/deploy_key root@8.138.28.212 "echo ok"
# 模拟 rsync 同步(先 dry-run 看会改什么)
rsync -avzn --delete \
--exclude='.well-known/' --exclude='.user.ini' \
docs/.vitepress/dist/ \
root@8.138.28.212:/www/wwwroot/doc.dingjm.top/
# 确认无误后去掉 -n 实际执行八、运维经验总结
8.1 部署前必查清单
- [ ] 本地
npm run build成功 - [ ]
package.json与package-lock.json同步 - [ ] SSH 免密登录验证通过
- [ ] rsync dry-run 确认变更范围正确
- [ ] 目标目录的"基础设施文件"已加入
--exclude - [ ] Secrets 名称与 yml 中引用完全一致
- [ ] 防火墙放行 SSH 端口
8.2 安全实践
- 最小权限:生产环境建议创建专用
deployer用户而非用 root - 密钥隔离:GitHub Action 用的密钥与日常 SSH 密钥分离
- 密钥轮换:密钥泄露(如粘贴到对话、聊天工具)后立即轮换
- 保护文件:用
--exclude防止自动化误删证书、配置 - 审计日志:Actions 运行历史本身就是审计记录,保留期默认 90 天
8.3 回滚方案
部署失败或发现问题时三种回滚方式:
方式 1:SSH 手动回滚(最快)
ssh root@8.138.28.212
cd /www/wwwroot/doc.dingjm.top
# 查看所有备份
ls -1dt .backup_*
# 用最近一个备份覆盖当前
rm -rf .rollback_tmp
cp -a .backup_20260710_185500 .rollback_tmp
rm -rf index.html assets/ # 删当前版本(保留保护文件)
mv .rollback_tmp/* .
rm -rf .rollback_tmp
# 注意:手动操作要小心 .well-known 等保护文件方式 2:GitHub Actions 重跑历史版本
在 Actions 页面找到稳定的 commit → Run workflow → 部署该版本
方式 3:git revert 触发重新部署
git revert <bad-commit>
git push # 自动触发部署到回滚后的版本8.4 持续优化方向
当前方案的局限与未来优化:
| 优化点 | 当前状态 | 改进方向 |
|---|---|---|
| 部署速度 | 全量 rsync 约 30s | 启用 rsync --compress 和 SSH 多路复用 |
| 健康检查 | 仅 HTTP 200 | 增加关键页面内容校验 |
| 通知机制 | 仅 Actions 日志 | 接入钉钉/企业微信机器人 |
| 多环境 | 单一生产 | 增加 staging 环境先发后切 |
| 蓝绿部署 | 备份式回滚 | 改用软链接原子切换 |
| 监控 | 无 | 接入 Uptime Robot 或宝塔监控 |
九、整个流程的关键认知
- CI/CD 不是工具而是流程:核心是用自动化替代手动,可重现替代"在我电脑上能跑"。
- lock 文件是契约:开发环境和 CI 环境的一致性靠
package-lock.json保证,不能随意修改或遗漏提交。 - 保护比删除重要:自动化部署最大的风险是误删,
--exclude优先于rm。 - 备份要可回滚:备份不能只是"留档",必须能快速恢复,rsync
--backup-dir自带这个能力。 - 本地调试远快于 CI 调试:每次推送后等几分钟看结果,比本地 dry-run 慢 10 倍。
- Secrets 是密钥唯一存放点:永远不要把密钥写进代码或日志,GitHub Secrets 是加密存储。
- 最小权限原则:哪怕是个人项目,也建议用专用 deployer 用户而非 root。
- 密钥泄露必须立即轮换:在对话、Issue、日志里贴过密钥的,全部视为已泄露。
十、参考资料
附:从零开始复刻此部署流程的步骤清单
如果你要在自己的项目中复刻这套部署:
- 本地:
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key生成密钥对 - 服务器:宝塔建站,公钥写入
~/.ssh/authorized_keys - 本地:
ssh -i ~/.ssh/deploy_key user@host验证免密登录 - 本地:把
.github/workflows/deploy.yml复制到你的项目 - 本地:根据你的构建命令调整
npm run build和dist路径 - GitHub:添加 5 个 Secrets(私钥、IP、用户、端口、路径)
- 本地:
git push触发首次部署 - 服务器:检查
ls -la /www/wwwroot/your-site/确认文件就位 - 浏览器:访问域名/IP 验证站点可访问
- 服务器:配置宝塔 Nginx 指向站点根目录,申请 SSL 证书
