反向代理與 HTTPS
Aivory 的 app 容器在 8787 埠上同源伺服前端 SPA 和 /api 後端,本身只講 HTTP。生產環境建議在它前面放一層反向代理負責 TLS 終止。本頁給出 Nginx 和 Caddy 兩套可以直接貼上的完整配置,並解釋每一項為什麼這麼寫。
前端和 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 心跳,防止中間代理因空閒而掐斷連線。反代側要做對兩件事:
- 關閉響應緩衝:代理若緩衝響應,token 會攢成一大塊再吐給瀏覽器,打字機效果直接消失,表現為"卡很久然後整段蹦出來"。
- 放寬讀超時:一次深度研究或長工具鏈回答可能持續幾十分鐘,讀超時給足餘量。
下面兩套配置都已包含這些處理。
方案一: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
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 要分別對待:
| 場景 | 服務端上限 | 對應環境變數 | 建議 |
|---|---|---|---|
| 普通檔案/文件上傳 | 預設 50MB | MAX_UPLOAD_BYTES | client_max_body_size 100m 已覆蓋 |
| 管理員備份匯入 | 預設 20GiB | MAX_BACKUP_BYTES | 見下方兩種做法 |
管理員在後臺執行備份匯入時,歸檔檔案可達 20GiB,遠超 100m。兩種做法:
- 臨時調大反代限制:把
client_max_body_size改為21g並nginx -s reload,匯入完成後改回。大檔案場景建議同時加proxy_request_buffering off,讓 Nginx 邊收邊轉發,避免先把整個歸檔緩衝到本地磁碟。 - 內網直連(推薦):在伺服器本機或內網直接訪問
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。
這條規則對運維的含義:
如果反代不設定 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 下開放的加密介面。適合純內網、無法申請證書的場景。
公網明文 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 接入 |