Skip to content
huazaiki's blog
Go back

上手配置一台新的服务器(五):配置日常更新工作流

Updated:
Edit page

上一篇文章中我们已经把博客跑起来了。但这里留下一个问题:服务器是 2 核 2G 的轻量云,pnpm build 跑一次接近一分钟,CPU 直接打满。每次写完文章还要 SSH 上去 git pull && pnpm build,太笨重了。

这一篇搭建一套自动化流程:本地写完文章 git push,GitHub Actions 在云端构建,构建完成后 Webhook 通知服务器自动拉取。之后更新博客只需要一条命令。

思路

服务器只负责「展示」,不负责「构建」。把构建拆到 GitHub Actions 上——免费、配置高、速度快的 CI 机器,构建完产物推到独立的 build 分支,服务器定时或者被 Webhook 触发去拉这个分支。

整体链路:

git push main


GitHub Actions: checkout → install → build → push dist/ 到 build 分支


GitHub Webhook 发 POST 到服务器


服务器 webhook 服务:校验 HMAC → git pull → Nginx 自动生效

改造 GitHub Actions

编写一个新的 deploy.yml ,用于构建博客静态内容后推送到 build 分支。

# .github/workflows/deploy.yml
name: Build and Push to build branch

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: write

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: "11.3.0"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"
          cache: "pnpm"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm run build

      - name: Push dist to build branch
        run: |
          cd dist
          git init
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add -A
          git commit -m "Build: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" || exit 0
          git push --force \
            "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" \
            HEAD:build

关键点:

服务器 Webhook 配置

接下来让服务器感知到 build 分支的更新。方案是用 webhook 这个轻量工具——GitHub 收到 push 后向服务器发一个带签名的 POST 请求,服务器校验后执行 git pull

安装 webhook

sudo apt install -y webhook

配置 hooks.json

sudo mkdir -p /etc/webhook
sudo vim /etc/webhook/hooks.json
[
  {
    "id": "blog-deploy",
    "execute-command": "/etc/webhook/deploy.sh",        <---- 触发脚本
    "command-working-directory": "/var/www/blog/dist",  <---- 目标文件夹
    "trigger-rule": {
      "match": {
        "type": "payload-hmac-sha256",
        "secret": "替换成你自己的随机密钥",
        "parameter": {
          "source": "header",
          "name": "X-Hub-Signature-256"
        }
      }
    }
  }
]

HMAC 签名校验保证只有 GitHub(持有同一个 secret)才能触发脚本。生成密钥:

openssl rand -hex 32

deploy.sh

这是 webhook 触发后实际执行的脚本。逻辑很简单:

  1. 写日志:每次触发都记录时间戳到 /var/log/webhook-deploy.log,方便事后排查「这次 webhook 到底有没有执行、什么时候执行的」。
  2. cd 到工作目录:失败(目录不存在、权限不足)直接退出并记录 FAIL,不继续往下走。
  3. git fetch + git reset --hard:之所以不用 git pull,是因为 GitHub Actions 对 build 分支是 force push,本地和远端历史完全不重合,git pull 会报冲突。fetch 拿到最新远端状态后 reset --hardorigin/build,干净利落地覆盖本地所有文件。
  4. stderr 重定向到日志2>> "$LOG_FILE" 把 git 命令的错误输出也写进日志,不然 webhook 静默执行,失败了你根本不知道原因。
sudo tee /etc/webhook/deploy.sh <<'SCRIPT'
#!/bin/bash

LOG_FILE="/var/log/webhook-deploy.log"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy triggered" >> "$LOG_FILE"

cd /var/www/blog/dist || {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] FAIL: cannot cd" >> "$LOG_FILE"
  exit 1
}

git fetch origin build 2>> "$LOG_FILE" || {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] FAIL: git fetch failed" >> "$LOG_FILE"
  exit 1
}

git reset --hard origin/build 2>> "$LOG_FILE"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy done" >> "$LOG_FILE"
SCRIPT

sudo chmod +x /etc/webhook/deploy.sh

systemd 服务

webhook 需要以守护进程方式一直在后台运行,用 systemd 管理最合适。几个关键配置:

sudo tee /etc/systemd/system/webhook.service <<'SYSTEMD'
[Unit]
Description=Webhook server
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/webhook -hooks /etc/webhook/hooks.json -port 9000 -verbose
Restart=always
User=<your_username>  # <---- 修改成自己的用户名

[Install]
WantedBy=multi-user.target
SYSTEMD

sudo systemctl daemon-reload
sudo systemctl enable --now webhook

daemon-reload 让 systemd 识别新写的 service 文件,enable --now 设置开机自启并立即启动。

Nginx 反向代理

blog.conf 的 443 server 块中加一个 location,把 /hooks/ 代理到 webhook:

location /hooks/ {
    proxy_pass http://127.0.0.1:9000/hooks/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

重载 Nginx:

sudo nginx -t && sudo systemctl reload nginx

GitHub Webhook

去 GitHub 仓库 Settings → Webhooks → Add webhook:

字段
Payload URLhttps://huazaiki.com/hooks/blog-deploy
Content typeapplication/json
Secretopenssl rand -hex 32 生成的那个
EventsJust the push event

添加后 GitHub 发一个 ping 测试连通性,显示绿勾就算通了。

踩坑记录

实际配置中踩了几个坑,记下来免得下次再掉进去。

#!/bin/bash 前面不能有空格。tee 写脚本时如果 heredoc 缩进了,可能导致第一行变成 #!/bin/bash。内核看到不是以 #! 开头,报 fork/exec: exec format error。用 xxd /etc/webhook/deploy.sh | head -1 确认第一字节是 23#)而不是 20(空格)。

webhook 的 trigger-rule 只对带有 HMAC 签名的 POST 请求放行。 在浏览器访问 https://huazaiki.com/hooks/blog-deploy 会看到 “Hook rules were not satisfied”——这是正常的安全行为,不是 bug。只有 GitHub 用你的 secret 签名的请求才能触发。

webhook 用哪个用户运行。 一开始我照着教程把 User 设成了 www-data,结果 webhook 触发后 git fetch 因为权限不足静默失败——仓库是我自己的用户 clone 的,www-data 读不了。后来直接把 service 文件里的 User 改成自己的用户名(比如 blogadmin),就绕过了权限问题。比 chown -R www-data:www-data /var/www/blog 更直接,也不用担心以后自己手动进去 git pull 调试时又把文件所有者改回去。

日常使用

配置完成后的日常流程:

  1. 在本地机器上编写文章

  2. git 推送,剩下的全自动

git add . && git commit -m "新文章" && git push origin main

1-2 分钟后访问博客,文章已经在上面了。不需要 SSH,不需要等服务器编译,不需要担心 CPU 飙到 100%。一次配置,长期省心。


Edit page

Previous Post
自定义 SSH 登陆服务器之后的提示信息(MOTD)
Next Post
上手配置一台新的服务器(四):部署博客与 HTTPS