本文记录一次 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 在问答过程中读取文件内容。

最终采用的流量结构如下:

/fastgpt-public/*

/fastgpt-private/*

其他路径

S3 内部管理请求

预签名 URL 文件读取

浏览器
chat.example.com:6080

Nginx :6080

MinIO API
127.0.0.1:3001

FastGPT
127.0.0.1:8085

fastgpt-minio:9000

这里没有采用 /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.

这类问题通常由以下因素造成:

  1. FastGPT 与 MinIO 的 Access Key、Secret Key 不一致;
  2. Region、协议、端口或 endpoint 配置不一致;
  3. 反向代理修改了参与签名的 Host 或请求路径;
  4. 使用了临时凭据,却没有传递 session token;
  5. 容器系统时钟错误。

需要特别注意:将容器时区改为 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 部署的兼容方案。长期方案应优先考虑:

  1. 升级到已确认修复该逻辑的 FastGPT 版本;
  2. 修改源码,使 S3 Bucket 路径不参与同源 URL 的相对化处理;
  3. 为 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_DOMAINS3_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

十一、生产环境建议

  1. 启用 HTTPS:普通 HTTP 会明文传输登录令牌、文件内容和预签名参数。即使只能开放一个端口,也可以直接在该端口终止 TLS。
  2. 不要使用默认密码:将 S3、MongoDB、Redis、FastGPT Root Key 等凭据放入受保护的环境变量或 Secret 管理系统。
  3. 限制后端端口:Nginx 与 Docker 同机时,将 FastGPT 和 MinIO 映射到 127.0.0.1
  4. 固定镜像版本:不要在生产环境使用浮动标签,升级前先验证文件上传和解析链路。
  5. 谨慎使用 Origin 兼容方案:清除 Origin 是针对 FastGPT v4.14.4 同源 S3 场景的工程性修复。升级版本后应重新验证是否还需要该配置。
  6. 保留访问日志:文件上传问题应同时查看浏览器 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 实际路径放在同一条请求链路中分析,才能解释为什么“预签名成功”“上传成功”和“文件读取成功”是三个彼此独立的阶段。

更多推荐