单端口部署 FastGPT 与 MinIO:从签名错误、CORS 到文件读取 404 的完整排障实录
本文记录一次 FastGPT v4.14.4 与 MinIO 共用 Nginx 单端口部署时的完整排障过程。最终目标是:服务器只对外开放一个端口,同时保证 FastGPT 页面、文件上传和文件解析都能正常工作。
一、部署背景
部署环境存在以下限制:
- FastGPT、MinIO、MongoDB、PostgreSQL 等组件通过 Docker Compose 运行;
- Nginx 与 Docker 位于同一台服务器;
- 对外只开放
6080端口; - FastGPT 页面通过
http://chat.example.com:6080访问; - MinIO API 不能直接暴露到公网;
- 需要支持浏览器直传文件,并让 FastGPT 在问答过程中读取文件内容。
最终采用的流量结构如下:
这里没有采用 /minio/... 作为对象存储路径,而是直接使用 S3 Path Style 的 Bucket 路径:
/fastgpt-public/...
/fastgpt-private/...
二、为什么不能简单使用 /minio 前缀
最初的外部地址配置为:
S3_EXTERNAL_BASE_URL: "http://10.0.0.10:6080/minio"
但 FastGPT v4.14.4 创建 MinIO 外部客户端时只读取 URL 的协议、主机名和端口,并不会将 pathname 作为 S3 基础路径使用。核心逻辑可以简化为:
const externalBaseURL = new URL(options.externalBaseURL);
new Client({
endPoint: externalBaseURL.hostname,
port: Number(externalBaseURL.port),
useSSL: externalBaseURL.protocol === 'https:'
});
因此 /minio 不是可靠的 S3 API 前缀。若再由 Nginx增加或删除该路径,预签名 GET/PUT 的 canonical path 还可能发生变化,最终导致:
SignatureDoesNotMatch
正确做法是让外部地址只包含协议、域名和端口:
S3_EXTERNAL_BASE_URL: "http://chat.example.com:6080"
然后在 Nginx 中根据 Bucket 名称路由到 MinIO。
相关源码:S3BaseBucket
三、问题一:预签名请求签名不一致
最早出现的错误是:
The request signature we calculated does not match the signature you provided.
这类问题通常由以下因素造成:
- FastGPT 与 MinIO 的 Access Key、Secret Key 不一致;
- Region、协议、端口或 endpoint 配置不一致;
- 反向代理修改了参与签名的 Host 或请求路径;
- 使用了临时凭据,却没有传递 session token;
- 容器系统时钟错误。
需要特别注意:将容器时区改为 Asia/Shanghai 只会改变时间显示方式,不会修正系统时钟,也不会直接修复签名。
本次部署统一使用以下内部配置:
S3_ENDPOINT: "fastgpt-minio"
S3_PORT: "9000"
S3_USE_SSL: "false"
S3_ACCESS_KEY: "${S3_ACCESS_KEY}"
S3_SECRET_KEY: "${S3_SECRET_KEY}"
S3_PUBLIC_BUCKET: "fastgpt-public"
S3_PRIVATE_BUCKET: "fastgpt-private"
同时,Nginx 转发 MinIO 请求时使用 $http_host,保留客户端请求中的端口:
proxy_set_header Host $http_host;
相比之下,$host 通常不包含非标准端口,可能导致预签名请求中的 Host 与 MinIO 实际收到的 Host 不一致。
四、问题二:浏览器触发 Private Network Access 拦截
文件上传地址最初是:
http://10.0.0.10:6080/fastgpt-private
而 FastGPT 页面通过以下地址访问:
http://chat.example.com:6080
浏览器报错:
The request client is not a secure context and the resource is in more-private address space local.
虽然两个地址最终指向同一台服务器,但浏览器判断 Origin 时使用的是“协议 + 主机名 + 端口”。域名和 IP 不同,因此它们属于不同源;同时,页面通过普通 HTTP 访问更私有的内网 IP,触发了 Chrome Private Network Access 限制。
解决方法是让 FastGPT 页面和上传 URL 使用完全相同的 Origin:
S3_EXTERNAL_BASE_URL: "http://chat.example.com:6080"
FE_DOMAIN: "http://chat.example.com:6080"
这样上传 URL 会变成:
http://chat.example.com:6080/fastgpt-private
浏览器不再进行跨域访问,也不需要额外添加宽泛的 CORS 响应头。
五、问题三:FastGPT 容器无法解析外部域名
统一域名后,生成预签名地址时又出现:
getaddrinfo EAI_AGAIN chat.example.com
FastGPT v4.14.4 会为 S3_EXTERNAL_BASE_URL 创建一个外部 MinIO Client。生成 POST 或 GET 预签名 URL时,这个客户端可能需要通过外部域名查询 Bucket Region。因此,域名不仅要能被浏览器解析,也必须能被 FastGPT 容器解析。
对于较新的 Docker,可以使用:
extra_hosts:
- "chat.example.com:host-gateway"
但旧版本 Docker/docker-compose 会报错:
invalid IP address in add-host: "host-gateway"
兼容旧版本的方案是直接映射宿主机内网地址:
extra_hosts:
- "chat.example.com:10.0.0.10"
该配置需要同时添加到可能创建或读取 S3 外部 URL 的服务:
services:
fastgpt:
extra_hosts:
- "chat.example.com:10.0.0.10"
fastgpt-plugin:
extra_hosts:
- "chat.example.com:10.0.0.10"
容器重新创建后可以验证:
docker exec fastgpt grep chat.example.com /etc/hosts
预期结果类似:
10.0.0.10 chat.example.com
六、问题四:上传成功,但文件内容变成 404
解决上传问题后,浏览器已经可以成功执行:
POST http://chat.example.com:6080/fastgpt-private
预签名接口也正常返回 code: 200、对象 Key、Policy 和签名。但在问答中要求模型读取文件时,模型拿到的内容却是:
Request failed with status code 404
这并不是中文编码乱码,也不是模型幻觉,而是 FastGPT 将文件下载错误文本当成了文件正文。
6.1 源码级根因
FastGPT v4.14.4 的文件读取流程中存在以下逻辑:
const parsedURL = new URL(url, 'http://localhost:3000');
if (requestOrigin && parsedURL.origin === requestOrigin) {
url = url.replace(requestOrigin, '');
}
const response = await axios.get(url, {
baseURL: serverRequestBaseUrl,
responseType: 'arraybuffer'
});
当页面 Origin 与 MinIO 外部地址相同时:
requestOrigin = http://chat.example.com:6080
fileUrl = http://chat.example.com:6080/fastgpt-private/chat/...
FastGPT 会把完整 URL 改成:
/fastgpt-private/chat/...
接着 Axios 使用 FastGPT 自身的本地地址作为 baseURL,实际请求变成:
http://<fastgpt-container>:3000/fastgpt-private/chat/...
但 FastGPT 的 3000 端口并没有 /fastgpt-private 路由,于是返回 404。异常捕获逻辑又会把错误文本格式化为文件内容,最终模型看到的就是:
Request failed with status code 404
相关源码:readFiles.ts
6.2 Nginx 兼容修复
在不重新编译 FastGPT 的情况下,可以仅对转发到 FastGPT 的请求清除 Origin:
location / {
proxy_pass http://127.0.0.1:8085;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
proxy_set_header X-Forwarded-Host $http_host;
# 避免 FastGPT v4.14.4 将同源 S3 URL 改写为本地相对路径
proxy_set_header Origin "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
注意不要在 MinIO 的两个 location 中清除 Origin。浏览器上传请求仍应原样转发给 MinIO。
清除 FastGPT 上游请求的 Origin 是针对 v4.14.4 同源 S3 部署的兼容方案。长期方案应优先考虑:
- 升级到已确认修复该逻辑的 FastGPT 版本;
- 修改源码,使 S3 Bucket 路径不参与同源 URL 的相对化处理;
- 为 MinIO 使用独立文件域名,并正确配置 HTTPS 与 CORS。
七、最终 Docker Compose 核心配置
以下配置假设 Nginx 与 Docker 在同一台服务器:
version: "3.3"
x-share-db-config: &x-share-db-config
S3_EXTERNAL_BASE_URL: "http://chat.example.com:6080"
S3_ENDPOINT: "fastgpt-minio"
S3_PORT: "9000"
S3_USE_SSL: "false"
S3_ACCESS_KEY: "${S3_ACCESS_KEY}"
S3_SECRET_KEY: "${S3_SECRET_KEY}"
S3_PUBLIC_BUCKET: "fastgpt-public"
S3_PRIVATE_BUCKET: "fastgpt-private"
services:
fastgpt-minio:
image: minio/minio:<固定版本>
restart: always
ports:
- "127.0.0.1:3001:9000"
- "127.0.0.1:3002:9001"
environment:
TZ: Asia/Shanghai
MINIO_ROOT_USER: "${S3_ACCESS_KEY}"
MINIO_ROOT_PASSWORD: "${S3_SECRET_KEY}"
volumes:
- ./fastgpt-minio:/data
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
fastgpt:
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.14.4
restart: always
ports:
- "127.0.0.1:8085:3000"
extra_hosts:
- "chat.example.com:10.0.0.10"
environment:
<<: *x-share-db-config
TZ: Asia/Shanghai
FE_DOMAIN: "http://chat.example.com:6080"
depends_on:
fastgpt-minio:
condition: service_healthy
fastgpt-plugin:
image: fastgpt-plugin:<固定版本>
restart: always
extra_hosts:
- "chat.example.com:10.0.0.10"
environment:
<<: *x-share-db-config
TZ: Asia/Shanghai
depends_on:
fastgpt-minio:
condition: service_healthy
将后端端口绑定到 127.0.0.1,可以避免 Docker 将 3001、3002 和 8085 暴露到所有网卡。部分 Linux 防火墙配置可能被 Docker 的 NAT 规则绕过,因此不能只依赖外层防火墙来隐藏这些端口。
八、最终 Nginx 6080 配置
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 6080 default_server;
server_name chat.example.com 10.0.0.10;
proxy_connect_timeout 60s;
proxy_read_timeout 900s;
proxy_send_timeout 900s;
client_max_body_size 100m;
location /fastgpt-public {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
proxy_set_header X-Forwarded-Host $http_host;
proxy_request_buffering off;
proxy_buffering off;
}
location /fastgpt-private {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
proxy_set_header X-Forwarded-Host $http_host;
proxy_request_buffering off;
proxy_buffering off;
}
location / {
proxy_pass http://127.0.0.1:8085;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
proxy_set_header X-Forwarded-Host $http_host;
# FastGPT v4.14.4 同源 S3 读取兼容处理
proxy_set_header Origin "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
这段配置只展示 6080 服务,不需要修改服务器已有的其他端口配置。
九、部署与验证
9.1 校验并重建容器
docker-compose -f docker-compose.yml config
docker-compose -f docker-compose.yml up -d --force-recreate fastgpt fastgpt-plugin
如果修改了 MinIO 端口映射,则应重建全部相关服务:
docker-compose -f docker-compose.yml up -d --force-recreate
9.2 验证容器内域名解析
docker exec fastgpt grep chat.example.com /etc/hosts
9.3 校验并加载 Nginx
nginx -t
nginx -s reload
9.4 验证请求链路
FastGPT 首页:
curl -I http://chat.example.com:6080/
MinIO 内部健康状态:
curl -i http://127.0.0.1:3001/minio/health/live
私有 Bucket 未签名访问通常会返回 MinIO XML 格式的 AccessDenied,这反而说明路由已经到达 MinIO:
curl -i http://chat.example.com:6080/fastgpt-private
完整业务验证应满足:
POST /api/core/chat/presignChatFilePostUrl 200
POST /fastgpt-private 204 或 200
GET /fastgpt-private/chat/<object-key>?X-Amz-* 200
如果最后一个 GET 返回 404,检查它是否被转发到了 FastGPT;如果返回 S3 XML 错误,则检查 Bucket、Key、签名 Host 和请求路径。
十、故障现象速查表
| 现象 | 主要原因 | 检查重点 |
|---|---|---|
SignatureDoesNotMatch |
凭据、Host、Region 或路径不一致 | Access Key、Secret Key、$http_host、endpoint |
| 浏览器 PNA/CORS 拦截 | 页面使用域名,上传使用内网 IP | FE_DOMAIN 与 S3_EXTERNAL_BASE_URL 是否同源 |
EAI_AGAIN |
FastGPT 容器无法解析外部域名 | 容器 DNS、extra_hosts |
invalid IP address: host-gateway |
Docker 版本过旧 | 使用明确的宿主机内网 IP |
| 上传成功、读取内容为 404 | v4.14.4 将同源 S3 URL 改成 FastGPT 本地路径 | Nginx FastGPT 路由清除上游 Origin |
| 大文件返回 413 | Nginx 默认请求体限制过小 | client_max_body_size |
十一、生产环境建议
- 启用 HTTPS:普通 HTTP 会明文传输登录令牌、文件内容和预签名参数。即使只能开放一个端口,也可以直接在该端口终止 TLS。
- 不要使用默认密码:将 S3、MongoDB、Redis、FastGPT Root Key 等凭据放入受保护的环境变量或 Secret 管理系统。
- 限制后端端口:Nginx 与 Docker 同机时,将 FastGPT 和 MinIO 映射到
127.0.0.1。 - 固定镜像版本:不要在生产环境使用浮动标签,升级前先验证文件上传和解析链路。
- 谨慎使用 Origin 兼容方案:清除 Origin 是针对 FastGPT v4.14.4 同源 S3 场景的工程性修复。升级版本后应重新验证是否还需要该配置。
- 保留访问日志:文件上传问题应同时查看浏览器 Network、FastGPT 日志、Nginx access log 和 MinIO 日志,不能只依赖前端报错。
十二、总结
单端口同时承载 FastGPT 和 MinIO 的关键,不是增加一个任意的 /minio 前缀,而是保持 S3 请求路径和 Host 不变,并按 Bucket 路径进行路由:
/fastgpt-public → MinIO
/fastgpt-private → MinIO
/ → FastGPT
整个故障链路体现了四个容易混淆的层次:
- S3 签名关注 endpoint、Host、Region 和 canonical path;
- 浏览器关注 Origin、CORS 和 Private Network Access;
- Docker 容器有独立的 DNS 与网络视角;
- FastGPT v4.14.4 的同源 URL 优化会把 S3 文件错误地指向自身服务。
只有将浏览器、Nginx、容器 DNS、FastGPT 外部 S3 Client 和 MinIO 实际路径放在同一条请求链路中分析,才能解释为什么“预签名成功”“上传成功”和“文件读取成功”是三个彼此独立的阶段。
更多推荐


所有评论(0)