↓ 跳过正文

Cloudflare Cloudflare Tunnel Cloudflared

笔记本就是服务器:用 Cloudflare Tunnel 把副业项目放上公网

Cloudflare Tunnel 是什么、安全性如何、要花多少钱?不开任何端口,让自己机器上的服务拿到真正的 HTTPS 主机名,再用 ingress 白名单决定公网能访问哪些路径,附 Docker Compose 配置。

笔记本就是服务器:用 Cloudflare Tunnel 把副业项目放上公网

我的每个想法都是这样开始的:在笔记本上跑起来了,能用,然后想让别人也看看。有很长一段时间,下一步总是一张云账单:一台机器、一个数据库、一个 bucket、一个指过去的域名,再为一个只有三个人会看的东西按月付费。对副业项目,或者一个正经项目的第一个月来说,这个顺序是反的:还不知道会不会有生产环境,就先为生产环境掏钱了。

这篇文章讲的是另一种顺序。服务继续跑在开发它的那台机器上,由 Cloudflare Tunnel 给它一个公开的 HTTPS 主机名。笔记本上没有任何东西对互联网监听:不做端口转发,不碰路由器设置,也不暴露 IP 地址。是机器主动连出去到 Cloudflare,Cloudflare 再把请求顺着这条连接送回来。Tunnel 本身免费,TLS 证书、DDoS 防护和缓存则随你多半已经有的 Cloudflare zone 一起附送。

例子是真实的。我的一个产品 跑在 Fly.io 上,它的移动应用从后端获取 JavaScript 的 OTA 更新(自托管的 Expo Updates)。我想在自己的 Mac 上再跑一份更新服务器,给开发构建用,生产环境原封不动。下面的输出都来自那个下午,包括没跑通的那部分。

Cloudflare Tunnel 到底是什么
#

普通的服务器是等别人连进来的。要从互联网访问一台笔记本,你得在路由器上开端口、转发到笔记本,还得指望运营商给你一个公网 IPv4 地址(很多地方都不给,我这里也是)。然后,那个端口上监听的所有东西,整天都暴露在互联网上每一个扫描器面前。

Tunnel 把方向反过来。cloudflared 跑在你的应用旁边,主动向 Cloudflare 的边缘建立出站连接,一共四条,连到附近的两个数据中心。你 zone 里的一条 DNS 记录把主机名指向这个 tunnel。当请求打到这个主机名时,Cloudflare 终止 TLS,把请求顺着其中一条连接转发下来,cloudflared 再按你配置里写的交给对应的本地服务。

机器只会往外连。Cloudflare 的 zone 规则在 tunnel 之前执行;ingress 规则决定什么能被访问到。

从安全角度看,关键在于路由器上没有任何入站规则,你的 IP 地址也从不出现在 DNS 里。有人扫描你家的网络连接,什么也找不到,因为上面根本没有东西在监听。

从头到尾的搭建
#

你需要一个托管在 Cloudflare 上的域名(免费套餐就够),以及装好 cloudflared(Mac 上 brew install cloudflared)。

1. 登录一次。 这会打开浏览器,你选好 zone,它会保存一张证书,让这台机器可以在那个 zone 里创建 tunnel 和 DNS 记录:

cloudflared tunnel login

2. 创建 tunnel。 这个项目的所有东西我都放在单独的文件夹里,因为凭据文件就是 tunnel 的身份:谁拿到它,谁就能替你的主机名提供流量。

mkdir -p ~/.cloudflared/myapp && cd ~/.cloudflared/myapp
cloudflared tunnel create --credentials-file ./creds.json myapp-local
chmod 600 creds.json
Tunnel credentials written to ./creds.json. Keep this file secret. To revoke these credentials, delete the tunnel.
Created tunnel myapp-local with id 1ade030c-…

3. 把主机名指向它。 每个主机名一条命令,创建一条指向 <tunnel-id>.cfargotunnel.com 的代理 CNAME:

cloudflared tunnel route dns myapp-local ota.example.com
cloudflared tunnel route dns myapp-local checkin-local.example.com

4. 写 ingress 规则。 这是对安全最要紧的部分,所以下面单独一节来讲。

5. 跑起来。 我的应用本来就跑在 Docker Compose 下,所以 cloudflared 只是同一个网络里多出来的一个容器。它按服务名访问应用,应用根本不需要在宿主机上发布端口:

# compose.tunnel.yml, used alongside the project's docker-compose.yml
services:
  cloudflared:
    image: cloudflare/cloudflared:2026.10.7
    command: tunnel --config /etc/cloudflared/config.yml run
    volumes:
      - ${HOME}/.cloudflared/myapp:/etc/cloudflared:ro
    restart: unless-stopped
    depends_on: [api, edge]
docker compose -f docker-compose.yml -f ~/.cloudflared/myapp/compose.tunnel.yml up -d cloudflared
INF Registered tunnel connection connIndex=0 … location=icn06 protocol=quic
INF Registered tunnel connection connIndex=1 … location=icn07 protocol=quic
INF Registered tunnel connection connIndex=2 … location=icn07 protocol=quic
INF Registered tunnel connection connIndex=3 … location=icn06 protocol=quic

四条连接连到首尔,笔记本就从外向内地上了互联网。restart: unless-stopped 让它在重启后、Docker 一启动就自动恢复。

暴露路径,而不是服务器
#

这是我差点犯的错,我猜也是最常见的一个。最省事的配置只有一行:hostname → http://localhost:8000。这会把整个开发服务器都发布出去。

