家中服务器没有公网入口时,可以用 Cloudflare Tunnel 对外发布容器网站。服务器上的 cloudflared 主动连接 Cloudflare,浏览器访问公开域名,再由这条连接把请求送到本地服务,不需要在家庭路由器上开放入站端口。

这里采用“正式 Tunnel+Docker Compose+Nginx”的方式,网站和 API 共用一个域名。远程管理 Mac、数据库等服务仍走私有网络,相关配置见 用 Tailscale 和 SSH 远程管理家中 Mac 和 一个 SSH 连接转发多个 Docker 服务端口。

先确定请求经过哪里

假设已有两个应用容器:web 监听 3000,api 监听 5001。应用在容器中监听可被同网络其他容器访问的地址,例如 0.0.0.0。加入 Nginx 和连接器后,请求路径是:

浏览器 https://app.example.com
    ↓
Cloudflare 边缘入口
    ↓ 加密 Tunnel
cloudflared 容器
    ↓ http://nginx:80
Nginx 容器
    ├─ /       → web:3000
    └─ /api/   → api:5001

浏览器使用 HTTPS,下面的 Nginx 源站使用 Docker 网络内的 HTTP。这里不再要求 Nginx 加载原来公网部署的域名证书,也不保留把全部 HTTP 无条件跳到同一 HTTPS 域名的规则,否则可能产生循环跳转。

这套 HTTP 回源建立在连接器与源站同机、同一受控 Docker 网络的前提上。如果源站位于另一台机器或需要跨不可信网络,应另行配置和验证源站 TLS。

下面从已有 web、api 服务和可管理的 Cloudflare 域名开始。域名接管过程见 Cloudflare DNS、代理与访问速度有什么关系。

创建正式 Tunnel

在 Cloudflare 控制台进入 Tunnel 管理页面,创建 cloudflared 类型的 Tunnel,选择 Docker 连接器。已有正式 Tunnel 可以复用,无须每次部署都重新创建。Cloudflare 控制台创建指引

这里不用临时 Quick Tunnel。它的域名会变化,当前也不支持 SSE,不适合需要流式聊天的稳定入口。Quick Tunnel 限制

把该 Tunnel 的 Token 存入服务器项目根目录的 .env,保留文件原有配置:

CLOUDFLARE_TUNNEL_TOKEN=<替换为该Tunnel的Token>

.env 不提交到 Git,也不把真实 Token 放进聊天或命令示例。Token 是连接器凭据,Tunnel ID 是后面 DNS 关联使用的标识,两者不同。

本篇由 Compose 运行连接器,不再另外执行控制台生成的 docker run,避免重复运行两套难以对应的容器。

把连接器和 Nginx 加入应用网络

在已有 compose.yaml 的 services 下合并下面两个服务。示例假定 web、api、nginx 和 cloudflared 都在同一个 Compose 默认网络;如果项目使用自定义网络,需要把它们接入同一个网络。

services:
  # 保留原有的 web、api 和其他服务。
  nginx:
    image: nginx:stable-alpine
    restart: unless-stopped
    ports:
      - '127.0.0.1:9001:80'
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - web
      - api
 
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: [tunnel, --no-autoupdate, run]
    environment:
      TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN:?Set CLOUDFLARE_TUNNEL_TOKEN}
    depends_on:
      - nginx

127.0.0.1:9001:80 用于在服务器本机检查入口。cloudflared 不通过这个宿主机端口访问 Nginx,而是使用 Docker 服务名 nginx:80。

cloudflared 容器里的 127.0.0.1 指向它自己,不是服务器,也不是 Nginx。这是填写 Tunnel 服务地址时容易弄错的地方。

配置 Nginx

在项目目录创建 nginx/default.conf,替换域名并按实际应用修改上游端口:

resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;
 
upstream app_web {
    zone app_web 64k;
    server web:3000 resolve;
}
 
upstream app_api {
    zone app_api 64k;
    server api:5001 resolve;
}
 
server {
    listen 80;
    server_name app.example.com;
    client_max_body_size 20m;
 
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Connection "";
    proxy_connect_timeout 10s;
    proxy_send_timeout 600s;
    proxy_read_timeout 600s;
 
    location = /api {
        return 308 /api/$is_args$args;
    }
 
    location ^~ /api/ {
        proxy_pass http://app_api/;
        proxy_buffering off;
        proxy_cache off;
        gzip off;
    }
 
    location / {
        proxy_pass http://app_web;
    }
}

