返回博客列表

博客自动化部署实战:GitHub、Gitee Webhook 与 Nginx

2026年4月8日
9 分钟阅读
技术

前言

博客上线之后,每次改完代码都要手动构建、手动上传、手动登录服务器替换文件。流程不复杂,但真的很烦。

我真正想要的是下面这种体验:

  1. 本地改完代码
  2. git push
  3. 服务器自动拉代码
  4. 自动构建
  5. 网站自动更新

听起来很普通,但这次为了把它真正跑通,我从 GitHub ActionsSelf-hosted RunnerWebhook 一路折腾到 Gitee 镜像仓库,几乎把所有坑都踩了一遍。

这篇文章记录的不是“理论上可以”,而是最后真的在国内云服务器上稳定跑起来的方案

为什么最后没用 GitHub Actions

一开始我尝试了最常见的两种做法:

方案优点缺点
GitHub Actions + SSH 部署配置简单,生态成熟云端机器外网 SSH 进入服务器,会触发安全告警
GitHub Actions + Self-hosted Runner所有任务都在自己服务器执行下载 Runner、下载 Node、拉代码都很慢
GitHub Webhook + 服务器脚本足够轻量,没有 Runner 依赖如果服务器直接拉 GitHub,依旧可能卡住
GitHub + Gitee 双仓库 + Gitee WebhookGitHub 保留主仓库,服务器从 Gitee 拉代码更快需要额外维护一个镜像仓库

最后真正落地的是第四种:GitHub 保留主仓库,Gitee 作为国内同步源和部署触发源,服务器只从 Gitee 拉代码

原因很现实:

  • GitHub Actions 云端机器访问服务器会触发安全告警。
  • Self-hosted Runner 在国内服务器上拉 GitHub 资源也不稳定。
  • 私有仓库直接 git pull GitHub 时,速度和稳定性都不理想。
  • Gitee 在国内访问更快,更适合做部署源。

最终架构

本地开发
   ↓
git push origin main
   ↓
同时推送到 GitHub / Gitee
   ↓
Gitee Push Webhook
   ↓
Nginx 反代到本机 webhook 服务
   ↓
deploy.sh
   ↓
git pull origin main(此时 origin 指向 Gitee)
   ↓
npm ci --registry https://registry.npmmirror.com
   ↓
npx vite build
   ↓
rsync 到 /var/www/myblog
   ↓
网站自动更新

这里有两个关键点:

  • GitHub 继续作为主仓库,方便代码托管、备份、以后继续接别的自动化。
  • 服务器只从 Gitee 拉代码,尽量避开 GitHub 国际线路带来的延迟和超时问题。

第一步:Nginx 配置

我的博客是 Vue 3 + Vite 构建的纯前端 SPA,路由使用的是 createWebHistory()。这意味着 Nginx 不仅要能托管静态资源,还必须把非真实文件路径回退到 index.html

同时,Webhook 请求也需要由 Nginx 转发到本机的 Node 服务。

