关于 cloudflare-tunnel-publish
一个通过 Cloudflare Tunnel 将本地 HTTP 服务发布到自定义域名的工作流技能,无需公网 IP 或开放入站端口。它引导初学者准备域名和令牌,并自动配置隧道、DNS、启动与验证。
主要功能
- 01将本地端口映射到公开域名
- 02配置 Cloudflare Tunnel 和 DNS
- 03复用健康隧道或创建新隧道
- 04启动并验证公开 URL
- 05支持多个子域名共用隧道
适用场景
- 01发布本地运行的 Web 服务
- 02公开 Starchild 预览
- 03为多个子域名配置同一隧道
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ starchild-ai-agent/ official-skills --skill cloudflare-tunnel-publish安装后在 Claude Code 中输入/cloudflare-tunnel-publish确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
scripts/cf_api.pycheck_status.pykeepalive.shrun_tunnel.shsetup.pyteardown.pyverify.py详细介绍
这个技能的作用
使用 Cloudflare Tunnel,将运行在本地端口上的服务(默认为 Starchild 预览,但适用于任何 HTTP 服务)变成外界可访问的服务,网址为 app.userdomain.com。无需公网 IP,无需开放入站端口,且提供免费 SSL。
流程中的两个角色:
- 用户手动完成(必须手动操作,无法自动完成):创建 Cloudflare 账户、在 Cloudflare 上购买或转移域名、创建 API Token。
- Agent 自动完成(此技能):验证 Token、选择区域、创建隧道、配置入口规则、创建 DNS、安装并启动
cloudflared,验证公网 URL 是否可用。
面向的用户
请将用户视为初学者。他们可能从未使用过 Cloudflare。每次只引导他们完成一个微步骤,等待确认后再继续。不要把完整的 10 步计划一次性发给用户后就不管了。
工作流
阶段 0 — 说明流程(1 条消息)
用通俗的语言告诉用户接下来会分 4 个阶段:
- 他们注册 Cloudflare 账户并添加域名(手动操作,约 5 分钟)
- 他们创建 API Token 并安全地交给你(手动操作,约 2 分钟)
- 你创建隧道和 DNS 并启动服务(自动操作,约 1 分钟)
- 你们一起测试 URL(自动操作)
询问:“你已经在 Cloudflare 上有域名了吗?还是需要从头开始?” 根据回答进入相应流程。
阶段 1 — 在 Cloudflare 上获取域名
如果用户还没有域名:
- 引导他们前往 https://dash.cloudflare.com/sign-up 注册
- 然后前往 https://dash.cloudflare.com/?to=/:account/domains 购买域名(Cloudflare 以注册局成本价销售
.com / .net / .org / .io / .dev / .app等域名),或者添加现有域名并更改名称服务器 - 等待他们确认“域名已在 Cloudflare 中激活”后再继续
可以告诉初学者:“当你的 Cloudflare 控制面板中显示域名状态为 Active 时,就可以继续了。”
如果用户已经有域名:跳到阶段 2。
阶段 2 — 创建 API Token
将此确切链接发给他们(该链接会在可能的情况下预填正确的权限,用户也可以手动创建):
https://dash.cloudflare.com/profile/api-tokens → Create Token → Create Custom Token
必需权限(请告诉他们添加以下三项):
- Account → Cloudflare Tunnel → Edit
- Zone → DNS → Edit
- Zone → Zone → Read
Account Resources:选择他们的账户。Zone Resources:选择 Include All zones(或指定该域名)。TTL:保留默认值。
他们点击 Continue to summary → Create Token 后,Cloudflare 会仅显示一次 Token。请告诉他们:不要把 Token 粘贴到聊天中。
阶段 3 — 安全接收 Token
调用 request_env_input,并提供:
env_vars=[{"key": "CLOUDFLARE_API_TOKEN", "label": "Cloudflare API Token", "required": true}]
reason="Used to create the tunnel and DNS record on your domain. Stored locally in workspace/.env, never echoed in chat."
等待用户通过安全弹窗提交。如果他们没有立即提交,不要反复重试,只需等待。
阶段 4 — 验证 Token 并选择区域
运行 python3 skills/cloudflare-tunnel-publish/scripts/verify.py。该脚本会显示:
- Token 是否有效
- 用户的 account_id(保存到
workspace/.cf_state.json) - 账户中的所有区域(域名)
如果有多个区域,询问用户要使用哪个域名。将 zone_id 和 zone_name 保存到状态文件中。
阶段 5 — 决定要发布的内容
向用户询问两项信息:
- 子域名(例如
app、demo、www)→ 最终主机名将为<sub>.<zone_name>。也允许使用根域名(@)。 - 本地端口(例如
8080、3000)。如果他们说“我的 Starchild 预览”,运行cat /data/previews.json 2>/dev/null,查找现有预览的端口;否则明确询问端口。
默认服务 URL:http://localhost:<port>。
阶段 6 — 创建隧道(自动化)
运行 python3 skills/cloudflare-tunnel-publish/scripts/setup.py --hostname <full_hostname> --port <port>。
默认复用隧道。 如果账户中已有正常运行的隧道,setup.py 会将新主机名添加到该隧道的 ingress,并将 DNS CNAME 指向它,而不是创建单独的隧道。一个隧道 = 一个 cloudflared 进程 = 一个保活监控程序。每个站点都创建新隧道,会导致你需要看管 N 个进程,也有 N 种可能在不知情的情况下失联(这正是促使我们修复此问题的那个 bug:第二个隧道的 cloudflared 进程退出后,无人监控,因为保活监控程序只知道第一个隧道)。
仅在有理由隔离流量时才传入 --new-tunnel(例如,某个站点的流量极高,可能会耗尽共享的边缘连接)。
脚本按以下顺序执行:
- 如果存在正常运行的隧道,则复用它(如果不存在,则创建名为
starchild-<hostname>的隧道;也可使用--new-tunnel强制创建新隧道) - 获取隧道的 运行令牌(用于启动
cloudflared的长 base64 字符串) - PUT ingress 配置:将
<hostname>→http://localhost:<port>合并到现有 ingress 规则中(保留其他主机名),并以404作为回退 - 创建 CNAME DNS 记录:
<hostname>→<tunnel_id>.cfargotunnel.com,已启用代理 - 将站点添加到
workspace/.cf_state.json中的sites数组(支持多个站点,见下方“多个站点”)
如果存在同名隧道,则复用它,而不是报错。
阶段 7 — 启动 cloudflared
运行 bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh——这是启动站点的标准方式。keepalive.sh 是统一的启动与修复程序(见下方“保持运行”):它会读取 .cf_state.json,启动应用(如果你记录了 --app-cmd)和隧道,并验证公开 URL。run_tunnel.sh 是更底层的仅启动隧道的启动器,由 keepalive.sh 调用——如果 workspace/bin/ 中没有 cloudflared,它会下载该程序,从 .cf_state.json 中读取 run_token,并运行 cloudflared tunnel run。
告知用户站点已上线。为了确保稳定运行,你还会将同一个 keepalive.sh 配置为开机启动并按计划运行——不要另行手写启动脚本。
阶段 8 — 验证
⚠️ 不要直接使用容器的 curl https://<hostname>——容器的解析器会缓存新域名的旧 NXDOMAIN 结果,给出错误信息。请始终通过 DoH 验证:
curl -sS "https://dns.google/resolve?name=<hostname>&type=A" | python3 -m json.tool
可能有以下三种结果:
Status: 0且Answer中有 IP → 已上线。现在运行curl -I https://<hostname>应返回 200/301/302。向用户展示其 URL。🎉Status: 3(NXDOMAIN)且 Authority = TLD 注册局 NS(例如ns.trs-dns.com)→ TLD 注册局尚未传播新域名。 告诉用户:配置已 100% 完成,请等待 30–60 分钟(新注册的域名可能需要最多 24 小时),然后重试。不要持续轮询——让他们在自己的设备上检查。- 隧道日志显示错误(检查
bash_process(action='log', session_id=...))→ 存在实际配置错误。常见原因:ingress 未指向正确端口、本地服务未运行、CNAME 目标错误。
决策规则
-
用户说“我的服务在自己的笔记本电脑上,不在 Starchild 中” → 流程完全相同,但第 7 阶段必须在他们的笔记本电脑上运行,而不是在此容器中。提供适用于其操作系统的对应安装命令:
- macOS:
brew install cloudflared && cloudflared tunnel run --token <TOKEN> - Linux/Windows:链接到 https://github.com/cloudflare/cloudflared/releases/latest
如果需要,通过
request_env_input发送 run_token;或者只打印一次,并告诉他们复制该令牌(可以安全地分享给自己的设备,但绝不要粘贴回聊天中)。
- macOS:
-
用户想要多个子域名 → 重用同一条隧道;PUT 一个列出所有主机名的新 ingress 配置;为每个主机名创建一个 CNAME。这现在是默认行为——只需为每个子域名运行
setup.py,它就会合并到现有隧道的 ingress 中。 -
用户想移除它 → 运行
python3 skills/cloudflare-tunnel-publish/scripts/teardown.py(删除 DNS 和隧道,并终止本地 cloudflared 进程)。
注意事项(⚠️ 均已在实际运行中确认)
Token / API
Cloudflare-Tunnel:Edit不会授予/accounts列表权限。 即使使用有效令牌,调用GET /accounts也会返回空列表。解决方案: 从任意 zone 内嵌的account.id字段中获取account_id——verify.py已经这样做了。不要仅为了解决此问题而添加Account:Account Settings:Read;使用 zone 的方法更简单。GET /accounts/{id}/cfd_tunnel/{tunnel_id}/token返回的“run token”是cloudflared tunnel run --token使用的令牌。不要与以下内容混淆:- tunnel secret — 仅与旧版本地管理的隧道有关(我们不使用)
- API Token — 用于调用 api.cloudflare.com
隧道 / Ingress
- CNAME 的目标必须是
<tunnel_id>.cfargotunnel.com,而不是隧道名称。 - 远程管理的隧道(
config_src: "cloudflare")通过 API 配置端点进行路由,而不是使用本地config.yml。不要生成该文件。 - 即使
config_src=cloudflare,通过 API 创建隧道也需要tunnel_secret字段(32 个随机字节,经过 base64 编码)。setup.py会自动生成。
Universal SSL 证书签发延迟 — 另一个大陷阱
DNS 传播后,用户仍可能在浏览器中遇到 ERR_SSL_VERSION_OR_CIPHER_MISMATCH。这不是 bug——Cloudflare 尚未为新主机名签发 Universal SSL 证书。
诊断(在容器中运行——无需身份验证):
echo | timeout 10 openssl s_client -connect <hostname>:443 -servername <hostname> 2>&1 | grep -E "(handshake|peer certificate|Cipher is)"
no peer certificate available+handshake failure→ 证书尚未签发 ⏳- 返回真实证书 → 正常运行 ✅
时间:
- 已有先前证书的现有 zones:通常少于 5 min
- 全新域名:15 min ~ 24 h(DNS 验证 + CA 签名 + 边缘节点传播)
告知用户:
- 打开
dash.cloudflare.com → <domain> → SSL/TLS → Edge Certificates - 找到类似
*.<domain>, <domain>的行并查看状态:Active→ 已完成,刷新浏览器Pending Validation/Initializing→ 请等待
- 确认
SSL/TLS → Overview → Encryption mode设置为 Full(而不是 Flexible 或 Full Strict)。隧道始终通过 HTTPS 连接到源站,因此 Full 是合适的设置。
不要: 告诉用户添加 Advanced Certificate($$$)或更改 DNS——这两种方法都无济于事。只需等待。
DNS 传播 — 大陷阱
新注册的域名需要 30 min ~ 2 h(有时长达 24 h)才能在全球 TLD 注册表中传播完成,即使 Cloudflare 仪表板立即显示“Active”也是如此。症状:
dig @1.1.1.1 yourdomain.com NS返回 NXDOMAIN(Status=3)- Authority 部分显示 TLD 注册表的 NS(例如,经由 Tucows 注册的
.fun域名显示ns.trs-dns.com),而不是 Cloudflare 的 NS cloudflared隧道已连接且运行正常,但https://yourdomain.com返回 DNS 解析失败
这不是该 skill 的 bug——而是 TLD 注册表同步延迟。 使用下面的诊断片段来区分这种情况与实际问题。告诉用户:“配置已完成。请等待 30–60 分钟后重试。我们这边无需再做任何操作。”
容器 DNS — 误报
在 Starchild 容器内部进行测试时,容器的本地解析器可能数小时都看不到新域名。务必使用公共 DoH 进行交叉验证:
curl -sS "https://dns.google/resolve?name=hello.example.com&type=A" | python3 -m json.tool
Status: 0,且Answer数组包含 IP → 正常 ✅Status: 3(NXDOMAIN),且Authority: ns.trs-dns.com(或类似的注册局 NS)→ TLD 传播仍在等待中 ⏳Status: 0,但没有Answer→ CNAME 已存在,但 Cloudflare 橙云尚未开始路由 → 等待 30 秒
保持服务运行 — 一个脚本,两个触发器
这是 Agent 容易出错的部分。“发布”很容易;让隧道站点持续运行数周才是真正的任务。Starchild 容器会在没有警告的情况下重启(平台更新、OOM、迁移、用户重启),而 cloudflared 也会在运行期间自行退出(网络短暂中断、边缘节点重置、QUIC 故障),但容器仍在运行。无论哪种情况,都会导致 https://yourdomain.com 返回 502 / 521 / 530 / 1033,直到进程被重启。DNS 和 Cloudflare 侧的隧道配置会保留(它们存储在 Cloudflare 服务器上)——只需重新启动本地进程。
设计:一个幂等的恢复核心(scripts/keepalive.sh),由两个触发器调用。 不要为每个项目编写单独的启动或修复脚本——这样会导致两份代码逐渐不一致。keepalive.sh 随此技能提供,且是通用脚本:它从 .cf_state.json 读取 hostname、port、app_cmd、app_dir,因此同一个文件可用于任何域名。调用它的 Agent 不需要编写任何项目专用的 shell 脚本。
keepalive.sh 每次运行时会:
- 检查公共 URL(检查可访问性,而不只是 PID——
cloudflared进程可能仍在运行,却已断开连接)。 - 状态正常 → 记录一行日志,静默退出。
- 服务不可用 → 排查原因:本地应用端口未打开 → 重启应用(通过
app_cmd)和隧道;只有隧道已退出 → 仅重启隧道。随后通过几次重试重新验证(覆盖冷启动预热时间)。 - 仅在状态转换时报告(记录于
run/keepalive.state):刚恢复或刚发生故障时输出一行;状态稳定时(正常,或已知不可用)静默。因此,定时任务只在有情况需要通知时发送信号,不会刷屏。
第 1 步 — 记录如何启动应用(在设置时)
向 setup.py 传入 --app-cmd / --app-dir,以便 keepalive 能重启应用,而不只是隧道:
python3 setup.py --hostname app.example.com --port 8765 \
--app-cmd "python3 server.py" --app-dir projects/myapp
如果省略 --app-cmd,keepalive 只会守护隧道,无法恢复已崩溃的应用。除非应用由其他服务进行监控,否则务必记录此参数。
第 2 步 — 启动站点
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh
幂等:启动所有已停止的服务,状态正常时不执行任何操作。
第 3 步 — 在容器重启后继续运行(启动触发器)
将 keepalive 添加到 workspace/setup.sh(容器每次启动时都会运行):
# Bring the tunnel site back after a restart. keepalive.sh only READS
# .cf_state.json — no Cloudflare API call, no token needed at boot.
if [ -f /data/workspace/.cf_state.json ]; then
bash /data/workspace/skills/cloudflare-tunnel-publish/scripts/keepalive.sh &
disown
fi
🚫 绝不要将
setup.py放入setup.sh。setup.py用于配置时运行:它会调用 Cloudflare API,可能轮换 run_token,并覆盖.cf_state.json。每次启动都运行它不仅浪费资源,还可能触及速率限制;如果 API 令牌已被移除,运行会失败,也可能更改原本正常的配置。启动时只能读取状态——这正是keepalive.sh的作用。名称“setup”容易让人想把它放进“setup.sh”;请克制这种冲动。
第 4 步 — 在运行过程中进程退出后继续运行(看门狗触发器)
将同一个脚本安排为低开销的 command 模式任务。一个定时任务即可守护所有站点——keepalive.sh 会在一次运行中遍历 .cf_state.json 中的所有站点,因此无需为每个站点单独设置看门狗:
scheduled_task(action="schedule",
schedule="every 2 minutes",
command="cd /data/workspace && bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh",
title="cloudflare tunnel keepalive (all sites)")
- 使用相对命令,并以
cd /data/workspace &&开头。绝对路径/data/workspace/...可能会被调度器规范化为不存在的/data/skills/...(workspace/这一段会被删掉),导致每次运行都静默失败。使用cd加相对路径即可避免此问题。(已在实际运行中确认。) - 保留默认的
deliver值,这样仅在状态转换时发出的提醒才能实际送达用户。keepalive 在状态正常时本来就不会发出消息,因此无需抑制刷屏——真正发生故障时应当通知你。 - 时间间隔可能会被规范化(例如,“每 2 分钟”→ 3 分钟)——这没问题。
验证持久性(在声称“稳定”之前,以下事项都要完成)
# 1. start + idempotency
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh # brings up
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh # silent no-op
tail -5 logs/keepalive.log # ok https://...
# 2. boot wired
grep keepalive setup.sh
# 3. watchdog registered + command stored correctly (relative path!)
# scheduled_task(action="list")
不要只编辑 setup.sh 就声称“长期稳定”。 这只能应对重启,无法应对运行期间的进程退出。确认启动触发器和定时触发器都指向
keepalive.sh,并且首次运行记录了 ok https://...。
端口冲突会导致无声故障。 其他工作区项目可能已经占用了常见端口(8000/8080/8765)。如果应用的 bind() 因 Address already in use 失败,就会退出,但 curl localhost:<port> 仍会返回 200——响应来自别人的应用。请使用较高且项目专属的端口,并验证页面内容确实是你的(curl https://yourdomain.com | head),而不只是返回 200。
明确告知用户:
🔔 你的站点运行在容器中。容器可能因更新/内存/迁移而随时重启,隧道进程偶尔也可能自行掉线(域名变成 502/521/530/1033)。我已经把一个自愈脚本写入
workspace/setup.sh(开机自动启动),并设置了每隔几分钟一次的巡检(掉线时自动重启,仅在恢复/失败时通知你)。两层都指向同一个脚本,你基本不用管。真打不开时,让我看一眼logs/keepalive.log和logs/cloudflared.log就能定位。
计划限制
- 免费计划足够了。 无需推销升级。
- 免费计划只公开代理 80/443 端口——这对我们无关紧要,因为隧道始终会向全网开放 443;
localhost:<port>可以是任意端口。
状态文件格式
workspace/.cf_state.json 支持多个站点。规范存储位置是
sites 数组;顶层的平铺字段为了向后兼容而保留,并且始终反映最后配置的站点:
{
"account_id": "...",
"zone_id": "...",
"zone_name": "example.com",
"sites": [
{
"hostname": "app.example.com",
"port": 8080,
"tunnel_id": "...",
"tunnel_name": "starchild-app-example-com",
"run_token": "...",
"app_cmd": "python3 server.py",
"app_dir": "/data/workspace/projects/myapp"
},
{
"hostname": "blog.example.com",
"port": 3000,
"tunnel_id": "...",
"run_token": "...",
"app_cmd": "",
"app_dir": ""
}
],
"hostname": "blog.example.com",
"port": 3000,
"tunnel_id": "...",
"run_token": "...",
"app_cmd": "",
"app_dir": ""
}
keepalive.sh 会读取 sites[],并在一次巡检中守护每个站点——一个看门狗,所有主机名。这解决了“每个站点都有自己的隧道
- 自己的 keepalive,但只有一个受到监控”的问题:现在恰好只有一个 keepalive 进程负责所有已配置的站点。
多站点——设计方案
一条隧道、多个主机名、一个看门狗。 这是唯一合理的拓扑结构:
setup.py默认复用在账户中找到的第一个健康隧道。它会将新主机名合并到该隧道现有的 ingress 规则中(保留其他主机名),并将 DNS CNAME 指向该隧道。.cf_state.json包含一个sites[]数组。每次运行setup.py都会追加(或替换)一个条目。teardown.py会移除一个条目。keepalive.sh会遍历sites[]并守护每个站点。一个进程、一个看门狗、所有主机名。如果某个站点与另一个站点共用隧道,同一个cloudflared进程就会为两者提供服务——keepalive 不会再启动第二个进程。
Agent 弄错时的情况(促成本节的那个错误):
每个站点都创建一条新隧道,就意味着有 N 个 cloudflared 进程、N 个 PID 文件、N 个需要配置的看门狗——而实际上最终只会设置一个看门狗。其他隧道会悄无声息地停止运行,几周都没人发现。复用隧道,将条目追加到 sites[],让一个 keepalive 守护所有站点。