官方 Nginx 镜像把 conf.d/*.conf 包含在 http 上下文中,因此这里可以写 resolver、upstream 和 server。127.0.0.11 是该 Docker 网络内的 DNS,resolve 让 Nginx 能随服务地址变化更新上游,要求共享内存 zone 和可用的 resolver。Nginx upstream 动态解析

proxy_pass http://app_api/ 末尾有 /,会把匹配的 /api/ 前缀替换掉。例如 /api/chat 转给后端时是 /chat。如果后端路由本来就包含 /api/chat,这里应改为 proxy_pass http://app_api;,保留原路径,不能直接照抄。Nginx proxy_pass 的 URI 规则

这里固定 X-Forwarded-Proto https,表达公开入口使用 HTTPS 的约定。应用需要仅信任预期代理并正确读取转发头,才能据此生成 URL 或处理安全 Cookie。它不适合直接搬到同时提供公网 HTTP 的入口中;本机测试得到的协议相关行为也会遵循这一约定。X-Forwarded-For 仍需按可信代理链解析,不能直接把连接器地址当成真实访客 IP。

先确认本机入口

先启动项目原有的应用及其数据服务,完成必要的数据库迁移,确认 web、api 可以运行。然后在服务器项目目录执行:

docker compose -f compose.yaml config --quiet
docker compose -f compose.yaml run --rm --no-deps nginx nginx -t

config --quiet 检查配置,不展开环境变量中的凭据。nginx -t 应返回配置语法与检查成功;失败时按报错位置修改,不继续启动故障入口。

通过后执行:

docker compose -f compose.yaml up -d --no-deps nginx
curl -i -H 'Host: app.example.com' http://127.0.0.1:9001/

应收到预期页面,普通首页通常是 HTTP 200。若项目本来就要求跳到登录页,应核对目标是否正确;反复跳回同一个 URL 则需要检查旧的 HTTPS 强制跳转。

已有 Nginx 运行时,修改挂载的配置文件不会让它自动重新加载。检查通过后可执行 docker compose -f compose.yaml exec nginx nginx -s reload;修改了容器环境或挂载配置,则用 up -d --force-recreate nginx 重建。

配置公开应用路由和 DNS

在该 Tunnel 的已发布应用路由中配置:

字段示例值
子域名app
根域名example.com,替换为自己的域名
服务类型HTTP
服务地址nginx:80,完整 URL 为 http://nginx:80

控制台通常会创建对应 DNS 记录。回到 DNS 记录页核对:名称是 app,类型是已代理 CNAME,目标是当前 <Tunnel-ID>.cfargotunnel.com。如果已有同名记录,先确认原用途,确定切换后修改它,不重复添加冲突的 A、AAAA 或 CNAME。

DNS 负责把请求交给哪个 Tunnel,应用路由负责把请求交给本地哪个服务。

回到服务器启动连接器:

docker compose -f compose.yaml up -d --no-deps cloudflared
docker compose -f compose.yaml ps -a
docker compose -f compose.yaml logs --since=5m --tail=80 cloudflared

最后在另一台电脑测试:

curl -i https://app.example.com/

检查页面、登录和 API,而不只看域名能否解析。Tunnel 发布本身不会替应用添加登录限制;如果只给自己使用,可以按需要配置 Cloudflare Access,或保留应用已有的认证。

1033、502 和循环跳转分别查哪里

现象先检查什么
本机端口连接失败Nginx 容器、端口映射、配置与退出日志
本机 200,公网 1033DNS 所指 Tunnel 是否有可用连接器,Token 是否属于该 Tunnel
Tunnel 返回无法到达源站的 502cloudflared 到 nginx:80 的网络、服务名与协议
Nginx 返回 API 502Nginx 到 API 的连接、后端状态和路径
反复跳回相同 HTTPS URL源站或应用是否仍无条件强制跳转

1033 表示 Cloudflare 找不到可用的 cloudflared 实例接收流量,不能单凭它断定 CNAME 填错。先检查对应连接器,再核对 DNS 是否指向另一个 Tunnel。源站 502 则要结合 cloudflared 与 Nginx 日志区分失败位置。Cloudflare Tunnel 常见错误