server {
    listen 80;
    server_name jaysblog.fun www.jaysblog.fun;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name jaysblog.fun www.jaysblog.fun;

    root /var/www/myblog;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/jaysblog.fun/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jaysblog.fun/privkey.pem;

    location /assets/ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    location /images/ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    location /webhook {
        proxy_pass http://127.0.0.1:9000/webhook;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Hub-Signature-256 $http_x_hub_signature_256;
        proxy_set_header X-Gitee-Token $http_x_gitee_token;
        proxy_set_header X-Gitee-Signature $http_x_gitee_signature;
        proxy_set_header X-Gitee-Event $http_x_gitee_event;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}

这里最容易漏掉两件事:

  1. try_files $uri $uri/ /index.html; 是 SPA 正常刷新的关键,少了这行,文章详情页一刷新就会 404。
  2. Webhook 相关请求头最好显式透传,不然签名校验很容易失败。

第二步:部署脚本

我在服务器上保留了一份源码目录:/var/www/myblog-source。每次收到 Webhook,就执行一个部署脚本:

#!/bin/bash
set -e

PROJECT_DIR="/var/www/myblog-source"
DEPLOY_DIR="/var/www/myblog"
LOG_FILE="/var/log/myblog-deploy.log"

echo "========================================" >> "$LOG_FILE"
echo "[$(date)] Deploy triggered" >> "$LOG_FILE"

cd "$PROJECT_DIR"

git pull origin main >> "$LOG_FILE" 2>&1
npm ci --registry https://registry.npmmirror.com >> "$LOG_FILE" 2>&1
npx vite build >> "$LOG_FILE" 2>&1

rsync -avz --delete --exclude='/images/' dist/ "$DEPLOY_DIR/" >> "$LOG_FILE" 2>&1
rsync -avz src/content/images/ "$DEPLOY_DIR/images/" >> "$LOG_FILE" 2>&1

echo "[$(date)] Deploy finished successfully" >> "$LOG_FILE"

这段脚本的思路非常直接:

  • git pull:拉取最新代码
  • npm ci:安装依赖,保证环境干净
  • npx vite build:构建站点
  • rsync:把构建产物和图片同步到站点目录

几个关键细节:

  • set -e:任何一步失败就立即停止,避免半成品上线。
  • 镜像源必须换成 npmmirror:这是从“卡几分钟”变成“几秒安装完”的关键。
  • distimages 分开同步:页面资源和图片资源职责不同,拆开处理更稳。
  • 服务器上的 origin 指向 Gitee:部署时不直接依赖 GitHub。

为了让 git pull 走 Gitee,我把源码仓库的远程地址改成了 Gitee:

cd /var/www/myblog-source
git remote set-url origin https://gitee.com/你的用户名/myBlog.git
git config --global credential.helper store
git pull origin main

如果仓库是私有的,第一次拉取时输入 Gitee 用户名和私人令牌,之后凭据就会缓存下来。

第三步:Webhook 服务

为了尽量轻量,我没有用 Express,而是直接用 Node.js 自带的 http 模块写了一个监听服务。

这里还有一个真实踩坑:因为项目 package.json 里有 "type": "module",所以这个脚本不能再叫 webhook-server.js,否则用了 require 之后会直接报:

ReferenceError: require is not defined in ES module scope

所以最终文件名改成了:webhook-server.cjs

核心逻辑如下:

const http = require('node:http')
const crypto = require('node:crypto')
const { execFile } = require('node:child_process')

const PORT = 9000
const SECRET = process.env.WEBHOOK_SECRET || 'myblog-deploy-secret'
const DEPLOY_SCRIPT = '/var/www/myblog-source/scripts/deploy.sh'

let deploying = false

function verifyGitHub(payload, signature) {
  if (!signature) return false
  const hmac = crypto.createHmac('sha256', SECRET)
  hmac.update(payload)
  const expected = 'sha256=' + hmac.digest('hex')
  try {
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  } catch {
    return false
  }
}

function verifyGitee(headers, body) {
  const token = headers['x-gitee-token']
  if (token && token === SECRET) return true

  const signature = headers['x-gitee-signature']
  if (!signature) return false

  const hmac = crypto.createHmac('sha256', SECRET)
  hmac.update(body)
  return signature === hmac.digest('base64')
}

const server = http.createServer((req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhook') {
    res.writeHead(404)
    res.end('Not Found')
    return
  }

  let body = ''
  req.on('data', chunk => { body += chunk })
  req.on('end', () => {
    const isGitHub = !!req.headers['x-hub-signature-256']
    const isGitee = !!req.headers['x-gitee-event']

    if (isGitHub && !verifyGitHub(body, req.headers['x-hub-signature-256'])) {
      res.writeHead(403)
      res.end('Forbidden')
      return
    }

    if (isGitee && !verifyGitee(req.headers, body)) {
      res.writeHead(403)
      res.end('Forbidden')
      return
    }

    const payload = JSON.parse(body)
    const ref = payload.ref || ''
    if (ref && ref !== 'refs/heads/main') {
      res.writeHead(200)
      res.end('Skipped: not main branch')
      return
    }

    if (deploying) {
      res.writeHead(200)
      res.end('Deploy already in progress')
      return
    }

    deploying = true
    res.writeHead(200)
    res.end('Deploy started')

    execFile('bash', [DEPLOY_SCRIPT], () => {
      deploying = false
    })
  })
})

server.listen(PORT, () => {
  console.log(`Webhook server listening on port ${PORT}`)
})

