跳转至

FastAPI 单端口部署方案

通过 FastAPI 同时托管 API 与前端静态文件,实现单端口对外服务。

需求背景

Dad 要求将 Agentic BI 项目"开放到 18816 端口给外网看",前后端都必须可访问。此前多个项目(Portal、ClawCraft、Gateway Admin)反复出现前后端分离部署导致的问题:

  • pm2 serve dist 只能返回静态文件,无法处理 /api/*
  • 多份源码目录(admin/admin-new/)导致部署错位
  • SPA fallback 配置不当导致旧 JS 请求返回 index.html
  • Caddy 多端口反代中 WS 端口配错导致 502

核心方案

from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

app = FastAPI()
dist_dir = Path("dist")

# API 路由(优先匹配)
@app.get("/api/health")
async def health():
    return {"ok": True}

# 静态资源
app.mount("/assets", StaticFiles(directory=dist_dir / "assets"), name="assets")

# SPA fallback(最后匹配)
@app.get("/{full_path:path}")
async def spa_fallback(full_path: str):
    file_path = dist_dir / full_path
    if file_path.exists() and file_path.is_file():
        return FileResponse(file_path)
    return FileResponse(dist_dir / "index.html")

路由优先级/api/* → 真实静态文件 → SPA fallback

演进时间线

日期 事件 端口
03-10 FastAPI 单端口方案首次落地 18816
03-10 修复 SSE 前端解析 bug(\r\n 换行符) 18816
03-11 接入 Caddy 域名反代 ademo.dora.restry.cn 28880
03-18 开发环境确认单端口结构 18816(前端)+ 18817(API)
03-20 从 pm2 serve 演进到统一应用托管
03-23 多端口反代问题再次暴露

Dad 的关键决策

  1. 统一端口对外:前后端必须从同一端口访问
  2. 开发/调试端口分离:Vite dev server 迁移到 4000 段,避免与 pm2 站点冲突
  3. 接入 Caddy:从裸端口升级为域名反代
  4. pm2 serve 改打包版本:开发环境不直接连内部调试地址

踩过的坑

SPA fallback 吞静态资源

try_files {path} /index.html

Caddy 的这条配置会让旧 JS 文件请求也返回 200 + index.html,浏览器拿 HTML 当 JS 执行,出现黑屏/刷新/报错。

教训:必须区分真实静态文件和前端路由 fallback。

SSE 流解析失败

单端口方案上线后,前端只显示"Agent团队正在分析"但无结果。根因是 SSE 解析中 \r\n 换行符导致空行检测失败、JSON 解析异常。重写 api.ts 的 SSE 解析逻辑后修复。

浏览器缓存干扰

旧 HTML/JS 被 Service Worker 缓存导致部署新版本后用户仍加载旧内容。解决方案: - 对 HTML 返回 Cache-Control: no-cache - 构建时注入 build hash 到 sw.js

WS 端口漂移

Gateway 配置 wsPort: 8080,但实际监听在 18791。Caddy 代理到配置端口导致 502。

教训:用 ss -ltnp 确认真实监听端口,不要只信配置文件。

端口体系

4000+  → Vite dev server(本地调试)
18816  → 单端口对外入口(前端 + API)
18817  → 后端内部 API
28880  → Caddy 反代后的内部端口

验证方式

# 确认监听
ss -ltnp | grep 18816

# 健康检查
curl http://127.0.0.1:18816/
curl http://127.0.0.1:18816/api/health

# 启动
uvicorn app:app --host 0.0.0.0 --port 18816