内网部署面板如何接收 GitHub 的自动部署通知

2542 字
13 分钟
内网部署面板如何接收 GitHub 的自动部署通知

面板只监听内网 IP,又不想把管理界面暴露出去——怎么让 GitHub 在 push 之后触发一次部署。

结论先放前面:只放行一个需要 Bearer token 的 POST 端点就够了。四个坑全是「配置都对,但被更早生效的东西拦住了」。

环境是 NAS 上的 Coolify + Cloudflare Tunnel。

一、先问清楚值不值得做#

在动手之前值得停一下。自动部署省下的是「构建完成后手动点一次 Deploy」这个动作,代价是:

  • 一条常驻的公网入口
  • 一套需要长期维护正确性的规则(隧道路由、WAF、token 有效期)
  • 每个仓库两个 Secret

如果你的应用大多是直接在面板粘贴 compose、一周才改几次,手动点更省心。本文的前提是:你已经有多个走 Git 仓库部署的应用,且改动频繁到手动部署成为负担。

面板本身的用法(网络别名、健康检查、compose 写法)见在 NAS 上用 Coolify 部署的十条约定。

二、三种方案#

方案原理代价
A. 隧道只放行部署接口给面板域名加一条隧道路由,Path 限定为部署 API,其余路径不可达一个端点暴露在公网,需维护隧道与 WAF 规则
B. 自托管 runnerGitHub Actions 跑在你自己的机器上,直接访问内网要维护 runner,占用 NAS 资源
C. 轮询NAS 上定时查镜像仓库的 digest,变了就调本地 API零暴露,但有延迟,且要自己写脚本

为什么走 A#

本文走 A。它的关键在于暴露面可以做得很小:只有一个需要 Bearer token 的 POST 端点,管理界面在公网上根本不存在。

如果对暴露面有更严格的要求,C 是彻底的答案——全程 NAS 主动出站,不需要任何入口。

三、动手:六步搭通#

第一步 拿到部署接口的地址#

Coolify 每个应用的 Webhooks 页面有一个 Deploy webhook URL,形如:

http://<面板地址>/api/v1/deploy?uuid=<应用uuid>&force=false

记下这个 uuid,后面要用。

第二步 建 API Token#

面板 → Keys & Tokens → 新建:

  • 权限只勾 Deploy,这是最小权限
  • 有效期设成最长或 Never

有效期这一项容易踩:默认 30 天,到期后 Actions 会静默失败——curl 返回 401,但没人会去看每次构建的日志。

第三步 本地先验证接口#

在内网直接测,排除面板侧的问题:

Terminal window
curl -s -X POST "http://<内网IP>:8000/api/v1/deploy?uuid=<uuid>&force=true" \
-H "Authorization: Bearer <token>"

成功返回:

{"deployments":[{"message":"Service xxx started. It could take a while, be patient.","resource_uuid":"..."}]}

两个容易撞上的响应:

  • {"message":"This endpoint has changed to a POST request."} — 漏了 -X POST
  • {"message":"Unauthenticated."} — 没带 Authorization 头,或 token 错

第四步 隧道只放行这一个路径#

Cloudflare Zero Trust → Networks → Tunnels → 选中隧道 → Public Hostname,新增一条:

字段值
Subdomaincoolify
Domain你的域名
Pathapi/v1/deploy
Servicehttp://<内网IP>:8000

这里有本文第一个坑,见第四节。

配好后验证两件事:

Terminal window
# 管理界面应该打不开
curl -I https://coolify.example.com/
# 期望:404
# 部署接口应该能通
curl -s -X POST "https://coolify.example.com/api/v1/deploy?uuid=<uuid>&force=true" \
-H "Authorization: Bearer <token>"
# 期望:Service xxx started

第五步 在构建 workflow 末尾加一步#

关键点:不要写成独立的 workflow 用 push 触发。那样部署会和镜像构建并行,Coolify 拉到的还是旧镜像。

正确做法是加在构建 workflow 的最后一个 job 末尾:

