跳至主要内容

反向代理與 HTTPS

Aivory 的 app 容器在 8787 埠上同源伺服前端 SPA 和 /api 後端,本身只講 HTTP。生產環境建議在它前面放一層反向代理負責 TLS 終止。本頁給出 Nginx 和 Caddy 兩套可以直接貼上的完整配置,並解釋每一項為什麼這麼寫。

為什麼不需要配置 CORS

前端和 API 由同一個程序、同一個埠伺服,瀏覽器視角下二者永遠同源。用哪個域名訪問,哪個域名就能用,不需要 PUBLIC_ORIGIN、不需要按域名配置 CORS 白名單。ALLOWED_ORIGINS 只在你把前端拆到與 API 不同源的部署形態下才有意義,單容器部署用不到。

開始之前:調整埠對映

快速部署的 compose 檔案預設把 app 對映到宿主機 80 埠("80:8787")。在同一臺機器上加裝 Nginx/Caddy 時,80/443 要讓給反代,把 app 改成只監聽本機迴環:

# deploy/docker-compose.prod.yml 中 app 服務的 ports 段
ports:
- "127.0.0.1:8787:8787"

改完執行 docker compose -f docker-compose.prod.yml up -d 重建 app 容器。之後反代統一轉發到 http://127.0.0.1:8787

健康檢查

對根路徑發 GET / 返回 200 即代表存活,可直接用作反代或雲負載均衡的健康檢查探測路徑。

關鍵前提:流式輸出走 SSE

Aivory 的 AI 回覆通過 SSE(Server-Sent Events)流式推送,沒有 WebSocket,因此不需要任何 Upgrade 相關配置。服務端每 15 秒會發送一次 ping 心跳,防止中間代理因空閒而掐斷連線。反代側要做對兩件事:

  1. 關閉響應緩衝:代理若緩衝響應,token 會攢成一大塊再吐給瀏覽器,打字機效果直接消失,表現為"卡很久然後整段蹦出來"。
  2. 放寬讀超時:一次深度研究或長工具鏈回答可能持續幾十分鐘,讀超時給足餘量。

下面兩套配置都已包含這些處理。

方案一:Nginx

完整配置

儲存為 /etc/nginx/sites-available/aivory.conf,替換 chat.example.com 為你的域名:

# HTTP:僅用於 certbot 驗證與跳轉 HTTPS
server {
listen 80;
listen [::]:80;
server_name chat.example.com;

# certbot webroot 驗證路徑(用 --nginx 外掛時可省略)
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}

# HTTPS 主站
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # nginx < 1.25.1 請刪除此行,改為在上面兩行 listen 末尾追加 http2
server_name chat.example.com;

ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;

# 普通上傳預設上限 50MB(MAX_UPLOAD_BYTES),此處留出餘量。
# 管理員備份匯入可達 20GiB,見下文「大請求體」一節。
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 與協議 ---
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

啟用並重載:

sudo ln -s /etc/nginx/sites-available/aivory.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

逐項說明

指令作用
proxy_pass http://127.0.0.1:8787轉發到 app 容器,SPA 和 /api 走同一個上游,不需要分 location
proxy_buffering off關閉響應緩衝,SSE 的每個事件立即透傳給瀏覽器,否則打字機效果消失
proxy_read_timeout 3600s讀超時放寬到 1 小時。服務端 15 秒一次的心跳能覆蓋大多數空閒場景,這裡是額外兜底,避免超長回答被代理掐斷
proxy_http_version 1.1上游使用 HTTP/1.1,支援長連線,是流式轉發的前提
proxy_set_header Connection ""清空 Connection 頭,保持與上游的 keepalive,避免被降級為短連線
client_max_body_size 100m請求體上限。Nginx 預設僅 1MB,不調大會導致上傳檔案直接返回 413
proxy_set_header Host $host透傳原始域名。單容器同源架構下任何域名都能用,只要 Host 被正確轉發(所有反代預設都會)
X-Real-IP / X-Forwarded-For傳遞真實客戶端 IP,與限流直接相關,見下文專節
X-Forwarded-Proto $scheme告知後端外層是 HTTPS

用 certbot 申請證書

# Debian / Ubuntu
sudo apt install certbot python3-certbot-nginx

# 自動修改 Nginx 配置並簽發證書
sudo certbot --nginx -d chat.example.com

# 驗證自動續期
sudo certbot renew --dry-run

--nginx 外掛會自動完成域名驗證、寫入 ssl_certificate 路徑並設定續期定時任務。如果你希望完全手工控制配置檔案,改用 webroot 模式:

sudo mkdir -p /var/www/certbot
sudo certbot certonly --webroot -w /var/www/certbot -d chat.example.com

簽發成功後證書位於 /etc/letsencrypt/live/chat.example.com/,與上面配置中的路徑一致。

方案二:Caddy

Caddy 自動申請並續期 Let's Encrypt 證書,自動 HTTP 跳 HTTPS,Caddyfile 只需三行:

