cloudflare-tunnel-publish
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhat this skill does
此技能的功能
Turns a service running on a local port (default: a Starchild preview, but works for any HTTP service) into something the world can reach at , using Cloudflare Tunnel. No public IP required, no inbound ports opened, free SSL.
app.userdomain.comTwo roles in the flow:
- User does manually (must, can't be automated): create Cloudflare account, buy/transfer domain to Cloudflare, create API Token.
- Agent does automatically (this skill): verify token, pick zone, create tunnel, configure ingress, create DNS, install + start , verify the public URL works.
cloudflared
借助Cloudflare Tunnel,将运行在本地端口的服务(默认是Starchild预览服务,但适用于任何HTTP服务)转换为可通过访问的公开服务。无需公网IP,无需开放入站端口,免费提供SSL证书。
app.userdomain.com流程中的两个角色:
- 用户手动操作(必须,无法自动化):创建Cloudflare账户,购买/转移域名到Cloudflare,创建API Token。
- Agent自动操作(此技能负责):验证Token,选择区域,创建隧道,配置入口规则,创建DNS记录,安装并启动,验证公开URL可正常访问。
cloudflared
Audience assumption
受众假设
Treat the user as a beginner. They may have never used Cloudflare. Walk them through one micro-step at a time, wait for confirmation, then move on. Do NOT dump the whole 10-step plan and disappear.
将用户视为初学者。他们可能从未使用过Cloudflare。请逐步引导他们完成每个微小步骤,等待确认后再继续。不要一次性抛出完整的10步计划后就结束对话。
Workflow
工作流程
Phase 0 — Set the stage (1 message)
阶段0 — 说明流程(1条消息)
Tell the user in plain language what's about to happen, in 4 phases:
- They register a Cloudflare account + add a domain (manual, ~5 min)
- They create an API Token and give it to you securely (manual, ~2 min)
- You build the tunnel + DNS + start it (automatic, ~1 min)
- You test the URL together (automatic)
Ask: "Do you already have a domain on Cloudflare, or do we need to start from scratch?" Branch on the answer.
用通俗易懂的语言告诉用户即将进行的4个阶段:
- 他们注册Cloudflare账户并添加域名(手动操作,约5分钟)
- 他们创建API Token并安全地提供给你(手动操作,约2分钟)
- 你构建隧道、配置DNS并启动服务(自动操作,约1分钟)
- 你们一起测试URL(自动操作)
询问:"你已经在Cloudflare上拥有域名了吗?还是我们需要从头开始?" 根据回答分支处理。
Phase 1 — Get the user a domain on Cloudflare
阶段1 — 帮助用户在Cloudflare上获取域名
If they don't have one yet:
- Direct them to https://dash.cloudflare.com/sign-up to register
- Then https://dash.cloudflare.com/?to=/:account/domains to buy a domain (Cloudflare sells etc. at registry cost), OR add an existing domain and change nameservers
.com / .net / .org / .io / .dev / .app - Wait for them to confirm "domain is active in Cloudflare" before proceeding
Beginner hint to share: "When the domain shows status Active in your Cloudflare dashboard, we're good to continue."
If they already have one: skip to Phase 2.
如果用户还没有域名:
- 引导他们访问https://dash.cloudflare.com/sign-up进行注册
- 然后访问https://dash.cloudflare.com/?to=/:account/domains购买域名(Cloudflare以注册成本价售卖`.com / .net / .org / .io / .dev / .app`等域名),或者添加现有域名并更改域名服务器
- 等待用户确认"域名已在Cloudflare中激活"后再继续
可分享给初学者的提示:"当你的Cloudflare控制台中域名状态显示为Active时,我们就可以继续了。"
如果用户已有域名:直接跳至阶段2。
Phase 2 — Create the API Token
阶段2 — 创建API Token
Send them this exact link (it pre-fills the right permissions when possible, and the user can also build it manually):
https://dash.cloudflare.com/profile/api-tokens → Create Token → Create Custom Token
Required permissions (tell them to add these three):
- Account → Cloudflare Tunnel → Edit
- Zone → DNS → Edit
- Zone → Zone → Read
Account Resources: their account. Zone Resources: Include All zones (or specifically the domain). TTL: leave default.
After they click Continue to summary → Create Token → Cloudflare shows the token once. Tell them: do not paste it in chat.
发送以下精确链接(它会预填充正确的权限,用户也可以手动配置):
https://dash.cloudflare.com/profile/api-tokens → Create Token → Create Custom Token
所需权限(告诉用户添加以下三项):
- Account → Cloudflare Tunnel → Edit
- Zone → DNS → Edit
- Zone → Zone → Read
账户资源:用户的账户。区域资源:Include All zones(或指定对应的域名)。TTL:保留默认值。
用户点击Continue to summary → Create Token后,Cloudflare会仅显示一次该Token。告诉用户:不要在聊天框中粘贴此Token。
Phase 3 — Receive the token securely
阶段3 — 安全接收Token
Call with:
request_env_inputenv_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."Wait for the user to submit it via the secure popup. Do not retry-loop if they don't submit immediately — just wait.
调用,参数如下:
request_env_inputenv_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."等待用户通过安全弹窗提交Token。如果用户未立即提交,不要循环重试 — 只需等待即可。
Phase 4 — Verify token + pick the zone
阶段4 — 验证Token并选择区域
Run . It prints:
python3 skills/cloudflare-tunnel-publish/scripts/verify.py- Token validity
- The user's account_id (saves to )
workspace/.cf_state.json - All zones (domains) on the account
If multiple zones, ask the user which domain to use. Save and to state.
zone_idzone_name运行。它会输出:
python3 skills/cloudflare-tunnel-publish/scripts/verify.py- Token有效性
- 用户的account_id(保存至)
workspace/.cf_state.json - 账户下的所有区域(域名)
如果存在多个区域,询问用户使用哪个域名。将和保存到状态文件中。
zone_idzone_namePhase 5 — Decide what to publish
阶段5 — 确定要发布的内容
Ask the user two things:
- Subdomain (e.g., ,
app,demo) → final hostname will bewww. Apex domain (<sub>.<zone_name>) is also allowed.@ - Local port (e.g., ,
8080). If they say "my Starchild preview", run3000to look up an existing preview's port; otherwise ask explicitly.cat /data/previews.json 2>/dev/null
Default service URL: .
http://localhost:<port>询问用户两件事:
- 子域名(例如、
app、demo)→ 最终主机名将是www。也允许使用 apex 域名(<sub>.<zone_name>)。@ - 本地端口(例如、
8080)。如果用户说“我的Starchild预览服务”,运行3000查找现有预览服务的端口;否则直接询问具体端口。cat /data/previews.json 2>/dev/null
默认服务URL:。
http://localhost:<port>Phase 6 — Build the tunnel (automated)
阶段6 — 构建隧道(自动化)
Run .
python3 skills/cloudflare-tunnel-publish/scripts/setup.py --hostname <full_hostname> --port <port>Tunnel reuse is the default. If a healthy tunnel already exists on the
account, adds the new hostname to that tunnel's ingress and points
the DNS CNAME at it — instead of creating a separate tunnel. One tunnel =
one process = one keepalive watchdog. Creating a new tunnel per
site is how you end up with N processes to babysit and N ways to silently go
dark (the exact bug that prompted this: a second tunnel's died
and nothing was watching it, because the watchdog only knew about the first).
setup.pycloudflaredcloudflaredPass only when you have a reason to isolate traffic (e.g. very
high volume on one site that would saturate the shared edge connections).
--new-tunnelThe script does, in order:
- Reuse a healthy tunnel if one exists (or create one named if none, or use
starchild-<hostname>to force a new one)--new-tunnel - Fetch the tunnel run token (a long base64 string used to start )
cloudflared - PUT the ingress configuration: merge →
<hostname>into the existing ingress rules (preserving other hostnames), fallbackhttp://localhost:<port>404 - Create a CNAME DNS record: →
<hostname>, proxied = true<tunnel_id>.cfargotunnel.com - Append the site to the array in
sites(multi-site support — see "Multiple sites" below)workspace/.cf_state.json
If a tunnel with the same name exists, reuse it instead of erroring.
运行。
python3 skills/cloudflare-tunnel-publish/scripts/setup.py --hostname <full_hostname> --port <port>默认复用隧道。如果账户中已存在健康的隧道,会将新主机名添加到该隧道的入口规则中,并将DNS CNAME指向该隧道 — 而不是创建单独的隧道。一个隧道对应一个进程和一个保活监控。为每个站点创建新隧道会导致需要维护N个进程,并且存在N种静默故障的可能(促使开发此技能的正是这样的bug:第二个隧道的进程崩溃,但监控只关注第一个隧道,因此无人察觉)。
setup.pycloudflaredcloudflared仅当有理由隔离流量时才使用参数(例如某个站点流量极高,会占用共享边缘连接的全部带宽)。
--new-tunnel脚本按以下顺序执行:
- 如果存在健康隧道则复用(如果不存在则创建名为的隧道,或使用
starchild-<hostname>强制创建新隧道)--new-tunnel - 获取隧道运行Token(一个长base64字符串,用于启动)
cloudflared - 更新入口配置:将→
<hostname>合并到现有入口规则中(保留其他主机名),默认返回404http://localhost:<port> - 创建CNAME DNS记录:→
<hostname>,proxied = true<tunnel_id>.cfargotunnel.com - 将站点信息追加到的
workspace/.cf_state.json数组中(支持多站点 — 见下文“多站点”部分)sites
如果同名隧道已存在,则复用该隧道而非报错。
Phase 7 — Start cloudflared
阶段7 — 启动cloudflared
Run — this is the
canonical way to bring the site up. is the single start+heal
brain (see "Keeping it alive" below): it reads , starts the app
(if you recorded ) and the tunnel, and verifies the public URL.
is the lower-level tunnel-only launcher that
calls — it downloads to if missing, reads the
run_token from , and runs .
bash skills/cloudflare-tunnel-publish/scripts/keepalive.shkeepalive.sh.cf_state.json--app-cmdrun_tunnel.shkeepalive.shcloudflaredworkspace/bin/.cf_state.jsoncloudflared tunnel runTell the user the site is up. The SAME is what you'll wire into
boot + a schedule for durability — don't hand-roll a separate starter.
keepalive.sh运行 — 这是启动站点的标准方式。是唯一的启动+自愈脚本(见下文“保持服务运行”部分):它读取,启动应用(如果记录了)和隧道,并验证公开URL是否可访问。是调用的底层隧道启动器 — 如果中没有,它会自动下载,从读取运行Token,然后执行。
bash skills/cloudflare-tunnel-publish/scripts/keepalive.shkeepalive.sh.cf_state.json--app-cmdrun_tunnel.shkeepalive.shworkspace/bin/cloudflared.cf_state.jsoncloudflared tunnel run告知用户站点已上线。同一个将用于开机启动和定时任务以保证服务持久性 — 不要手动编写单独的启动脚本。
keepalive.shPhase 8 — Verify
阶段8 — 验证
⚠️ Do not use the container's directly — the container's resolver caches stale NXDOMAIN for new domains and will lie to you. Always verify via DoH:
curl https://<hostname>bash
curl -sS "https://dns.google/resolve?name=<hostname>&type=A" | python3 -m json.toolThree possible outcomes:
- + IPs in
Status: 0→ live. NowAnswershould return 200/301/302. Show the user their URL. 🎉curl -I https://<hostname> - (NXDOMAIN) + Authority = TLD registry NS (e.g.
Status: 3) → TLD registry hasn't propagated the new domain yet. Tell the user: configuration is 100% done, wait 30–60 min (newly registered domains can take up to 24 h), then retry. Don't keep polling — let them check on their own device.ns.trs-dns.com - Tunnel logs show errors (check ) → real config bug. Common culprits: ingress not pointing at the right port, local service not running, wrong CNAME target.
bash_process(action='log', session_id=...)
⚠️ 不要直接使用容器内的进行验证 — 容器的解析器会缓存新域名的陈旧NXDOMAIN记录,导致结果不准确。始终通过DoH进行验证:
curl https://<hostname>bash
curl -sS "https://dns.google/resolve?name=<hostname>&type=A" | python3 -m json.tool可能的三种结果:
- +
Status: 0中包含IP地址 → 服务已上线。此时Answer应返回200/301/302。将URL展示给用户。 🎉curl -I https://<hostname> - (NXDOMAIN) + Authority = TLD注册商NS(例如
Status: 3)→ TLD注册商尚未完成新域名的传播。告知用户:配置已100%完成,请等待30–60分钟(新注册的域名可能需要长达24小时),然后重试。不要持续轮询 — 让用户在自己的设备上检查。ns.trs-dns.com - 隧道日志显示错误(查看)→ 存在实际配置错误。常见原因:入口规则未指向正确端口、本地服务未运行、CNAME目标错误。
bash_process(action='log', session_id=...)
Decision rules
决策规则
-
User says "my service is on my laptop, not in Starchild" → exact same flow, but Phase 7 must run on their laptop, not in this container. Give them the equivalent install command for their OS:
- macOS:
brew install cloudflared && cloudflared tunnel run --token <TOKEN> - Linux/Windows: link to https://github.com/cloudflare/cloudflared/releases/latest
Send the run_token via if needed, or just print it once and tell them to copy it (it's safe to share with their own machine, but never paste back to chat).
request_env_input
- macOS:
-
User wants multiple subdomains → reuse the same tunnel; PUT a new ingress config that lists all hostnames; create one CNAME per hostname. This is now the default behavior — just runfor each subdomain and it will merge into the existing tunnel's ingress.
setup.py -
User wants to remove it → run(deletes DNS + tunnel + kills the local cloudflared process).
python3 skills/cloudflare-tunnel-publish/scripts/teardown.py
-
用户说“我的服务在我的笔记本电脑上,不在Starchild中” → 流程完全相同,但阶段7必须在用户的笔记本电脑上运行,而非此容器中。提供适用于其操作系统的等效安装命令:
- macOS:
brew install cloudflared && cloudflared tunnel run --token <TOKEN> - Linux/Windows: 链接至https://github.com/cloudflare/cloudflared/releases/latest
必要时通过发送运行Token,或仅打印一次并告知用户复制(在用户自己的设备上分享是安全的,但不要粘贴回聊天框)。
request_env_input
- macOS:
-
用户需要多个子域名 → 复用同一个隧道;更新入口配置以列出所有主机名;为每个主机名创建一个CNAME记录。这是当前默认行为 — 只需为每个子域名运行,它会合并到现有隧道的入口规则中。
setup.py -
用户希望移除服务 → 运行(删除DNS记录、隧道并终止本地cloudflared进程)。
python3 skills/cloudflare-tunnel-publish/scripts/teardown.py
Gotchas (⚠️ all confirmed in real runs)
注意事项(⚠️ 均在实际运行中已确认)
Token / API
Token / API
- does NOT grant
Cloudflare-Tunnel:Editlisting. Calling/accountsreturns an empty list even with a valid token. Solution: deriveGET /accountsfrom any zone's embeddedaccount_idfield —account.idalready does this. Do NOT addverify.pyjust to fix it; the zone trick is cleaner.Account:Account Settings:Read - The "run token" from is what
GET /accounts/{id}/cfd_tunnel/{tunnel_id}/tokenconsumes. Do not confuse with:cloudflared tunnel run --token- tunnel secret — only relevant for legacy locally-managed tunnels (we don't use)
- API Token — used to call api.cloudflare.com
- 权限不允许列出
Cloudflare-Tunnel:Edit。即使Token有效,调用/accounts仍会返回空列表。解决方案:从任意区域的内嵌GET /accounts字段推导account.id—account_id已实现此逻辑。不要为解决此问题添加verify.py权限;区域字段的方法更简洁。Account:Account Settings:Read - 返回的“运行Token”是
GET /accounts/{id}/cfd_tunnel/{tunnel_id}/token使用的Token。不要与以下混淆:cloudflared tunnel run --token- tunnel secret — 仅与旧版本地管理隧道相关(我们不使用)
- API Token — 用于调用api.cloudflare.com
Tunnel / Ingress
隧道 / 入口规则
- The CNAME target must be , NOT the tunnel name.
<tunnel_id>.cfargotunnel.com - Remotely-managed tunnel () routes via the API config endpoint, NOT a local
config_src: "cloudflare". Do not generate one.config.yml - Creating a tunnel via API requires a field (32 random bytes, base64) even for
tunnel_secret.config_src=cloudflaregenerates one automatically.setup.py
- CNAME目标必须是,而非隧道名称。
<tunnel_id>.cfargotunnel.com - 远程管理隧道()通过API配置端点路由,而非本地
config_src: "cloudflare"。不要生成该文件。config.yml - 通过API创建隧道时,即使,也需要
config_src=cloudflare字段(32个随机字节,base64编码)。tunnel_secret会自动生成该字段。setup.py
Universal SSL provisioning lag — the OTHER big trap
通用SSL证书颁发延迟 — 另一个重大陷阱
After DNS propagates, the user may still hit in the browser. This is NOT a bug — Cloudflare hasn't issued the Universal SSL certificate for the new hostname yet.
ERR_SSL_VERSION_OR_CIPHER_MISMATCHDiagnosis (run from container — no auth needed):
bash
echo | timeout 10 openssl s_client -connect <hostname>:443 -servername <hostname> 2>&1 | grep -E "(handshake|peer certificate|Cipher is)"- +
no peer certificate available→ cert not issued yet ⏳handshake failure - Real cert returned → working ✅
Timing:
- Established zones with prior certs: usually < 5 min
- Brand-new domains: 15 min ~ 24 h (DNS validation + CA signing + edge propagation)
What to tell the user:
- Open
dash.cloudflare.com → <domain> → SSL/TLS → Edge Certificates - Look for a row like and check status:
*.<domain>, <domain>- → done, refresh browser
Active - /
Pending Validation→ waitInitializing
- Confirm is Full (not Flexible, not Full Strict). Tunnel always carries HTTPS to the origin, so Full is the right match.
SSL/TLS → Overview → Encryption mode
Don't: Tell the user to add an Advanced Certificate ($$$) or to change DNS — neither helps. Just wait.
DNS传播完成后,用户在浏览器中仍可能遇到错误。这不是bug — Cloudflare尚未为新主机名颁发通用SSL证书。
ERR_SSL_VERSION_OR_CIPHER_MISMATCH诊断(从容器运行 — 无需授权):
bash
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 - 返回有效证书 → 服务正常 ✅
时间范围:
- 已有证书的成熟区域:通常<5分钟
- 全新域名:15分钟 ~ 24小时(DNS验证 + CA签名 + 边缘节点传播)
告知用户:
- 打开
dash.cloudflare.com → <domain> → SSL/TLS → Edge Certificates - 查找类似的条目并查看状态:
*.<domain>, <domain>- → 已完成,刷新浏览器
Active - /
Pending Validation→ 等待Initializing
- 确认为Full(不是Flexible,也不是Full Strict)。隧道始终通过HTTPS连接到源站,因此Full是正确的模式。
SSL/TLS → Overview → Encryption mode
不要做: 告知用户添加高级证书(付费)或更改DNS — 这两种方法都无效。只需等待即可。
DNS propagation — the big trap
DNS传播 — 重大陷阱
Newly registered domains take 30 min ~ 2 h (sometimes up to 24 h) to propagate across the global TLD registry, even when the Cloudflare dashboard shows "Active" instantly. Symptoms:
- returns NXDOMAIN (Status=3)
dig @1.1.1.1 yourdomain.com NS - The Authority section shows the TLD's registry NS (e.g., for
ns.trs-dns.comvia Tucows), NOT Cloudflare's NS.fun - tunnel is connected and healthy, but
cloudflaredreturns DNS resolution failurehttps://yourdomain.com
This is NOT a bug in the skill — it's TLD registry sync lag. Use the diagnostic snippet below to distinguish it from real issues. Tell the user: "Configuration is complete. Wait 30–60 minutes and try again. Nothing more to do on our side."
新注册的域名需要30分钟 ~ 2小时(有时长达24小时)才能在全球TLD注册商完成传播,即使Cloudflare控制台立即显示“Active”。症状:
- 返回NXDOMAIN(Status=3)
dig @1.1.1.1 yourdomain.com NS - Authority部分显示TLD注册商的NS(例如域名的
.fun,由Tucows提供),而非Cloudflare的NSns.trs-dns.com - 隧道已连接且健康,但
cloudflared返回DNS解析失败https://yourdomain.com
这不是技能的bug — 是TLD注册商同步延迟。 使用以下诊断代码片段区分实际问题。告知用户:“配置已完成。请等待30–60分钟后重试。我们这边无需再进行任何操作。”
Container DNS — false negative
容器DNS — 假阴性
When testing from inside the Starchild container, the container's local resolver may not see new domains for hours. Always cross-check with public DoH:
bash
curl -sS "https://dns.google/resolve?name=hello.example.com&type=A" | python3 -m json.tool- +
Status: 0array with IPs → working ✅Answer - (NXDOMAIN) +
Status: 3(or similar registry NS) → TLD propagation pending ⏳Authority: ns.trs-dns.com - but no
Status: 0→ CNAME exists but Cloudflare orange-cloud not yet routing → wait 30sAnswer
在Starchild容器内测试时,容器的本地解析器可能在数小时内无法识别新域名。始终通过公共DoH交叉验证:
bash
curl -sS "https://dns.google/resolve?name=hello.example.com&type=A" | python3 -m json.tool- +
Status: 0数组包含IP地址 → 服务正常 ✅Answer - (NXDOMAIN) +
Status: 3(或类似注册商NS)→ TLD传播待处理 ⏳Authority: ns.trs-dns.com - 但无
Status: 0→ CNAME已存在但Cloudflare橙色云尚未开始路由 → 等待30秒Answer
Keeping it alive — one script, two triggers
保持服务运行 — 一个脚本,两个触发方式
This is the part agents get wrong. "Publish" is easy; keeping a tunnel site up
for weeks is the real job. The Starchild container restarts without warning
(platform updates, OOM, migration, user reboot), and also dies
on its own mid-life (network blip, edge reset, QUIC failure) while the container
keeps running. Either one leaves returning
502 / 521 / 530 / 1033 until something restarts the processes. DNS and the
Cloudflare-side tunnel config survive (they live on Cloudflare's servers) — only
the local processes need relaunching.
cloudflaredhttps://yourdomain.comThe design: ONE idempotent recovery brain () called from
TWO triggers. Do not write per-project starter/healer scripts — that's how the
two copies drift apart. ships with this skill and is generic: it
reads , , , from , so the same
file works for any domain. The calling agent writes ZERO project-specific shell.
scripts/keepalive.shkeepalive.shhostnameportapp_cmdapp_dir.cf_state.jsonWhat does each run:
keepalive.sh- Probe the public URL (reachability, not just PID — a process can be alive but disconnected).
cloudflared - Healthy → log one line, exit silently.
- Down → diagnose: local app port closed → restart app (via ) and tunnel; only the tunnel dead → restart just the tunnel. Then re-verify with a few retries (covers cold-start warm-up).
app_cmd - Report on state transitions only (tracked in ): newly-recovered or newly-failed prints one line; steady-state (healthy, or already-known-down) is silent. So a scheduled task pushes signal, never spam.
run/keepalive.state
这是Agent容易出错的部分。“发布”很简单;让隧道站点持续运行数周才是真正的挑战。Starchild容器可能会无预警重启(平台更新、内存不足、迁移、用户重启),并且进程也可能在容器运行期间自行崩溃(网络中断、边缘节点重置、QUIC失败)。无论哪种情况,都会返回502 / 521 / 530 / 1033错误,直到进程被重启。DNS和Cloudflare端的隧道配置会保留(存储在Cloudflare服务器上) — 只需重启本地进程即可。
cloudflaredhttps://yourdomain.com设计思路:一个幂等的自愈脚本(),通过两个触发方式调用。 不要为每个项目编写单独的启动/自愈脚本 — 这会导致多个副本逐渐不一致。随此技能一起发布,具有通用性:它从读取、、、,因此同一个文件适用于任何域名。调用Agent无需编写任何项目特定的shell脚本。
scripts/keepalive.shkeepalive.sh.cf_state.jsonhostnameportapp_cmdapp_dir每次运行时会执行以下操作:
keepalive.sh- 探测公开URL的可达性(不仅仅是PID — 进程可能存活但已断开连接)。
cloudflared - 如果健康 → 记录一行日志,静默退出。
- 如果服务中断 → 诊断:本地应用端口关闭 → 重启应用(通过)和隧道;仅隧道崩溃 → 仅重启隧道。然后进行几次重试验证(覆盖冷启动预热时间)。
app_cmd - 仅报告状态转换(在中跟踪):服务恢复或新故障发生时打印一行日志;稳定状态(健康或已知故障)则静默。因此定时任务只会发送必要的通知,不会产生垃圾信息。
run/keepalive.state
Step 1 — record how to start the app (at setup time)
步骤1 — 记录应用启动方式(在设置阶段)
Pass / to so keepalive can restart the app,
not just the tunnel:
--app-cmd--app-dirsetup.pypython3 setup.py --hostname app.example.com --port 8765 \
--app-cmd "python3 server.py" --app-dir projects/myappIf you omit , keepalive guards the tunnel only and cannot revive a
crashed app. Always record it unless the app is supervised elsewhere.
--app-cmd向传递 / 参数,以便keepalive脚本可以重启应用,而不仅仅是隧道:
setup.py--app-cmd--app-dirpython3 setup.py --hostname app.example.com --port 8765 \
--app-cmd "python3 server.py" --app-dir projects/myapp如果省略,keepalive脚本只会监控隧道,无法恢复崩溃的应用。除非应用由其他进程管理,否则始终记录此参数。
--app-cmdStep 2 — start the site
步骤2 — 启动站点
bash skills/cloudflare-tunnel-publish/scripts/keepalive.shIdempotent: starts whatever is down, no-op when healthy.
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh幂等操作:启动所有已中断的服务,服务健康时无任何操作。
Step 3 — survive container restarts (boot trigger)
步骤3 — 应对容器重启(开机触发)
Add keepalive to (runs on every container boot):
workspace/setup.shbash
undefined将keepalive脚本添加到(每次容器启动时运行):
workspace/setup.shbash
undefinedBring the tunnel site back after a restart. keepalive.sh only READS
重启后恢复隧道站点。keepalive.sh仅读取
.cf_state.json — no Cloudflare API call, no token needed at boot.
.cf_state.json — 开机时无需调用Cloudflare API,也无需Token。
if [ -f /data/workspace/.cf_state.json ]; then
bash /data/workspace/skills/cloudflare-tunnel-publish/scripts/keepalive.sh &
disown
fi
> 🚫 **NEVER put `setup.py` in `setup.sh`.** `setup.py` is config-time: it calls
> the Cloudflare API, may rotate the run_token, and overwrites `.cf_state.json`.
> Running it on every boot is wasteful, can hit rate limits, breaks if the API
> token was removed, and can change a working config. Boot must only *read* state
> — that's exactly what `keepalive.sh` does. The name "setup" tempts you to put
> it in "setup.sh"; resist it.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,可能会轮换运行Token,并覆盖`.cf_state.json`。每次开机运行它会造成资源浪费,可能触发速率限制,如果API Token已移除则会失败,还可能更改正常运行的配置。开机时应仅*读取*状态 — 这正是`keepalive.sh`的作用。“setup”这个名称可能会诱使你将其放入“setup.sh”;请抵制这种诱惑。Step 4 — survive mid-life process death (watchdog trigger)
步骤4 — 应对进程中途崩溃(监控触发)
Schedule the SAME script as a cheap -mode task. One schedule
guards all sites — keepalive.sh iterates every site in
in a single pass, so you never need a per-site watchdog:
command.cf_state.jsonscheduled_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)")- Use a relative command with . An absolute
cd /data/workspace &&path can be normalized by the scheduler into a non-existent/data/workspace/...(the/data/skills/...segment gets dropped), so every run fails silently.workspace/+ relative path is immune. (Confirmed in a real run.)cd - Keep at its default so the transition-only alerts actually reach the user. keepalive is already silent on healthy runs, so there's no spam to suppress — and a real outage should ping you.
deliver - The interval may be normalized (e.g. "every 2 minutes" → 3 min) — fine.
将同一个脚本设置为廉价的模式定时任务。一个定时任务即可监控所有站点 — keepalive.sh会一次性遍历中的所有站点,因此无需为每个站点单独设置监控:
command.cf_state.jsonscheduled_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 - 保留默认值,以便状态转换的警报能真正送达用户。keepalive脚本在服务健康时已静默,因此不会产生垃圾信息 — 真正的故障应触发通知。
deliver - 时间间隔可能会被规范化(例如“every 2 minutes” → 3分钟) — 这是正常的。
Verify durability (do all of this before claiming "stable")
验证持久性(在声称“稳定”前完成所有这些操作)
bash
undefinedbash
undefined1. start + idempotency
1. 启动 + 幂等性测试
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://...
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh # 启动服务
bash skills/cloudflare-tunnel-publish/scripts/keepalive.sh # 静默无操作
tail -5 logs/keepalive.log # 确认日志显示ok https://...
2. boot wired
2. 检查开机启动配置
grep keepalive setup.sh
grep keepalive setup.sh
3. watchdog registered + command stored correctly (relative path!)
3. 确认监控已注册且命令存储正确(相对路径!)
scheduled_task(action="list")
scheduled_task(action="list")
**Don't claim "long-term stable" after only editing `setup.sh`.** That covers
restarts but not mid-life death. Confirm BOTH triggers (boot + schedule) point at
keepalive.sh, and that the first run logged `ok https://...`.
**Port collision is a silent failure.** Other workspace projects may already hold
common ports (8000/8080/8765). If your app's `bind()` fails with `Address already
in use` it exits, but `curl localhost:<port>` still returns 200 — someone else's
app is answering. Use a high, project-unique port and verify the page is *your*
content (`curl https://yourdomain.com | head`), not just a 200.
**Tell the user explicitly:**
> 🔔 你的站点跑在容器里。容器可能因更新/内存/迁移**随时**重启,隧道进程偶尔也会自己掉线(域名变 502/521/530/1033)。我已经把一个自愈脚本写进了 `workspace/setup.sh`(开机自动拉起)并设了每几分钟一次的巡检(掉线自动重拉、恢复/失败才通知你)。两层都指向同一个脚本,你基本不用管。真打不开时让我看一眼 `logs/keepalive.log` 和 `logs/cloudflared.log` 就能定位。**不要仅编辑`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` 就能定位。Plan limits
计划限制
- Free plan is enough. No upsell needed.
- Free plan only proxies ports 80/443 publicly — irrelevant to us, since the tunnel always exposes 443 to the world; can be anything.
localhost:<port>
- 免费计划已足够。无需升级付费。
- 免费计划仅公开代理80/443端口 — 这对我们无关紧要,因为隧道始终向外界暴露443端口;可以是任意端口。
localhost:<port>
State file format
状态文件格式
workspace/.cf_state.jsonsitesjson
{
"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.shsites[]- its own keepalive, and only one was watched" bug: there is now exactly one keepalive process that knows about every configured site.
workspace/.cf_state.jsonsitesjson
{
"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.shsites[]Multiple sites — the design
多站点设计
One tunnel, many hostnames, one watchdog. This is the only sane topology:
- defaults to reusing the first healthy tunnel it finds on the account. It merges the new hostname into the tunnel's existing ingress rules (preserving other hostnames) and points the DNS CNAME at that tunnel.
setup.py - holds a
.cf_state.jsonarray. Eachsites[]run appends (or replaces) one entry.setup.pyremoves one entry.teardown.py - iterates
keepalive.shand guards every site. One process, one watchdog, all hostnames. If a site shares a tunnel with another, the samesites[]process serves both — keepalive won't start a second one.cloudflared
When the agent gets this wrong (the bug that prompted this section):
creating a new tunnel per site means N processes, N PID files,
N watchdogs to wire — and in practice only one watchdog ever gets set up. The
other tunnels silently die and nobody notices for weeks. Reuse the tunnel,
append to , let one keepalive guard them all.
cloudflaredsites[]一个隧道,多个主机名,一个监控。 这是唯一合理的拓扑:
- 默认复用账户中找到的第一个健康隧道。它将新主机名合并到隧道的现有入口规则中(保留其他主机名),并将DNS CNAME指向该隧道。
setup.py - 包含
.cf_state.json数组。每次运行sites[]会追加(或替换)一个条目。setup.py会删除一个条目。teardown.py - 遍历
keepalive.sh并监控所有站点。一个进程,一个监控,覆盖所有主机名。如果多个站点共享一个隧道,同一个sites[]进程会为所有站点提供服务 — keepalive不会启动第二个进程。cloudflared
当Agent处理错误时(促使编写此部分的bug):为每个站点创建新隧道意味着N个进程、N个PID文件、N个需要配置的监控 — 而实际上通常只有一个监控被正确设置。其他隧道会静默崩溃,数周内无人察觉。复用隧道,追加到,让一个keepalive脚本监控所有站点。
cloudflaredsites[]