跳到主要内容

Cloudflare 代理配置

把 Aivory 挂到 Cloudflare 后面,可以一次拿到三样东西:隐藏源站 IP(访客只能看到 Cloudflare 边缘节点的地址)、免费且自动续期的边缘证书(浏览器到 Cloudflare 这一段的 HTTPS 不用你操心)、以及 Cloudflare 的 DDoS 缓解与基础 WAF。附带的好处还有全球 Anycast 就近接入和静态资源边缘缓存。

本页给出两条完整路线:

  • 方案 A:橙云代理。源站有公网 IP,DNS 记录开启 Cloudflare 代理(橙色云朵),流量路径为「访客 → Cloudflare → 源站 Nginx → app」。
  • 方案 B:Cloudflare Tunnel。源站没有公网 IP 或不想开放任何入站端口,由 cloudflared 容器主动向 Cloudflare 建立出站隧道。

两条路线都建立在同一个事实上:Aivoryapp 容器单容器同源伺服 SPA 和 /api,用哪个域名访问,哪个域名就能用,不需要为 Cloudflare 修改 ALLOWED_ORIGINS 或任何域名配置(该变量只在前后端分离部署时才有意义)。

前置条件:域名接入 Cloudflare

  1. 注册 Cloudflare 账号,在面板点击 Add a domain,输入你的域名(如 example.com),选择 Free 套餐即可。
  2. Cloudflare 会扫描现有 DNS 记录并给出两个 NS 服务器地址,到你的域名注册商处把 Nameserver 改成这两个地址。
  3. 等待状态变为 Active(通常几分钟到几小时),之后该域名的 DNS 与代理都在 Cloudflare 面板管理。

同时确认 Aivory 侧的基础部署已完成:Docker Compose 生产部署已跑通,方案 A 还要求已按反向代理与 HTTPS在源站装好 Nginx。

方案 A:橙云代理

第 1 步:DNS 记录开启橙云

在 Cloudflare 面板 DNS → Records 中添加(或修改)指向源站的记录,并把 Proxy status 设为 Proxied(橙色云朵):

TypeNameContentProxy status
Achat源站 IPv4 地址Proxied(橙云)
AAAAchat源站 IPv6 地址(如有)Proxied(橙云)

开启橙云后,dig chat.example.com 解析到的是 Cloudflare 边缘 IP,源站真实地址不再对外暴露。

想更彻底地隐藏源站