chat.example.com {
reverse_proxy 127.0.0.1:8787
}
sudo systemctl reload caddy
關於 flush_interval

Caddy v2 檢測到 Content-Type: text/event-stream 時會自動停用響應緩衝,所以上面三行通常開箱即用。若你的版本較舊或疊加了其它會引入緩衝的中間層,可顯式關閉:

chat.example.com {
reverse_proxy 127.0.0.1:8787 {
flush_interval -1
}
}

flush_interval -1 表示每收到一個位元組立即刷給客戶端,等價於 Nginx 的 proxy_buffering off

Caddy 預設不限制請求體大小,也預設透傳 X-Forwarded-For,因此檔案上傳、備份匯入和真實 IP 都不需要額外配置。

大請求體:上傳與備份匯入

Aivory 有兩類大請求體,反代的 client_max_body_size 要分別對待:

場景服務端上限對應環境變數建議
普通檔案/文件上傳預設 50MBMAX_UPLOAD_BYTESclient_max_body_size 100m 已覆蓋
管理員備份匯入預設 20GiBMAX_BACKUP_BYTES見下方兩種做法

管理員在後臺執行備份匯入時,歸檔檔案可達 20GiB,遠超 100m。兩種做法:

  1. 臨時調大反代限制:把 client_max_body_size 改為 21gnginx -s reload,匯入完成後改回。大檔案場景建議同時加 proxy_request_buffering off,讓 Nginx 邊收邊轉發,避免先把整個歸檔緩衝到本地磁碟。
  2. 內網直連(推薦):在伺服器本機或內網直接訪問 http://127.0.0.1:8787 執行匯入,完全繞開反代的體積與超時限制。

服務端自身的上限由 MAX_UPLOAD_BYTES / MAX_BACKUP_BYTES 控制,詳見進階環境變數

真實 IP 與限流

Aivory 按客戶端 IP 維護限流計數(計數器存放在 Redis)。客戶端 IP 的判定規則是:

  • 僅當直連對端是內網或迴環地址(也就是請求確實來自你部署的反代)時,才信任 X-Forwarded-For / X-Real-IP,並取 X-Forwarded-For最右側的非內網條目作為真實 IP。
  • 直連對端是公網地址時,這些頭一律忽略,直接採用 TCP 對端地址。因此公網攻擊者偽造 X-Forwarded-For 無法冒充他人 IP。

這條規則對運維的含義:

反代必須正確追加 X-Forwarded-For

如果反代不設定 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for,後端看到的所有請求都來自反代自己的 IP。全站使用者會共享同一份限流計數:一個人觸發限流,所有人一起被限。上面的 Nginx 配置已包含正確寫法,Caddy 預設行為即正確。

Cloudflare 場景:流量路徑變成 訪客 -> Cloudflare -> 源站 Nginx -> app,源站 Nginx 看到的對端是 Cloudflare 節點。需要用 Nginx 的 real_ip 模組,基於 Cloudflare 下發的 CF-Connecting-IP 頭先還原真實訪客 IP,再進入上面的轉發鏈路:

# 在 http 或 server 塊中宣告信任的 Cloudflare 回源網段(節選,完整列表見 Cloudflare 官方釋出)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# ...其餘 Cloudflare 網段...
real_ip_header CF-Connecting-IP;

這樣 $remote_addr 就是真實訪客 IP,$proxy_add_x_forwarded_for 追加的也是正確的值。完整的網段列表維護與逐步配置見 Cloudflare 接入

可以不上 HTTPS 嗎

可以但不建議。Aivory 在非安全上下文(純 HTTP)下依然完整可用:應用內的請求籤名演算法帶有純 JS 回退實現,不依賴瀏覽器僅在 HTTPS 下開放的加密介面。適合純內網、無法申請證書的場景。

強烈建議啟用 HTTPS

公網明文 HTTP 意味著登入憑證、對話內容全部裸奔在鏈路上,任何中間節點都可竊聽或篡改。只要有域名,用上面任意一套配置幾分鐘即可上線 HTTPS。

常見問題排查

現象原因處理
回答不流式,卡頓後整段出現反代開著響應緩衝Nginx 確認 proxy_buffering off;檢查中間是否還有別的緩衝層(如某些 CDN)
長回答中途斷開讀超時過短確認 proxy_read_timeout 3600s;若經過 CDN,同步檢查 CDN 的空閒超時
上傳檔案報 413反代請求體上限太小調大 client_max_body_size
備份匯入失敗歸檔超過反代上限臨時調大到 21g,或內網直連 8787 埠匯入
所有使用者同時被限流X-Forwarded-For 未正確追加按上文補齊 proxy_set_header 兩行
用了 Cloudflare 後限流按 CF 節點 IP 計源站未還原真實 IP配置 real_ip 模組 + CF-Connecting-IP,見 Cloudflare 接入