背景
服务器是 2H2G 的轻量机,构建 Next.js 会直接爆内存,所以构建放在 GitHub Actions 上,产物打成 zip 推到服务器。之前用 Innei 大佬写的 SSH + SCP 传输,但每次部署都得开 22 端口,要么一直开着吃扫描,要么每次手动开关,都不爽。
服务器上 Nginx 已经在跑了,443 本来就开着。那直接在 443 上加一个接收产物的路由,GitHub Actions 用 curl POST 过来就行了。
服务器端
Webhook 服务
一个原生 Node.js HTTP 服务,监听 127.0.0.1:9000,不装任何依赖。核心就三步:校验 secret、流式写入 zip、执行部署。
流式写入是关键。第一版把 zip 整个读进内存,47MB 直接超 PM2 的内存限制,进程被杀,Nginx 返回 502。改成 req.pipe(fs.createWriteStream()) 之后内存占用不到 10MB。
const fileStream = fs.createWriteStream(zipPath);
req.pipe(fileStream);
fileStream.on("finish", () => {
deploy(zipPath, runNumber);
});
部署逻辑和原来 SSH 脚本里的一样:解压 → 检测 monorepo/flat 结构 → 软链接 .env 和 server.js → pm2 reload。每次部署放进 ~/shiro/<run_number>/,回滚脚本不用改。
Nginx
在 API 域名的配置里加一个 location:
location = /deploy {
client_max_body_size 200m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_pass http://127.0.0.1:9000/deploy;
proxy_set_header X-Deploy-Secret $http_x_deploy_secret;
proxy_set_header X-Run-Number $http_x_run_number;
proxy_request_buffering off;
}
proxy_request_buffering off 不能少,不然 Nginx 会先把整个 zip 缓冲到磁盘再转发。
GitHub Actions 端
原来的 appleboy/scp-action + appleboy/ssh-action 整个换成一行 curl:
- name: Deploy via webhook
run: |
response=$(curl -s -w "\n%{http_code}" \
--max-time 300 --retry 3 --retry-delay 10 \
-X POST "${{ vars.DEPLOY_URL }}" \
-H "X-Deploy-Secret: ${{ secrets.DEPLOY_SECRET }}" \
-H "X-Run-Number: ${{ github.run_number }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @release.zip)
http_code=$(echo "$response" | tail -1)
echo "$response" | sed '$d'
[ "$http_code" = "200" ] || exit 1
不需要 SSH 密钥,不需要 ssh-keyscan,不需要 known_hosts。DEPLOY_URL 放 Variables,DEPLOY_SECRET 放 Secrets。
上游自动同步
Shiro 上游在持续更新,我的仓库基于上游有一些定制。手动定期合并容易忘,写个 sync.yml 自动化:
on:
schedule:
- cron: '17 2 * * *'
workflow_dispatch:
每天定时 checkout 源码仓库,git fetch upstream main,如果有新提交就 git merge --no-edit,推送后通过 repository_dispatch 触发构建部署。
合并冲突时直接报错退出,不做自动解决。-X theirs 会静默覆盖我的定制代码,对有自己修改的 fork 来说不可接受。冲突时 GitHub 会发邮件通知,手动处理就好,绝大多数情况下不会冲突。
踩过的坑
502 的真正原因不是 Nginx 配置,是 PM2 的 max_memory_restart 默认 64M 太小。Webhook 服务在接收 zip 时内存到了 70MB,PM2 杀进程,Nginx 拿到断连。改成流式写入 + 把限制提到 256M 解决。另外 pm2 restart 不会重新读取 ecosystem 配置,必须 pm2 delete + pm2 start。
首次 sync 莫名冲突,报 taze.config.js 的 add/add 冲突,但我刚手动合并过,上游最新提交也没改这个文件。加了诊断日志后第二次运行完全正常。原因大概是第一次触发时 Actions checkout 到了旧版本,git 找到的合并基点更早,把双方各自存在的同名文件判定为独立新增。时序问题,不是逻辑问题。
完整链路
全程不需要开 22 端口,443 是 Nginx 本来就在用的。