橙云只是让 DNS 不再泄露源站 IP。如果希望源站防火墙层面也只接受来自 Cloudflare 的回源流量,可以在防火墙(ufw / 云安全组)里仅放行 Cloudflare 官方公布的回源网段(https://www.cloudflare.com/ips/)访问 443 端口。这一步是可选加固,不影响功能。

第 2 步:选择 SSL/TLS 加密模式

SSL/TLS → Overview 中选择加密模式。三种模式的区别在于「Cloudflare 到源站」这一段怎么走:

模式浏览器 → CloudflareCloudflare → 源站源站要求是否推荐
FlexibleHTTPSHTTP 明文源站只需监听 80,无需证书仅内网/可信链路
FullHTTPSHTTPS,但不校验证书任意证书(含自签名)过渡用
Full (strict)HTTPSHTTPS,校验证书有效性受信任的真实证书推荐

推荐 Full (strict):源站按反向代理与 HTTPS配好 Let's Encrypt 证书即可满足要求。如果不想跑 certbot,也可以在 SSL/TLS → Origin Server 里签发一张 Cloudflare Origin CA 证书(有效期最长 15 年,只被 Cloudflare 回源信任)装到源站 Nginx 上,同样满足 Full (strict)。

源站只有 HTTP 时用 Flexible 的利与弊:

  • :Aivory 支持生产环境 HTTP 明文部署(应用的请求签名算法有纯 JS 回退,不依赖浏览器安全上下文的加密 API)。而 Flexible 模式下浏览器访问的是 https:// 地址,页面处于安全上下文,反而规避了纯明文 HTTP 部署下的各种浏览器限制,源站却完全不用配置证书,是零证书运维成本的最快路径。
  • :Cloudflare 到源站这一段是明文,任何中间网络都能看到流量内容(包括登录凭据和对话内容)。只有当这段链路可信(如同机房内网、云厂商私网)时才建议使用;走公网回源请务必升级到 Full (strict)。
Flexible 模式最常见的翻车:重定向死循环

反向代理与 HTTPS的示例 Nginx 配置会把 80 端口的请求 301 到 HTTPS。在 Flexible 模式下,Cloudflare 永远用 HTTP 回源,源站又永远 301 回 https://,浏览器会陷入无限重定向。用 Flexible 时,源站 80 端口必须直接伺服业务,不做 HTTPS 跳转。下面给出对应的最小配置。

Flexible 模式下的源站 Nginx(纯 HTTP,仍保留 SSE 与真实 IP 处理):

server {
listen 80;
listen [::]:80;
server_name chat.example.com;

# 普通上传默认上限 50MB(MAX_UPLOAD_BYTES),留出余量
client_max_body_size 100m;

location / {
proxy_pass http://127.0.0.1:8787;

# SSE 流式输出
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_http_version 1.1;
proxy_set_header Connection "";

# 真实客户端 IP(配合下一步的 real_ip 配置)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Flexible + OAuth 的组合注意

Flexible 模式下源站链路是 HTTP,若配置了 OAuth 登录,请把 OAUTH_CALLBACK_BASE_URL 显式设为对外的 https://chat.example.com,避免回调地址被拼成 http://,并同步在 OAuth 提供商侧登记 https 回调地址。详见 FAQ

第 3 步:源站 Nginx 还原真实访客 IP

这一步不可省略。Aivory 按客户端 IP 维护限流计数,判定规则是:仅当直连对端是内网或回环地址时才信任 X-Forwarded-For / X-Real-IP(取其中最右侧的非内网条目),直连公网对端时一律忽略这些头。

开启橙云后,源站 Nginx 看到的 TCP 对端是 Cloudflare 边缘节点。如果不做还原,Nginx 转发给 app 的「真实 IP」就是 Cloudflare 节点 IP:全球访客被折叠成少数几个 Cloudflare 节点地址,共享限流计数,一个人触发限流会连坐一大片用户。

Cloudflare 会把访客真实 IP 放在 CF-Connecting-IP 请求头里。用 Nginx 的 real_ip 模块还原:新建 /etc/nginx/conf.d/cloudflare-realip.conf,内容如下:

# /etc/nginx/conf.d/cloudflare-realip.conf
# 信任的 Cloudflare 回源网段,官方列表:https://www.cloudflare.com/ips/
# IPv4
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

# IPv6
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

# 从 Cloudflare 注入的头里取访客真实 IP
real_ip_header CF-Connecting-IP;
sudo nginx -t
sudo systemctl reload nginx

主流发行版的 nginx.conf 默认包含 include /etc/nginx/conf.d/*.conf;(http 块内),上述文件会自动生效;如果你的发行版没有,把这段内容放进任意 http 块级别的配置中即可。

工作原理是一条完整的信任链:

  1. real_ip 模块看到对端在 set_real_ip_from 白名单内,用 CF-Connecting-IP 的值覆写 $remote_addr,此时 $remote_addr 就是访客真实 IP。
  2. 原有 server 块里的 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 不用改,它把还原后的真实 IP 追加进 X-Forwarded-For 转发给 app。
  3. app 的直连对端是本机回环(127.0.0.1:8787),满足「内网/回环」条件,于是信任该头并取最右侧非内网条目,拿到的正是访客真实 IP。

验证:重载后访问站点,tail -f /var/log/nginx/access.log 里的来源 IP 应该是你自己的公网 IP,而不是 Cloudflare 网段的地址。

网段列表会变吗

Cloudflare 的回源网段多年保持稳定,但仍建议偶尔核对官方列表。可以用一条命令重新生成配置主体:

{ curl -s https://www.cloudflare.com/ips-v4; echo; curl -s https://www.cloudflare.com/ips-v6; } \
| sed '/^$/d; s/^/set_real_ip_from /; s/$/;/'

SSE 长连接:开箱即用,但别缓存 API

Aivory 的 AI 回复通过 SSE(Server-Sent Events)流式推送,没有 WebSocket。Cloudflare 代理对 SSE 的透传是开箱即用的,原因有两点:

  • 空闲超时:Cloudflare 代理连接的空闲超时约 100 秒,而 Aivory 服务端每 15 秒发一次 ping 心跳,连接永远不会被判定为空闲,长回答和深度研究任务不会被中途掐断。不需要任何额外配置
  • 缓冲:Cloudflare 对不进入缓存的响应不做整体缓冲,只要 API 响应不被缓存,token 就会实时到达浏览器。

因此在 Cloudflare 侧唯一要保证的就是:/api/* 不进缓存(下一节的 Cache Rule 处理)。源站 Nginx 侧的 proxy_buffering off 与放宽读超时仍然必须保留,见反向代理与 HTTPS

Cache Rules:API 绕过,静态资源放心缓存

Cloudflare 默认不缓存 text/html 与 API 这类动态响应,理论上不配规则也能正常工作。但显式声明可以杜绝日后误开「Cache Everything」之类全局规则时殃及 API。在 Caching → Cache Rules 中建两条规则:

规则匹配条件动作说明
API 绕过URI Path starts with /api/Bypass cache保证 SSE 与所有接口永不进缓存
静态资源URI Path starts with /assets/Eligible for cache,Edge TTL 设长(如 1 个月)构建产物文件名自带内容指纹,内容变了文件名就变,长缓存绝对安全

规则顺序:API 绕过放在前面(Cache Rules 按序匹配)。

面板开关:该关的和该开的

开关位置建议原因
Rocket LoaderSpeed → Optimization关闭它会重排脚本加载顺序,对 SPA 极易造成白屏或功能异常
Always Use HTTPSSSL/TLS → Edge Certificates开启所有 http:// 访问在边缘 301 到 https://
Auto Minify无需处理该功能已被 Cloudflare 下线,不用找了
Brotli 压缩Speed保持默认(开)默认已启用
HTTP/2 / HTTP/3Network保持默认(开)默认已启用
WebSocketsNetwork无关Aivory 流式走 SSE,不是 WebSocket,此开关开与关均不影响

请求体上限与「备份导入」绕行

Cloudflare 按套餐限制单个请求体大小:

套餐请求体上限
Free / Pro100MB
Business200MB
Enterprise500MB

对照 Aivory 的两类大请求体:

场景服务端默认上限走橙云是否受影响
普通文件/文档上传(MAX_UPLOAD_BYTES)50MB不受影响,Free 套餐即可覆盖
管理员备份导入(MAX_BACKUP_BYTES)20GiB会被 Cloudflare 拒绝,任何套餐都不够

管理员执行备份导入时归档可达 20GiB,必须绕开 Cloudflare 代理,任选其一:

  1. 内网直连(推荐):在服务器本机或内网直接访问 http://127.0.0.1:8787 的管理后台执行导入,完全不经过 Cloudflare 和源站 Nginx 的体积限制。
  2. 灰云子域:另建一条 DNS 记录如 direct.chat.example.com,Proxy status 设为 DNS only(灰色云朵),直连源站执行导入。源站 Nginx 需为该域名准备 server 块并把 client_max_body_size 调到 21g(参考反向代理与 HTTPS的大请求体一节)。
灰云子域会暴露源站 IP

DNS only 记录直接解析到源站真实地址,与「隐藏源站」的目标冲突。建议导入完成后删除该记录,或从一开始就用内网直连方案。

方案 B:Cloudflare Tunnel

Tunnel 由源站内的 cloudflared 进程主动向外连接 Cloudflare 边缘,流量经隧道回源。适合没有公网 IP(家宽 NAT、内网服务器)或不想开放任何入站端口的场景:防火墙可以完全封死入站,源站 IP 天然不暴露,也不再需要源站 Nginx 和证书。

第 1 步:在面板创建 Tunnel 拿 token

  1. 进入 Cloudflare 面板的 Zero Trust(首次使用需初始化,Free 计划即可)。
  2. Networks → Tunnels → Create a tunnel,连接器类型选 Cloudflared,起个名字(如 aivory)。
  3. 创建后页面会显示安装命令,其中包含一长串 token(eyJh... 开头)。只复制 token 本身,下一步以环境变量方式交给容器。

第 2 步:compose 增加 cloudflared 服务

deploy/docker-compose.prod.ymlservices 下追加(与 app 同级):

cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN:?请在 .env 中设置 Cloudflare Tunnel token}
networks:
- internal
depends_on:
app:
condition: service_healthy

.env 中加入:

TUNNEL_TOKEN=eyJh...你的token...

要点:

  • cloudflared 加入栈内已有的私有网络 internal,与 app 容器直接互通,回源不出宿主机。
  • Tunnel 是纯出站连接,cloudflared 不需要任何 ports 映射。
  • app 服务的 ports 可以从默认的 "80:8787" 收紧为 "127.0.0.1:8787:8787":公网流量全部走隧道,本机保留的 8787 直连专门留给管理员备份导入(见下)。
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml logs -f cloudflared # 看到 Registered tunnel connection 即连通

第 3 步:配置 Public hostname(ingress)

回到面板中该 Tunnel 的 Public Hostname 标签页,添加:

字段
Subdomainchat
Domainexample.com
TypeHTTP
URLapp:8787

即 ingress 指向 http://app:8787:cloudflaredapp 同在 internal 网络,直接用服务名互访。保存后 Cloudflare 会自动创建对应的橙云 DNS 记录,浏览器访问 https://chat.example.com 即可打开 Aivory。

Tunnel 模式下的真实 IP 与限流

Tunnel 模式不需要 Nginx,真实 IP 链路自动成立:Cloudflare 边缘把访客 IP 写入 X-Forwarded-ForCF-Connecting-IP,cloudflared 原样转发给 app;app 的直连对端是 cloudflared 容器的私网地址,满足信任条件,于是取 X-Forwarded-For 最右侧非内网条目,即访客真实 IP。部署完成后按收尾清单验证一次即可。

Tunnel 模式下的备份导入

隧道流量同样经过 Cloudflare 边缘,请求体上限与橙云完全一致(Free/Pro 100MB 起)。20GiB 级别的备份导入仍需绕行:用上一步保留的 127.0.0.1:8787 在服务器本机直连导入,这也是 compose 里建议保留该端口映射的原因。

两方案对比

维度方案 A:橙云代理方案 B:Cloudflare Tunnel
公网 IP / 开放端口需要,至少开放 443不需要,零入站端口
源站组件Nginx + 证书(Full strict)仅 cloudflared 容器
证书运维源站需真实证书(certbot 或 Origin CA)无,边缘证书由 Cloudflare 管理
源站隐藏橙云隐藏 DNS,建议再配防火墙白名单天然隐藏,防火墙可全封入站
真实 IP 还原需配 Nginx real_ip 模块自动成立,无需配置
SSE 流式正常(15s 心跳覆盖边缘超时)正常(同左)
请求体上限按 CF 套餐(Free 100MB)相同
备份导入内网直连或灰云子域本机直连 127.0.0.1:8787
故障面Nginx、证书续期、CF 三处cloudflared 进程与 CF 两处

一句话建议:有公网 IP 且已经在跑 Nginx,选方案 A;新部署、家宽或不想碰证书,选方案 B。

收尾核对清单

全部配完后逐项过一遍:

  • SSL/TLS 模式:方案 A 为 Full (strict)(或明确接受了 Flexible 的取舍且回源链路可信);方案 B 无需设置。
  • Always Use HTTPS 已开启,访问 http://chat.example.com 会 301 到 https。
  • Rocket Loader 已关闭。
  • Cache Rules:/api/* Bypass cache 规则存在且排在最前。
  • OAuth 回调(若启用 OAuth 登录):OAUTH_CALLBACK_BASE_URL 设为对外的 https:// 域名,OAuth 提供商侧回调地址已同步更新,详见 FAQ
  • SSE 流式正常:发一条消息,回复应逐字出现打字机效果。如果卡很久后整段蹦出,先检查源站 Nginx 是否遗漏 proxy_buffering off,再检查是否有缓存规则命中了 /api/*
  • 限流按真实 IP 生效:方案 A 看源站 Nginx access log,来源应为访客公网 IP 而非 Cloudflare 网段;方案 B 从两个不同网络(如手机流量与家庭宽带)访问,确认互不影响限流计数。若发现所有用户共享限流,回查 real_ip 配置或 X-Forwarded-For 转发。
  • 备份导入通道:确认保留了内网直连入口(127.0.0.1:8787)或已了解灰云子域做法,不要在需要恢复数据的当口才发现 100MB 上限。
  • 健康检查(如使用外部拨测):对根路径 GET / 返回 200 即代表存活。

排障时的通用思路:先绕过 Cloudflare 直连源站(本机 curl http://127.0.0.1:8787/)确认应用本身正常,再逐层往外排查 Nginx、Cloudflare 面板配置。