不用公网IP、不用开端口,Cloudflare Tunnel + Caddy 搞定内网服务公网访问

2026-06-14

家里或公司内网跑了一堆服务——NAS 管理界面、Home Assistant、各种 Dashboard——想从外面访问怎么办?

传统做法是路由器端口映射,配合 DDNS。但这套方案有几个硬伤:运营商可能封端口、公网 IP 不一定有、每加一个服务都要改路由器规则、HTTPS 证书还得自己折腾。

Cloudflare Tunnel + Caddy 的组合可以绕开这些问题。零端口暴露,一个域名按子域名分发到不同内网服务,HTTPS 由 Cloudflare 自动处理。

Cloudflare Tunnel 连接云端与内网服务器

整体架构

整个链路分三层:

用户浏览器
    │ (HTTPS)
    ▼
Cloudflare CDN + TLS 终止
    │
    ▼
Cloudflare Tunnel (cloudflared)
    │ (HTTP, 内网)
    ▼
Caddy 反向代理
    ├── a.your-domain.com → 服务 A(端口 5666)
    ├── b.your-domain.com → 服务 B(端口 3000)
    └── c.your-domain.com → 以后加新服务只改 Caddyfile

Cloudflare Tunnel 负责从 Cloudflare 的网络打通到你的内网,Caddy 负责在内网里按域名把流量分发到不同服务。两者分工明确:Tunnel 管”怎么进来”,Caddy 管”去了哪里”。

为什么不用 Nginx?Caddy 的配置比 Nginx 简洁得多——一个反向代理规则三行搞定,自带热更新,改完 Caddyfile 不用 reload 就生效。虽然在这个场景下自动 HTTPS 需要关掉(后面会讲),但整体配置量还是少很多。

第一步:域名准备

你需要一个域名,把它的 DNS 托管切到 Cloudflare。

免费域名可以在 DNSHE(dnshe.com)这类平台注册,不需要实名。付费域名直接在 Cloudflare 自己买也行。

拿到域名后,在 Cloudflare 添加站点,按提示把 NS 记录改成 Cloudflare 分配的 nameserver。等 DNS 生效(通常几分钟),域名就在 Cloudflare 管理下了。

第二步:创建 Cloudflare Tunnel

安装 cloudflared 命令行工具:

curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared

登录并创建隧道:

cloudflared tunnel login
cloudflared tunnel create my-tunnel

login 会打开浏览器让你授权。创建成功后会输出 Tunnel ID 和 credentials 文件路径,记下来。

如果你更喜欢图形界面,也可以直接在 Cloudflare Dashboard → Zero Trust → Networks → Tunnels 里创建,效果一样。

第三步:部署 Caddy

用 Docker 跑 Caddy,配置目录提前建好:

mkdir -p /opt/caddy/config /opt/caddy/data

写 Caddyfile:

{
    auto_https off
    http_port 80
}

:80 {
    @svc-a host a.your-domain.com
    handle @svc-a {
        reverse_proxy 192.168.1.100:5666
    }

    @svc-b host b.your-domain.com
    handle @svc-b {
        reverse_proxy 192.168.1.100:3000
    }

    handle {
        respond 404
    }
}

启动容器:

docker run -d \
  --name caddy \
  --restart unless-stopped \
  -p 8880:80 \
  -v /opt/caddy/config:/config \
  -v /opt/caddy/data:/data \
  caddy:latest \
  caddy run --config /config/Caddyfile --adapter caddyfile

这里映射到 8880 而不是 80,是为了避免和宿主机上可能已有的 Web 服务冲突。你可以根据实际情况选端口。

如果你的环境比较新,用 Docker Compose 更干净:

services:
  caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
      - "8880:80"
    volumes:
      - ./config:/config
      - ./data:/data
    command: caddy run --config /config/Caddyfile --adapter caddyfile

两个关键配置解释

auto_https off:Caddy 跟在 Cloudflare 后面,TLS 终止已经由 Cloudflare 处理了。如果不开这个,Caddy 会尝试自己申请 Let’s Encrypt 证书,但在 Tunnel 后面它完成不了 ACME 验证,会直接报错。

http_port 80:关掉 auto_https 后,Caddy 默认只监听 443 不监听 80。加上这行强制它在 80 端口(也就是容器里的 80 端口)上监听 HTTP 请求。

