Cloudflare 代理配置
把 Aivory 挂到 Cloudflare 后面,可以一次拿到三样东西:隐藏源站 IP(访客只能看到 Cloudflare 边缘节点的地址)、免费且自动续期的边缘证书(浏览器到 Cloudflare 这一段的 HTTPS 不用你操心)、以及 Cloudflare 的 DDoS 缓解与基础 WAF。附带的好处还有全球 Anycast 就近接入和静态资源边缘缓存。
本页给出两条完整路线:
- 方案 A:橙云代理。源站有公网 IP,DNS 记录开启 Cloudflare 代理(橙色云朵),流量路径为「访客 → Cloudflare → 源站 Nginx → app」。
- 方案 B:Cloudflare Tunnel。源站没有公网 IP 或不想开放任何入站端口,由
cloudflared容器主动向 Cloudflare 建立出站隧道。
两条路线都建立在同一个事实上:Aivory 的 app 容器单容器同源伺服 SPA 和 /api,用哪个域名访问,哪个域名就能用,不需要为 Cloudflare 修改 ALLOWED_ORIGINS 或任何域名配置(该变量只在前后端分离部署时才有意义)。
前置条件:域名接入 Cloudflare
- 注册 Cloudflare 账号,在面板点击 Add a domain,输入你的域名(如
example.com),选择 Free 套餐即可。 - Cloudflare 会扫描现有 DNS 记录并给出两个 NS 服务器地址,到你的域名注册商处把 Nameserver 改成这两个地址。
- 等待状态变为 Active(通常几分钟到几小时),之后该域名的 DNS 与代理都在 Cloudflare 面板管理。
同时确认 Aivory 侧的基础部署已完成:Docker Compose 生产部署已跑通,方案 A 还要求已按反向代理与 HTTPS在源站装好 Nginx。
方案 A:橙云代理
第 1 步:DNS 记录开启橙云
在 Cloudflare 面板 DNS → Records 中添加(或修改)指向源站的记录,并把 Proxy status 设为 Proxied(橙色云朵):
| Type | Name | Content | Proxy status |
|---|---|---|---|
| A | chat | 源站 IPv4 地址 | Proxied(橙云) |
| AAAA | chat | 源站 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 到源站」这一段怎么走:
| 模式 | 浏览器 → Cloudflare | Cloudflare → 源站 | 源站要求 | 是否推荐 |
|---|---|---|---|---|
| Flexible | HTTPS | HTTP 明文 | 源站只需监听 80,无需证书 | 仅内网/可信链路 |
| Full | HTTPS | HTTPS,但不校验证书 | 任意证书(含自签名) | 过渡用 |
| Full (strict) | HTTPS | HTTPS,校验证书有效性 | 受信任的真实证书 | 推荐 |
推荐 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)。
反向代理与 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 模式下源站链路是 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 块级别的配置中即可。
工作原理是一条完整的信任链:
- real_ip 模块看到对端在
set_real_ip_from白名单内,用CF-Connecting-IP的值覆写$remote_addr,此时$remote_addr就是访客真实 IP。 - 原有 server 块里的
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;不用改,它把还原后的真实 IP 追加进X-Forwarded-For转发给 app。 - 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 Loader | Speed → Optimization | 关闭 | 它会重排脚本加载顺序,对 SPA 极易造成白屏或功能异常 |
| Always Use HTTPS | SSL/TLS → Edge Certificates | 开启 | 所有 http:// 访问在边缘 301 到 https:// |
| Auto Minify | 无 | 无需处理 | 该功能已被 Cloudflare 下线,不用找了 |
| Brotli 压缩 | Speed | 保持默认(开) | 默认已启用 |
| HTTP/2 / HTTP/3 | Network | 保持默认(开) | 默认已启用 |
| WebSockets | Network | 无关 | Aivory 流式走 SSE,不是 WebSocket,此开关开与关均不影响 |
请求体上限与「备份导入」绕行
Cloudflare 按套餐限制单个请求体大小:
| 套餐 | 请求体上限 |
|---|---|
| Free / Pro | 100MB |
| Business | 200MB |
| Enterprise | 500MB |
对照 Aivory 的两类大请求体:
| 场景 | 服务端默认上限 | 走橙云是否受影响 |
|---|---|---|
普通文件/文档上传(MAX_UPLOAD_BYTES) | 50MB | 不受影响,Free 套餐即可覆盖 |
管理员备份导入(MAX_BACKUP_BYTES) | 20GiB | 会被 Cloudflare 拒绝,任何套餐都不够 |
管理员执行备份导入时归档可达 20GiB,必须绕开 Cloudflare 代理,任选其一:
- 内网直连(推荐):在服务器本机或内网直接访问
http://127.0.0.1:8787的管理后台执行导入,完全不经过 Cloudflare 和源站 Nginx 的体积限制。 - 灰云子域:另建一条 DNS 记录如
direct.chat.example.com,Proxy status 设为 DNS only(灰色云朵),直连源站执行导入。源站 Nginx 需为该域名准备 server 块并把client_max_body_size调到21g(参考反向代理与 HTTPS的大请求体一节)。
DNS only 记录直接解析到源站真实地址,与「隐藏源站」的目标冲突。建议导入完成后删除该记录,或从一开始就用内网直连方案。
方案 B:Cloudflare Tunnel
Tunnel 由源站内的 cloudflared 进程主动向外连接 Cloudflare 边缘,流量经隧道回源。适合没有公网 IP(家宽 NAT、内网服务器)或不想开放任何入站端口的场景:防火墙可以完全封死入站,源站 IP 天然不暴露,也不再需要源站 Nginx 和证书。
第 1 步:在面板创建 Tunnel 拿 token
- 进入 Cloudflare 面板的 Zero Trust(首次使用需初始化,Free 计划即可)。
- Networks → Tunnels → Create a tunnel,连接器类型选 Cloudflared,起个名字(如
aivory)。 - 创建后页面会显示安装命令,其中包含一长串 token(
eyJh...开头)。只复制 token 本身,下一步以环境变量方式交给容器。
第 2 步:compose 增加 cloudflared 服务
在 deploy/docker-compose.prod.yml 的 services 下追加(与 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 标签页,添加:
| 字段 | 值 |
|---|---|
| Subdomain | chat |
| Domain | example.com |
| Type | HTTP |
| URL | app:8787 |
即 ingress 指向 http://app:8787:cloudflared 与 app 同在 internal 网络,直接用服务名互访。保存后 Cloudflare 会自动创建对应的橙云 DNS 记录,浏览器访问 https://chat.example.com 即可打开 Aivory。
Tunnel 模式下的真实 IP 与限流
Tunnel 模式不需要 Nginx,真实 IP 链路自动成立:Cloudflare 边缘把访客 IP 写入 X-Forwarded-For 与 CF-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 面板配置。