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 的关键决策¶
- 统一端口对外:前后端必须从同一端口访问
- 开发/调试端口分离:Vite dev server 迁移到 4000 段,避免与 pm2 站点冲突
- 接入 Caddy:从裸端口升级为域名反代
- pm2 serve 改打包版本:开发环境不直接连内部调试地址
踩过的坑¶
SPA fallback 吞静态资源¶
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 确认真实监听端口,不要只信配置文件。