第四步:部署 Cloudflare Tunnel

写配置文件:

tunnel: <你的TUNNEL_ID>
credentials-file: /etc/cloudflared/credentials.json

ingress:
  - service: http://127.0.0.1:8880

这里只有一条 ingress 规则:把所有流量转发到 Caddy。路由分发交给 Caddy 处理。

启动:

docker run -d \
  --name cloudflared \
  --restart unless-stopped \
  --network host \
  -v /opt/cloudflared:/etc/cloudflared:ro \
  cloudflare/cloudflared:latest \
  tunnel --config /etc/cloudflared/config.yml run

--network host 让 cloudflared 能直接访问宿主机上的 127.0.0.1:8880(也就是 Caddy 容器映射出来的端口)。

如果你用 Docker Compose,注意两个容器之间的网络互通。最简单的做法是让它们共享同一个 Docker network:

services:
  caddy:
    image: caddy:latest
    # ... 同上

  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --config /etc/cloudflared/config.yml run
    volumes:
      - ./cloudflared:/etc/cloudflared:ro
    depends_on:
      - caddy

Compose 模式下,cloudflared 的 service 地址可以写 http://caddy:80(用容器名做 DNS),不需要 --network host

第五步:配置 DNS

在 Cloudflare Dashboard 里为每个子域名添加 CNAME 记录:

类型: CNAME
名称: a(或其他子域名)
目标: <TUNNEL_ID>.cfargotunnel.com
代理: 开启(橙色云朵)

或者更省事:在 Zero Trust → Tunnels → 你的隧道 → Public Hostname 里配置,它会自动创建 DNS 记录。

第六步:验证

# 公网访问测试
curl -s -o /dev/null -w "%{http_code}" https://a.your-domain.com

# 本地 Caddy 直连测试
curl -s -H 'Host: a.your-domain.com' http://127.0.0.1:8880

两个都返回 200 就通了。

加新服务:只需两步

反向代理按域名分发请求到不同服务

以后要加新的内网服务:

  1. 在 Caddyfile 里加一段 host matcher + reverse_proxy
  2. 在 Cloudflare 加一条 CNAME 记录

Tunnel 配置不用动。Caddy 会自动热加载新配置。

五个踩坑点

这部分是从实际部署中总结出来的,网上很少集中讲。

1. Dashboard 配置会覆盖本地 config.yml

你在本地 config.yml 里写了多条 ingress 规则,实际访问全指向同一个服务?很可能是 Cloudflare Dashboard 里也配了路由规则,它的优先级比本地文件高。

验证方法:用 API 查隧道配置,如果返回 "source": "cloudflare" 就说明配置来自 Dashboard。

解决办法:在 Dashboard 里把路由配对,或者全用 catch-all 规则转发到 Caddy,让 Caddy 来做路由分发(也就是本文推荐的方案)。

2. 配置文件权限

permission denied — cloudflared Docker 容器默认用非 root 用户跑,配置文件权限太严(比如 700)就读不了。

chmod 644 /opt/cloudflared/config.yml /opt/cloudflared/credentials.json

3. Caddy 报 ACME 验证失败

日志里出现 Cannot negotiate ALPN protocol "acme-tls/1" 就是因为没关 auto_https。加上那两行全局配置就好。

4. auto_https off 后 Caddy 不响应

关了 auto_https 但没写 http_port 80,Caddy 就不监听任何端口。一定要两个一起写。

5. 免费域名和通配符

Cloudflare 免费计划不支持通配符 DNS 记录。*.your-domain.com 需要 Pro 或 Enterprise。

不过问题不大:每加一个服务就手动加一条 CNAME,反正 Tunnel 那边不用改。

安全性

这个方案的一个核心优势是零端口暴露。你的路由器不需要开放任何端口,cloudflared 主动向外连接 Cloudflare 的网络,流量加密传输。

此外 Cloudflare 提供 DDoS 防护和 WAF。如果想更严格,可以在 Cloudflare Access 里给特定域名加认证层——比如只有你自己的邮箱才能访问管理界面。

总结

Cloudflare Tunnel + Caddy 是一套轻量的内网服务暴露方案。一个管隧道,一个管路由,各司其职。加新服务改两行配置就行,不用碰路由器,不用申请证书,不用记端口号。

如果你手上正好有内网服务想从外面访问,这套方案值得试试。