这个服务做了几件事:

  • 同时支持 GitHub 和 Gitee 的请求校验
  • 只接收 main 分支的 Push
  • 如果当前已经在部署,就拒绝新的重复触发
  • 只负责“验签 + 触发脚本”,逻辑尽量简单

然后用 pm2 托管,保证进程常驻和开机自启:

WEBHOOK_SECRET="myblog-deploy-secret" pm2 start scripts/webhook-server.cjs --name myblog-webhook
pm2 save
pm2 startup

第四步:Gitee Webhook

真正负责触发部署的是 Gitee 仓库的 Push Webhook

配置内容非常直接:

  • URL:https://jaysblog.fun/webhook
  • 密码:myblog-deploy-secret
  • 事件:只勾 Push

保存之后,Gitee 会发一个测试请求。只要服务器能收到并返回成功,整条链路就算通了。

第五步:本地如何同时维护 GitHub 和 Gitee

我不想放弃 GitHub,因为它仍然是主仓库、代码备份和以后扩展自动化的基础。但部署又确实更适合走 Gitee。

所以比较舒服的做法是:一次 push,同时推 GitHub 和 Gitee

git remote set-url --add --push origin https://github.com/你的用户名/myBlog.git
git remote set-url --add --push origin https://gitee.com/你的用户名/myBlog.git

这样以后只需要:

git push origin main

GitHub 和 Gitee 都会收到最新代码,Gitee 负责触发部署。

如果你不想配置双推,也可以继续把 Gitee 当镜像仓库,手动同步后再让 Gitee 发 Webhook。

这次踩过的坑

1. GitHub Actions 版本号并不是随便写的

一开始配置 ssh-deploy 的时候,直接写了 @v5,结果 Actions 直接报错找不到版本。后来才发现它的可用版本是明确标签,比如 v5.1.0

这件事虽然不大,但很典型:自动化的坑往往不是“不会写代码”,而是工具细节没对上。

2. Self-hosted Runner 理论很好,实践很慢

把 Runner 装到自己的服务器上,确实可以避免外网 SSH 告警。但问题是:

  • 下载 Runner 包慢
  • 下载 Node 慢
  • checkout 私有仓库也慢
  • 构建前准备阶段比真正构建还花时间

最终算下来,它对我的场景并不划算。

3. 服务器直接拉 GitHub 依然可能卡住

即使不用 Actions,改成 Webhook 后服务器自己执行 git pull,只要远程仓库还是 GitHub,国内网络抖动时一样会卡住。

这也是我最后把部署源切到 Gitee 的根本原因。

4. type: module 会影响运维脚本

这次一个很典型的坑就是:主项目用了 ESM,不代表所有脚本都要跟着用 ESM。

像这种偏运维、偏脚本化的 Node 文件,如果你懒得改成 import 语法,直接改成 .cjs 往往更省事。

5. npm 源不换,部署速度会非常难受

服务器默认从国外 npm 源下载依赖,在国内经常慢到不可接受。

解决方式:

npm config set registry https://registry.npmmirror.com

或者在部署脚本里显式写:

npm ci --registry https://registry.npmmirror.com

6. SPA 一定要处理路由回退

只要你前端用了 createWebHistory(),Nginx 就必须处理:

location / {
    try_files $uri $uri/ /index.html;
}

否则首页没问题,文章页一刷新就会 404。

7. 私有仓库认证也是部署链路的一部分

私有仓库不是“配好一次就结束”,而是部署过程中每一步都要考虑认证问题:

  • GitHub 需要个人令牌
  • Gitee 需要私人令牌
  • 服务器要能稳定保存凭据

否则自动化最后会死在最基础的 git pull 上。

最终效果

现在这套流程终于变成了我想要的样子:

  1. 本地改代码
  2. git push origin main
  3. GitHub 和 Gitee 同时收到代码
  4. Gitee Push Webhook 通知服务器
  5. 服务器自动拉代码、构建、部署
  6. 网站更新

如果要排查问题,我通常看两个地方:

tail -f /var/log/myblog-deploy.log
pm2 logs myblog-webhook

一个看部署脚本跑到了哪一步,一个看 Webhook 服务有没有收到请求。

回头看,这套方案并不算“最优雅”,但它足够稳定、足够直接,也真的适合我现在这台国内云服务器。

对个人博客来说,能稳定跑起来,很多时候比“技术上更标准”更重要。

评论 留下你的痕迹