开发服务器不是为这个设计的。我的开着 DEBUG=True,任何错误都会显示完整的 traceback 和配置。它还关掉了认证(AUTH_MODE=disabled),任何 bearer token 都会让你以测试用户身份登录。这在 localhost 上完全合理,放到公网主机名上就是灾难。你的多半也有类似的东西:用默认密码的管理后台、灌测试数据的端点、debug toolbar,或者因为只有你自己在调用、所以信任调用方的 API。

所以 tunnel 配置是一份白名单。cloudflared 按顺序拿每个请求去匹配 ingress 规则,按主机名,也按路径上的正则表达式。第一条匹配的生效,最后一条兜住其余所有请求:

tunnel: myapp-local
credentials-file: /etc/cloudflared/creds.json

ingress:
  # OTA: the two read-only endpoints the phone calls, and the files they link.
  - hostname: ota.example.com
    path: ^/api/expo-updates/(manifest|status)/$
    service: http://api:8000
  - hostname: ota.example.com
    path: ^/media/
    service: http://api:8000
  # Device check-ins, authenticated by each device's own key.
  - hostname: checkin-local.example.com
    path: ^/c/$
    service: http://edge:8081
  # Everything else, including /admin/ and the publish endpoint: 404 at the edge.
  - service: http_status:404

发布端点就在同一台服务器上、同一个前缀下(/api/expo-updates/import-update/),但它不在列表里。管理后台和 API 的其余部分也不在。这些请求根本出不了 Cloudflare。

你不必盲信自己的正则;cloudflared 会告诉你一个 URL 匹配的是哪条规则:

cloudflared tunnel --config config.yml ingress rule https://ota.example.com/admin/
https://ota.example.com/api/expo-updates/manifest/        service: http://api:8000
https://ota.example.com/api/expo-updates/import-update/   service: http_status:404
https://ota.example.com/admin/                            service: http_status:404
https://ota.example.com/media/expo-updates/x.js           service: http://api:8000
https://checkin-local.example.com/c/                      service: http://edge:8081
https://checkin-local.example.com/health                  service: http_status:404

我会把这个检查写进脚本,每次改配置都跑一遍。只要几秒钟,而一条比你本意匹配得更多的正则,正是那种只会从别人那里得知的错误。

还有两条经验法则:

  • 暴露出去的东西应该自己负责认证。 OTA 端点提供的是公开的只读数据。设备 check-in 用每台设备自己的密钥签名。两者的安全都不依赖网络。如果你想给别人看一个没有登录的页面,就在那个主机名前面放 Cloudflare Access(50 个用户以内免费),而不是自己去做登录。
  • 别把 tunnel 当成开发流程里常驻后台的一部分。 有意识地启动它,只为你想要的那个主机名,用完就停。docker compose stop cloudflared 一条命令就把一切从互联网上撤下来。

吃掉我主机名的那个重定向
#

Tunnel 连上了,规则也验证过了,我从外部试了一下:

https://ota.example.com/api/expo-updates/status/?…   301
https://ota.example.com/api/expo-updates/manifest/   301
https://ota.example.com/admin/                       301
https://checkin-local.example.com/c/                 301

每条路径都返回同一个 301,包括那些 tunnel 本该回 404 的。这说明请求根本没到 cloudflared。Location 头写的是 https://www.example.com/,再加上 server: cloudflare 头,事情就清楚了:是 Cloudflare 自己在应答。

这是我自己挖的坑。几个月前我加了一条 zone 级的重定向规则,“其他任何子域名 → www”,用来当那些遍历编造出来的子域名的扫描器的黑洞。重定向规则在 Cloudflare 的请求流水线里执行,早于请求被发往源站,而 tunnel 不过是一个源站。同一条规则也拦住过我的预览环境,让 Workers 根本见不到请求。修法和上次一样:在规则表达式里再加一个例外。

… and not http.host in {"ota.example.com" "checkin-local.example.com"}

如果你的 tunnel 主机名返回的是 Cloudflare 的重定向或错误页面,而不是你的应用,那在调试 tunnel 之前,先检查这个 zone 的 Rules → Redirect Rules、Page Rules 和所有 WAF 自定义规则。cloudflared 会记录它转发的每个请求,所以日志为空就说明请求根本没走到那一步。

它做不到的事
#

  • 来自公网的 UDP。 公开的 tunnel 主机名只承载 HTTP(S)(外加 WebSocket 和 gRPC)。我产品的设备平时通过 UDP 上的 CoAP 做 check-in,这走不了 tunnel;连本地副本的设备会退回到 HTTPS check-in,每次要多花几秒钟。如果你的想法是游戏服务器或 VoIP,tunnel 就不是合适的工具。
  • 在线率。 这是你的笔记本。合上盖子,站点就挂了;Cloudflare 会显示一个 530 错误页。拿来做演示、十个人的内测或者一份 staging 副本都没问题,但不适合任何有人依赖的东西。
  • 编译进构建里的东西。 移动应用的更新 URL 是原生配置,所以只有用 tunnel URL 构建出来的包才会用它。我的商店构建依然连着生产环境,这也正是这套方案敢拿来试的原因。

成本
#

免费套餐的 Cloudflare zone、tunnel、它的证书和 DNS 记录:0 美元。机器本来就开着。唯一真正的成本是,现在你自己就是运维值班团队。

它帮你省下的,是从“在我机器上能跑”到“值得花钱托管”之间的那个阶段。当天下午你就能把链接发给测试、设计师或者第一个客户。如果没人在意,你什么也没损失。如果有人在意,迁到真正的主机上也只是一处改动:把 CNAME 指到别处。

评论

评论经审核后显示。