写在前面
每次写完博客,都要 SSH 登服务器 git pull + hugo 重新构建——写了三五十篇之后,这事就变得很烦。本文记录我用 GitHub Actions + SSH 搭建自动部署的完整过程:push 到 GitHub,服务器自动拉代码、重新构建,全程零手动。
这套方案适合自己有 VPS、用 Nginx 托管静态站点的同学。读完能直接照着配。
一、为什么要自动部署
1.1 手动部署的痛点
1
2
3
4
5
6
7
| 写完文章 → 本地预览 → git push → SSH 登服务器 → cd 目录 → git pull → hugo 构建 → 完成
每篇文章都要重复后三步,而且:
✗ 容易忘(push 了但没部署,线上还是旧的)
✗ 多设备时混乱(公司 push 了,家里没同步)
✗ 服务器上手动操作易出错(命令敲错、忘记更新主题)
✗ 出门没电脑就改不了博客
|
1.2 自动化之后
1
2
3
4
5
6
| 写完文章 → git push → 自动部署 → 线上更新
✓ push 即部署,所见即所得
✓ 任何设备、任何地方都能更新
✓ 不用记服务器命令
✓ 主题更新、配置改动都自动同步
|
二、部署方案对比
Hugo 静态站点的自动部署有几种主流方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 方案 原理 优点 缺点
──────────────────────────────────────────────────────────────────────────────
1. Actions SSH GitHub 通过 SSH 登服务器 复用现有流程 依赖服务器环境
执行 执行 git pull + hugo 服务器改动小 (git/hugo)
配置简单
2. Actions 构建 GitHub 跑 Hugo,rsync 推 构建不依赖服务器 要配 rsync +
+ rsync 静态文件到服务器 环境,更标准 nginx 目录
3. 服务器 cron 服务器定时 git pull + hugo 零 GitHub 配置 有几分钟延迟
最简单
4. 托管平台 Cloudflare Pages / Vercel 免服务器、全球 要改 DNS、
托管 / Netlify CDN、免费 可能不符现有架构
|
选型建议
1
2
3
4
| 有自己的 VPS + 现在手动 SSH 部署 → 方案1(本文)
有自己的 VPS,想让构建脱离服务器 → 方案2
不想碰 GitHub Actions → 方案3
没有服务器 / 想要免费 CDN → 方案4
|
本文用方案1:最贴合"已经在手动 SSH 部署"的现状,服务器几乎零改动,迁移成本最低。
三、方案1 的原理
很多人对 GitHub Actions 的 runs-on 有误解,先讲清楚。
3.1 整体流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 你 push 代码到 GitHub
│
▼
┌───────────────────┐
│ GitHub 虚拟机 │ ← runs-on: ubuntu-latest
│ (GitHub 免费提供) │ 这只是个"跑腿的"临时环境
│ │
│ 唯一的动作: │
│ 通过 SSH 连你的服务器│
└─────────┬─────────┘
│ SSH
▼
┌───────────────────┐
│ 你的 VPS (Debian等) │ ← 真正部署发生的地方
│ │
│ cd 仓库目录 │
│ git pull │
│ hugo 构建 │
│ → nginx 目录更新 │
└───────────────────┘
|
3.2 runs-on 不是你的服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
| runs-on: ubuntu-latest
这指定的是 GitHub Actions 的"执行环境"——
GitHub 免费给你的一台临时 Ubuntu 虚拟机。
它和你的服务器系统(Debian/CentOS 都行)无关。
这台虚拟机只负责一件事:发起 SSH 连接到你的服务器。
任务完成即销毁。
打个比方:
runs-on 的 Ubuntu = 快递员(跑腿的)
你的服务器 = 收件人(真正干活的地方)
快递员是谁不重要,能把包裹(SSH 命令)送到就行
|
理解这点很关键:部署逻辑跑在你的服务器上,GitHub 虚拟机只是触发器。
四、前置准备
4.1 服务器要求
1
2
3
4
| ✓ 一台 VPS / 云服务器,有 SSH 和 root(或 sudo)权限
✓ 已安装 git 和 hugo(手动部署过的话肯定有)
✓ Nginx(或其他 Web 服务器)已配置好,能托管静态文件
✓ 仓库已 git clone 到服务器(手动部署过的话肯定有)
|
4.2 生成专用部署密钥
关键:给 GitHub Actions 单独生成一对密钥,不要和你日常 SSH 的 key 混用。这样更安全(最小权限、可随时撤销)。
SSH 登服务器执行:
1
2
3
4
5
6
7
8
9
10
11
12
| # 1. 生成密钥对(ed25519 更安全更快,无密码)
ssh-keygen -t ed25519 -f ~/.ssh/github_actions -N "" -C "github-actions-deploy"
# 生成两个文件:
# ~/.ssh/github_actions 私钥(给 GitHub)
# ~/.ssh/github_actions.pub 公钥(留在服务器)
# 2. 把公钥加入授权列表(让持有私钥的 GitHub 能登录)
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# 3. 查看并复制私钥内容(下一步要填到 GitHub)
cat ~/.ssh/github_actions
|
1
2
3
4
5
6
| 复制私钥时要注意:
✓ 完整复制,包含 -----BEGIN/END OPENSSH PRIVATE KEY----- 两行
✓ 包含所有换行
✗ 不要漏掉任何字符
这把私钥只填到 GitHub Secrets,绝不放进代码仓库
|
五、配置 GitHub Secrets
Secrets 是 GitHub 加密存储的变量,workflow 里通过 ${{ secrets.XXX }} 引用,不会出现在日志和代码里。
打开仓库 → Settings → Secrets and variables → Actions → New repository secret,添加 3 个:
| Secret 名 | 值 | 说明 |
|---|
SERVER_HOST | 服务器 IP 或域名 | 如 1.2.3.4 或 jiwei.space |
SERVER_USER | SSH 用户名 | 如 root |
SERVER_SSH_KEY | 第四步复制的私钥 | 完整内容 |
1
2
3
| 如果你的 SSH 端口不是 22:
额外加一个 SERVER_PORT(如 2222)
workflow 里把 port 那行的注释去掉
|
六、编写 workflow
在仓库根目录创建 .github/workflows/deploy.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # Hugo 自动部署:push 到 main 后,通过 SSH 登服务器执行 git pull + hugo
name: Deploy
on:
push:
branches: [main] # push 到 main 时触发
workflow_dispatch: # 也允许在 Actions 页面手动触发
jobs:
deploy:
runs-on: ubuntu-latest # GitHub 提供的执行环境(不是你的服务器)
steps:
- name: SSH 登录服务器并部署
uses: appleboy/ssh-action@v1 # 成熟的 SSH Action
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
# 端口非 22 时,取消下行注释并配置 SERVER_PORT
# port: ${{ secrets.SERVER_PORT }}
script: |
set -e
cd /var/www/blog/site # ← 改成你服务器上仓库的路径
git pull --ff-only
# git submodule update --init --recursive # 更新主题时取消注释
hugo --minify --cleanDestinationDir -d /var/www/blog/public
echo "✅ 部署完成 $(date '+%F %T')"
|
6.1 逐段讲解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| on:
push:
branches: [main] # 监听 main 分支的 push
workflow_dispatch: # 加这个能在 Actions 页面点按钮手动跑
→ 每次 push 文章/配置,自动触发;也能手动重跑
uses: appleboy/ssh-action@v1
→ 社区维护的 SSH Action,稳定好用,不用自己写 SSH 逻辑
set -e
→ 任何命令失败立即中止(避免 pull 失败还继续 hugo)
cd /var/www/blog/site
→ 进到服务器上的仓库目录(手动部署时你 cd 的那个)
git pull --ff-only
→ 拉最新代码,--ff-only 禁止产生 merge commit(服务器不该有本地改动)
hugo --minify --cleanDestinationDir -d /var/www/blog/public
→ 三个参数的作用见下节
|
6.2 hugo 命令的三个关键参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| -d /var/www/blog/public
指定输出目录(-d = destination)
直接输出到 nginx 的 web 根目录,省去 cp/rsync 步骤
--minify
压缩 HTML/CSS/JS(去注释、去空白)
省带宽、提升加载速度、对 SEO 友好
生产环境建议加上
--cleanDestinationDir
构建前清理目标目录里不在生成的文件
⚠️ 重要:如果不加,删了某篇文章后旧的 HTML 还会残留
访问者可能撞到已删除的页面
前提:目标目录只放 Hugo 输出(别混别的文件)
|
6.3 主题是 submodule 的话
1
2
3
4
5
6
7
8
| 如果主题用 git submodule 管理(很多 Hugo 主题推荐这么做),
git pull 不会自动同步 themes/ 目录。
哪天你更新了主题(git submodule update --remote themes/stack),
记得把这行的注释取消:
git submodule update --init --recursive
不更新主题就不用管,保持注释即可。
|
七、触发与验证
7.1 提交触发
1
2
3
| git add .github/workflows/deploy.yml
git commit -m "ci: 添加自动部署"
git push
|
这次 push 本身就会触发首次部署(因为 workflow 监听 push)。
7.2 查看执行结果
打开仓库的 Actions 标签页(github.com/<用户>/<仓库>/actions):
1
2
3
| 🟢 绿色 ✓ → 成功!以后每次 push 自动部署
🔴 红色 ✗ → 失败,点进去看哪一步报错
🟡 黄色 → 正在运行
|
点开失败的 run,能看到每一步的日志,定位问题。
7.3 手动触发
配置了 workflow_dispatch 后,在 Actions 页面:
- 左侧选 Deploy workflow
- 右侧点 Run workflow → 选 main 分支 → Run
适合改了 secrets 或服务器配置后,不 push 代码也能重新部署。
八、常见问题
8.1 hugo: command not found
最常见的坑。 SSH 的 non-login shell 环境变量不全,可能找不到 hugo(尤其 snap 安装的)。
1
2
3
| # 在服务器上查 hugo 的绝对路径
which hugo
# 可能输出:/usr/local/bin/hugo 或 /snap/bin/hugo
|
然后把 workflow 里的 hugo 改成绝对路径:
1
2
3
4
5
| script: |
set -e
cd /var/www/blog/site
git pull --ff-only
/usr/local/bin/hugo --minify --cleanDestinationDir -d /var/www/blog/public
|
8.2 git pull 失败
1
2
3
4
5
6
7
8
9
10
11
12
| 原因1:路径不对
cd 的目录不是 git 仓库(没有 .git)
→ 确认服务器上仓库的真实路径
原因2:服务器仓库有本地改动
--ff-only 会拒绝产生 merge
→ 服务器上别手动改文件,所有改动走 git push
原因3:仓库是 private
服务器 git pull 需要认证
→ 给服务器配 deploy key,或用 https + token
(public 仓库无此问题)
|
8.3 SSH 连接失败
1
2
3
4
5
6
7
8
9
10
| Permission denied (publickey)
→ 私钥没配对,检查 SERVER_SSH_KEY 内容是否完整
→ 公钥是否加到 authorized_keys
Connection timeout
→ 防火墙/安全组没放行 SSH 端口
→ 或 SERVER_HOST 填错
端口非 22
→ 加 SERVER_PORT secret,workflow 取消 port 行注释
|
8.4 部署成功但页面没更新
1
2
3
4
5
6
7
8
| 可能1:浏览器缓存
→ 强制刷新(Ctrl+F5)或无痕模式
可能2:CDN 缓存
→ 清 CDN 缓存,或等缓存过期
可能3:hugo 构建输出了,但 nginx root 指向的目录不对
→ 确认 -d 的目录 = nginx server 块的 root
|
九、安全注意事项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 1. 用专用密钥,不共用
给 GitHub Actions 单独一对密钥
和你日常 SSH 的 key 分开
万一泄露,只影响部署,且能单独撤销
2. 私钥只进 Secrets,绝不进代码
.gitignore 不用管(Secrets 根本不在仓库里)
别手滑把私钥贴到代码或日志
3. 最小权限(进阶)
本文用了 root,简单但权限过大
更安全的做法:建专用 deploy 用户,只给仓库和 nginx 目录的权限
不能 sudo,限制 SSH 只能执行特定命令
4. 密钥可随时撤销
怀疑泄露 → 服务器删掉 authorized_keys 里那行公钥即可
重新生成一对,更新 GitHub Secret
5. 限制来源 IP(进阶)
服务器防火墙只允许 GitHub IP 段 SSH(但 GitHub IP 会变)
或用部署密钥 + 强制命令
|
十、进阶优化(可选)
跑通基础版后,可以按需增强:
10.1 部署失败通知
GitHub Actions 默认会在失败时发邮件给仓库 owner。想更及时:
1
2
3
4
5
6
7
8
9
10
11
| jobs:
deploy:
steps:
# ... 部署步骤 ...
- name: 失败通知
if: failure()
run: |
# 调用钉钉/飞书/企业微信 webhook
curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=XXX" \
-H "Content-Type: application/json" \
-d '{"msgtype":"text","text":{"content":"博客部署失败!"}}'
|
10.2 多环境部署
1
2
3
4
| main 分支 → 生产服务器
dev 分支 → 测试服务器
用分支触发不同 job,或用 environment 区分 secrets
|
10.3 加上构建产物校验
1
2
3
4
5
6
7
8
| script: |
set -e
cd /var/www/blog/site
git pull --ff-only
hugo --minify --cleanDestinationDir -d /var/www/blog/public
# 校验关键页面是否生成
test -f /var/www/blog/public/index.html || { echo "构建失败"; exit 1; }
echo "✅ 部署完成 $(date '+%F %T')"
|
十一、小结
本文记录了用 GitHub Actions + SSH 自动部署 Hugo 博客的完整流程:
- 方案选型:对比 4 种方案,方案1(Actions SSH 执行)最贴合手动 SSH 部署的现状
- 原理:runs-on 是 GitHub 的跑腿虚拟机,真正的部署在你的服务器上发生
- 密钥:生成专用 ed25519 密钥对,公钥留服务器,私钥进 GitHub Secrets
- Secrets:SERVER_HOST / SERVER_USER / SERVER_SSH_KEY(端口非 22 加 SERVER_PORT)
- workflow:用 appleboy/ssh-action,执行 git pull + hugo,三个关键参数(-d / –minify / –cleanDestinationDir)
- 触发:push 自动触发 + workflow_dispatch 手动触发
- 排坑:hugo PATH(最常见)、git pull 失败、SSH 连接、页面缓存
- 安全:专用密钥、最小权限、可撤销
配好之后,写博客的流程简化为:
1
2
3
| hugo new posts/xxx.md # 写文章
hugo server # 本地预览
git add && git commit && git push # 发布,自动部署
|
从此告别 SSH 手动操作,专注写内容。这正是静态博客 + CI/CD 的理想工作流。