Hugo 博客自动部署实战:GitHub Actions + SSH

写在前面

每次写完博客,都要 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.4jiwei.space
SERVER_USERSSH 用户名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 页面:

  1. 左侧选 Deploy workflow
  2. 右侧点 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 的理想工作流。