# 镜像 manifest 合成完毕后,通知面板拉新镜像重新部署。
# if 条件与上面 latest tag 的 enable 条件保持一致:
# 只有真正刷新了 :latest 的构建才触发部署,工作分支的镜像不动 latest。
- name: Trigger deploy
if: github.ref_type == 'tag' || github.ref == 'refs/heads/main'
run: |
response=$(curl -s -w "\n%{http_code}" -X POST \
"https://coolify.example.com/api/v1/deploy?uuid=${{ secrets.COOLIFY_UUID }}&force=true" \
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}")
body=$(echo "$response" | head -n -1)
code=$(echo "$response" | tail -n 1)
echo "HTTP $code"
echo "$body"
if [ "$code" != "200" ]; then
echo "::error::部署触发失败"
exit 1
fi
echo "::notice::部署已排队"

force=true 是必需的:镜像 tag 固定为 latest 时,不强制的话面板会认为配置没变化而跳过部署。

第六步 建 Secret#

仓库 Settings → Secrets and variables → Actions:

  • COOLIFY_TOKEN — 所有仓库可共用,也可以建在组织级
  • COOLIFY_UUID — 每个仓库对应的应用不同,各自建

六步做完,push 一次就该自动部署了。 下面四个坑是我实际撞上的,链路不通时对着找。

四、坑一:隧道 ingress 的匹配顺序#

隧道进来之后那一段(Traefik 路由、证书、中间件)单独写过一篇网关 HTTPS 的分层排查。

配好路径规则后,两条 curl 全部返回 404,而且是 404 page not found 这种纯文本——这是 cloudflared 自己返回的,说明请求到了隧道但没匹配到任何规则。

看 cloudflared 的日志能直接定位:

Terminal window
docker logs <cloudflared容器> --tail 40

日志里会打印当前的 ingress 配置:

{"ingress":[
{"hostname":"*.example.com", "service":"http://traefik:8888"},
{"hostname":"coolify.example.com", "path":"api/v1/deploy", "service":"http://192.168.x.x:8000"},
{"service":"http_status:404"}
]}

问题一目了然:通配符规则排在前面。cloudflared 的 ingress 是按顺序匹配、第一条命中即止,coolify.example.com 先被 *.example.com 吃掉,转给了反向代理,而代理上没有这个域名的路由,于是 404。

修法:把具体规则拖到通配符之前#

在 Public Hostname 列表里调整顺序:

{"ingress":[
{"hostname":"coolify.example.com", "path":"api/v1/deploy", "service":"http://192.168.x.x:8000"},
{"hostname":"*.example.com", "service":"http://traefik:8888"},
{"service":"http_status:404"}
]}

这条规则要记住:凡是新增具体 hostname,都必须排在通配符之前。

五、坑二:workflow_run 只在默认分支生效#

最初的方案是写一个独立的 deploy.yml,用 workflow_run 监听构建 workflow 的完成事件:

on:
workflow_run:
workflows: ["Build and Push Docker Image"]
types: [completed]

文件路径正确、语法正确、push 成功,但 Actions 页面左侧的 workflow 列表里始终不出现这个 workflow,手动触发的按钮也无从点起。

原因是 GitHub Actions 的一条硬性规则:workflow_run 和 schedule 类型的触发器,只有当 workflow 文件存在于仓库的默认分支时才生效。

如果你的开发分支不是默认分支(比如长期在一个功能分支上迭代),这个 workflow 永远不会被注册。

两条出路#

  1. 把文件合并到默认分支
  2. 放弃独立 workflow,把触发步骤并进构建 workflow 的最后一步

选 2 更好,除了绕开这个限制,还顺带解决了时序问题——步骤在同一个 job 里顺序执行,构建完成才轮到部署,不需要任何跨 workflow 的协调。

六、坑三:Cloudflare 的机器人挑战拦住了 GitHub runner#

链路打通后,本地 curl 能成功,GitHub Actions 却返回 403,响应体是一整页 HTML:

<title>Just a moment...</title>
...
<span id="challenge-error-text">Enable JavaScript and cookies to continue</span>

这是 Cloudflare 的 Managed Challenge。GitHub runner 的 IP 被判定为自动化流量,要求执行 JS 验证——curl 当然做不到。

本地能通、CI 不能通,这个差异本身就指向了来源 IP 的判定。

WAF 自定义规则管不到它#

第一反应是加 WAF 自定义规则跳过:

(http.host eq "coolify.example.com" and starts_with(http.request.uri.path, "/api/v1/deploy"))

Action 选 Skip,把所有 WAF 组件都勾上。但改完仍然 403。

原因在响应里的 cType: 'managed' 和一个已知行为:免费版的 Bot Fight Mode 执行在 WAF 自定义规则之前,Skip 管不到它。

解法是直接关掉 Bot Fight Mode:Security → Settings → Bot Fight 模式 → 关闭。

关掉它安全吗#

取决于你的服务构成。如果域名下的服务要么套了统一登录、要么只在内网可达、要么本身就是公开的,那么 Bot Fight Mode 提供的保护本就有限。

它拦的是「看起来像脚本」的请求:真正的恶意扫描器会伪装 UA 绕过,而老实的自动化工具(比如你的 CI)反倒被拦。

关掉之后,DDoS 防护、托管规则集、浏览器完整性检查、自定义规则都还在。

那条 WAF 自定义规则不用删——Bot Fight Mode 只是拦截来源之一,这条规则继续为这个路径跳过其余组件。

七、坑四:Secret 没建,变量为空#

关掉 Bot Fight Mode 后,403 变成了:

HTTP 404
{"message":"No resources found."}

这是好消息——请求终于打到了面板,是面板说找不到资源。

原因是 COOLIFY_UUID 这个 Secret 还没建。GitHub Actions 对未定义的 secret 不报错,直接替换成空字符串,URL 变成 uuid=&force=true,面板自然找不到对应的应用。

八、故障速查:靠响应码认位置#

这条链路上每一环的失败都有独特的响应特征,不用猜:

响应卡在哪
404 page not found(纯文本)隧道没匹配到规则
403 + HTML 挑战页Cloudflare 拦截
401 Unauthenticatedtoken 错或没传
404 {"message":"No resources found."}uuid 错或为空
200 + Service xxx started成功

九、验证整条链路#

Secret 建好后重跑,逐层确认:

1. Actions 日志

HTTP 200
{"deployments":[{"message":"Service xxx started. It could take a while, be patient.","resource_uuid":"..."}]}

注意这只说明触发成功,部署本身还在跑。

2. 面板

应用页 → Deployments,最上面一条的状态从 In progress 变成 Finished。

3. 容器

Terminal window
docker ps --format '{{.Names}}\t{{.Status}}' | grep <应用名>

Up 时间重新从零计时,且最终是 (healthy),说明容器确实被重建了。

4. 打开网站

这次改的东西生效了没有——最终验收只能靠这一步。

十、小结#

整条链路的形态:

push → 构建镜像并推到 registry → 同一 job 的最后一步 curl 部署接口
→ Cloudflare 边缘(WAF 规则放行)→ 隧道(路径匹配)
→ 内网面板 → 拉新镜像 → 容器重建

四个坑归纳起来是同一类问题:配置本身都是对的,但被某个「更早生效的东西」拦在了前面。

通配符 ingress 排在具体规则之前、Bot Fight Mode 执行在 WAF 之前、workflow_run 的注册发生在默认分支上、空 Secret 在请求发出前就被替换掉了。

排查这类问题,比起反复检查配置本身,更有效的是沿着请求路径逐段验证,看它到底走到了哪一步就停下了。每一环的失败都有自己的响应特征,认识这些特征比任何配置检查清单都快。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

内网部署面板如何接收 GitHub 的自动部署通知
https://blog.always1ov.com/posts/coolify-github-auto-deploy/
作者
薯饼
发布于
2026-09-14
许可协议
CC BY-NC-SA 4.0