Skip to main content

反向代理与 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 接入