用一条 Cloudflare Tunnel 收敛全部对外服务
把"逐个暴露端口 + 逐条挂 DNS"改成一条隧道统一出口后,宿主不再监听任何对外端口。记录 ingress 顺序、多副本 HA 的真实作用边界,以及 metrics 默认绑定带来的意外暴露。
起点:散乱的暴露方式
最初每加一个服务就是三件事:容器里写 ports:、防火墙放行、DNS 挂一条 A 记录指向源站 IP。服务一多就出现几个必然结果:
- 宿主上有一堆
0.0.0.0监听,其中任何一个服务出问题都是直通公网 - 源站 IP 出现在 DNS 里,等于把回源地址公开
- 证书要在每个服务上各自处理(自签、或者各自的 ACME 流程)
- 忘记给某个端口加规则时,它默认是开着的
改成隧道之后,这三件事变成一件:加一条 ingress 规则。
架构
互联网 ──► Cloudflare 边缘(TLS 在此终止)
▲
│ 隧道:cloudflared 主动外连建立
│
cloudflared ×N ──共享 docker 网络──► 各服务容器
关键性质:cloudflared 是出站连接。宿主不监听任何对外端口,不需要公网入站,也不需要为它开防火墙。源站 IP 不再出现在任何 DNS 记录里。
ingress 规则的两件事
规则托管在 Cloudflare 侧(token 模式隧道),本地没有配置文件。只有两个高频错误:
1. 顺序。 具体 hostname 必须排在兜底项之前:
ingress:
- hostname: notes.example.com
service: http://notes:3000
- hostname: files.example.com
service: http://files:5244
- service: http_status:404 # 兜底必须在最后
写反了不会报错,只会让所有域名都命中 404。
2. DNS 指向隧道而不是源站。 每个 hostname 需要一条 proxied 的 CNAME:
<子域> CNAME <隧道ID>.cfargotunnel.com (Proxied)
橙色云必须打开,否则流量不会经过边缘,TLS 也就不会被终止。
多副本 HA:作用边界比想象中窄
同一隧道可以注册多个 connector 副本,边缘会自动分流。这解决的是「进程还活着但连接已经断了」这一类故障——此时:
- Docker 的
HEALTHCHECK能发现(cloudflared tunnel ready探/ready) - 但 Docker 不会因为 unhealthy 就重启容器,这一点经常被误解
所以多副本真正的价值是:单个 connector 挂死时,边缘绕行到另一个副本,服务不中断。健康检查只负责报告,不负责自愈。二者不能互相替代——只做健康检查会有"显示不健康但一直不恢复"的窗口,只做多副本则缺少可观测性。
一个意外暴露:metrics 默认绑全网卡
cloudflared 的 --metrics 参数不写地址时默认绑 [::]。这意味着同一容器网络里的任何容器都能读到:
/metrics— 连接数、请求统计/debug/pprof/— goroutine 与堆转储
后者的价值不必多说:堆转储里可能有 token、请求头、内存中的凭据。如果那个共享网络里还跑着第三方镜像(很多镜像不是你自己构建的),这就不是理论风险。
修复只是加一个地址:
command: tunnel --no-autoupdate --metrics 127.0.0.1:20241 run
值得留意的是:这类"默认值恰好是宽的那个"的问题,往往在部署时被当作可用配置抄进文档,然后一路带着走。
新增一个服务:三步
# 1. 让服务加入共享网络。注意不写 ports:
services:
app:
image: some/image
networks: [edge]
# 有意不写 ports: —— 这是重点
networks:
edge:
external: true
# 2. 加 ingress 规则(云端),把 hostname 映射到服务名
# 3. 加 DNS:CNAME <子域> → <隧道ID>.cfargotunnel.com(Proxied)
不需要碰端口,不需要碰防火墙。把"新增服务"从三处改动收敛成一处,是这次改造最大的收益。
资源上限顺带要设
隧道把服务藏到了边缘后面,但容器本身的内存并没有上限。同机多个服务共用内存时,一个 OOM 会牵连全部。mem_limit / memswap_limit / pids_limit 三个一起设,其中 memswap_limit 等于 mem_limit 是有意的——它禁止换页,避免内存压力变成磁盘 IO 抖动。宿主 swap 本来就紧张时,让容器快速失败比让整机一起卡要好。
排查表
| 现象 | 原因 | 处理 |
|---|---|---|
| HTTP 521 | 边缘连不上源站,或 DNS 刚改还没收敛 | 等 30 秒重试;看 connector 日志 |
| HTTP 404 | 该 hostname 没有匹配到 ingress 规则 | 检查规则顺序与 hostname 拼写 |
| HTTP 502 | connector 通了但后端拒绝连接 | 在共享网络里直接探后端端口 |
| 域名不解析 | 本地 resolver 负缓存 | 换公共 DNS 复核,如 dig @1.1.1.1 |
| 改了密码/配置不生效 | 只改了运行时状态,没改 compose 的环境变量 | 改 compose 并重建容器 |
最后一条值得单独强调:只要 environment 里存在某个配置项,它每次启动都会覆盖运行时设置。这是"改了没生效"最常见的成因。
小结
- 隧道把入站攻击面从"若干个监听端口"收敛成"一条出站连接 + 边缘的规则"
- ingress 顺序和 DNS 的 proxied 状态是两个最容易错且不报错的地方
- 多副本解决连接挂死,健康检查只报告不自愈,两者都需要
- 默认值要逐项审:
--metrics不写地址就是全网卡