跳至主要内容

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 面板配置。