P3-2 baseline 落库从路由下沉到服务层(predict_baseline 内直接落库),
删除路由层 _persist_baseline,三种模式统一 result.prediction_id,对外 JSON 不变。
P3-1 MatchPredictPanel.PredictionPanel 拆为 OutcomePanel/AgentsPanel/ReasoningPanel
三个子组件,本文件保留 PredictModal/PredictProgress/Spinner,对外导出路径不变。
P3-3 docs 加 ⚠️ 多 worker 陷阱红字 + STRICT_SINGLE_WORKER 环境变量(启动期强制拒绝多 worker)。
P3-4 docs 新增「同站部署 vs 跨站 CSRF」节。
12 KiB
06 · 部署
前置要求
- Docker & Docker Compose
- bzzoiro API Key(必填)
- LLM API Key(必填,OpenAI / Deepseek / 兼容接口)
Docker Compose 部署(推荐)
# 1. 配置环境变量
cp .env.example .env
# 编辑 .env: 填 LLM_API_KEY / BZZOIRO_KEY
# 2. 启动(自动执行数据库迁移)
docker compose up -d --build
# 3. 验证
curl http://localhost:8000/health
docker-compose.yml 包含 3 个服务:
| 服务 | 端口 | 说明 |
|---|---|---|
postgres |
5433 | PostgreSQL 16 |
api |
8000 | FastAPI 应用(启动时自动执行 alembic upgrade head) |
frontend |
3000 | React 前端(多阶段构建,nginx 服务静态文件) |
数据卷 pgdata 持久化数据库,重启不丢数据。
注意:
api服务启动时会先执行alembic upgrade head迁移数据库,再启动 uvicorn。 容器内数据库连接自动使用postgres服务名(通过 composeenvironment覆盖.env中的DB_HOST)。
生产上线检查清单
公网上线前逐项勾选。第 1–4 项由 src/core/security_check.py 在 APP_ENV=production
启动时强制校验,不满足直接拒绝启动(开发环境仅告警);管理鉴权另有请求期 fail-closed(503)。
- 1.
APP_ENV=production— 安全校验、CookieSecure、管理端点 fail-closed 均以它为总开关 - 2.
SECRET_KEY强随机 — 用openssl rand -base64 32生成;禁止弱值/短值(弱值黑名单会拒绝启动,含把生成指令原样粘进去的情况) - 3. 管理鉴权至少其一 —
ADMIN_PASSWORD或ADMIN_API_KEY(后台改过密码后以数据库哈希优先);两者皆空时管理接口 503 - 4. 数据库强密码 — 禁止
football:football等示例弱密码(启动校验会拒绝);compose 的POSTGRES_PASSWORD必填,缺失时容器拒绝启动 - 5. HTTPS — 由反代(Nginx/Caddy)终结 TLS;
APP_ENV=production下会话 Cookie 自动Secure(且 HttpOnly + SameSite=Lax) - 6. 反代信任头 —
TRUST_PROXY_HEADERS=True,且仅可信反代可达 API;反代需设置X-Forwarded-For($proxy_add_x_forwarded_for)与X-Real-IP,否则限流/日志按反代 IP 计数 - 7. 限流前置到网关 — 推荐 Nginx
limit_req(配置见安全与限流);应用内限流与 KeyRing 为单进程内存实现,多 worker 各自独立计数会把实际配额放大 N 倍(启动时会打印一次性告警) - 8. uvicorn 单 worker — compose/Dockerfile 默认单 worker,保持即可;需横向扩容时先在网关统一限流,再起多实例(每实例仍单 worker)
⚠️ 多 worker 陷阱:应用内限流(
_RateLimiter)与 KeyRing 均为进程内纯内存状态,多 worker 部署(如uvicorn --workers 4)时各进程各自独立计数、互不共享——实际限流配额会被放大 N 倍、KeyRing 限流状态也不同步。 若确需多 worker,必须前置 Nginx/网关做全局限流(见安全与限流),并设环境变量STRICT_SINGLE_WORKER=True(见下)在启动期强制拒绝多 worker,避免静默配额漂移。
- 9. 启动后健康检查 —
curl /health返回 200(存活);curl /health/ready返回 200(就绪,校验数据库连通,不可达时 503) - 10. 数据库迁移 — compose/Dockerfile 启动命令已内置
alembic upgrade head && uvicorn …,升级镜像重启即自动迁移,无需手动执行
本地开发部署
# 1. 安装依赖
pip install -e ".[dev]"
# 2. 启动 PostgreSQL(单独)
docker run -d --name profeto-pg \
-e POSTGRES_USER=football -e POSTGRES_PASSWORD=football -e POSTGRES_DB=football \
-p 5432:5432 postgres:16-alpine
# 3. 配置 .env
cp .env.example .env
# 4. 建表
alembic upgrade head
# 5. 启动 API
uvicorn src.api.app:app --reload
# 6. 启动前端(另一个终端)
cd frontend && npm install && npm run dev
访问:
- API 文档: http://localhost:8000/docs
- 前端界面: http://localhost:5173
环境变量
| 变量 | 必需 | 默认值 | 说明 |
|---|---|---|---|
APP_ENV |
❌ | development |
production / development |
LOG_LEVEL |
❌ | INFO |
日志级别 |
API_PORT |
❌ | 8000 |
API 服务端口映射 |
FRONTEND_PORT |
❌ | 3000 |
前端服务端口映射 |
POSTGRES_USER |
✅ | — | PostgreSQL 用户名 |
POSTGRES_PASSWORD |
✅ | — | PostgreSQL 密码 |
POSTGRES_DB |
❌ | football |
PostgreSQL 数据库名 |
POSTGRES_PORT |
❌ | 5433 |
PostgreSQL 端口映射 |
DATABASE_URL |
✅ | — | PostgreSQL 连接 URL(Docker 内会被覆盖) |
LLM_PROVIDER |
❌ | openai |
提供商名(仅标记) |
LLM_API_KEY |
✅ | — | API Key |
LLM_BASE_URL |
❌ | https://api.openai.com/v1 |
接口地址(Ollama/Deepseek 用) |
LLM_MODEL |
❌ | gpt-4o |
默认模型 |
LLM_TIMEOUT |
❌ | 60 |
单次调用超时(秒) |
LLM_SPECIALIST_MODEL |
❌ | — | 专家模型(回落 LLM_MODEL) |
LLM_AGGREGATOR_MODEL |
❌ | — | 终裁模型(回落 LLM_MODEL) |
BZZOIRO_KEY |
✅ | — | bzzoiro 数据源 Key(唯一数据源) |
CORS_ORIGINS |
❌ | http://localhost:5173,... |
允许的跨域来源 |
STRICT_SINGLE_WORKER |
❌ | False |
True 时若以多 worker 启动则拒绝(防限流配额漂移) |
SECRET_KEY |
❌ | — | 加密主密钥(生产环境必填) |
ADMIN_PASSWORD |
❌ | — | 管理后台密码(留空=不启用) |
ADMIN_API_KEY |
❌ | — | 机器/脚本调用的 API Key |
同站部署 vs 跨站 CSRF
Profeto 管理鉴权使用 HttpOnly Cookie 会话(登录后服务端写入),allow_credentials=True 的 CORS 配置允许浏览器跨域携带 Cookie——这也引入了 CSRF 面。部署拓扑决定风险等级:
同站部署(推荐): 前端与 API 同域(反代把 / 与 /api 都转发到同一后端,或同源端口)。
- 浏览器视为 same-origin,CORS 不触发;
SameSite=Lax会话 Cookie 天然阻断跨站请求携带。 - 风险最低。
CORS_ORIGINS可设为空或同域来源,仅作兜底。
跨站部署: 前端与 API 不同域(如前端 app.example.com、API api.example.com,或开发时 localhost:3000 → localhost:8000)。
- 必须把 API 域名列入
CORS_ORIGINS,且allow_credentials=True才能携带 Cookie。 - 此时任何被允许域下的页面都能构造带 Cookie 的请求 → CSRF 面:
- 状态变更接口(采集/回测/改密等写操作)要求管理员 Cookie + 同域,攻击者无法从第三方站点读取 Cookie,但可构造跨域表单/请求——
SameSite=Lax会阻断跨站 POST 表单提交(顶级导航 GET 仍放行),这是当前主要防线。 GET /api/v1/admin/*只读接口受SameSite=Lax下顶级导航可能被利用,但攻击者无法读取响应(CORS 不匹配时浏览器拦截)。
- 状态变更接口(采集/回测/改密等写操作)要求管理员 Cookie + 同域,攻击者无法从第三方站点读取 Cookie,但可构造跨域表单/请求——
- 加固建议:
- 反代层加
Origin/Referer校验,仅放行CORS_ORIGINS列表中的来源(即便 FastAPI CORS 已通过,反代校验是多一层纵深)。 - 写操作要求自定义请求头(如
X-Requested-With: XMLHttpRequest),第三方站点无法在无预检下添加自定义头,天然阻断简单跨站 POST。 - 生产强制 HTTPS(
APP_ENV=production下 Cookie 自动Secure),防中间人窃 Cookie。
- 反代层加
LLM 提供商配置示例
OpenAI
LLM_API_KEY=sk-xxxx
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o
Deepseek
LLM_API_KEY=sk-xxxx
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat
Ollama(本地)
LLM_API_KEY=ollama
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL=llama3.1
分档配置(专家用便宜模型)
LLM_MODEL=gpt-4o
LLM_SPECIALIST_MODEL=gpt-4o-mini
LLM_AGGREGATOR_MODEL=gpt-4o
数据库迁移
Alembic 管理 schema 变更:
# 查看当前版本
alembic current
# 升级到最新
alembic upgrade head
# 回退一级
alembic downgrade -1
# 生成新迁移(改 models.py 后)
alembic revision --autogenerate -m "描述"
# 空迁移(手动写 SQL)
alembic revision -m "描述"
Docker Compose 自动迁移: api 容器启动时会自动执行 alembic upgrade head,
无需手动运行。本地开发时需手动执行迁移。
已有迁移:
0001_initial: 初始 5 张表0002_agent_outputs: predictions 加mode+agent_outputs0003_injuries: 增加 injuries 表0004_snapshot_and_constraints: 增加约束0005_prediction_status_and_stats_provenance: 增加时间语义0006-0012: 后续 schema 调整、约束命名对齐、partial unique index 等
备份与恢复
# 备份
docker exec profeto-postgres pg_dump -U football football > backup.sql
# 恢复
cat backup.sql | docker exec -i profeto-postgres psql -U football football
监控
健康检查
| 端点 | 含义 | HTTP 状态码 |
|---|---|---|
/health |
存活检查(liveness) | 始终 200(进程在跑即活) |
/health/ready |
就绪检查(readiness) | DB 可达 200,不可达 503 |
探针配置
Docker Compose
docker-compose.yml 已为 api 服务配置 readiness:
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8000/health/ready || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
要点:必须指向 /health/ready 而非 /health——后者始终 200,在数据库故障时仍会接收流量,导致请求全部失败。
Kubernetes
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 15
readinessProbe:
httpGet:
path: /health/ready
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
两探针必须区分:
livenessProbe用/health:仅在进程死锁/崩溃时重启,避免误杀。readinessProbe用/health/ready:DB 不可用时停止转发流量,恢复后自动切回。
验证
# 宿主机直接运行(经本地 8000 端口)
python3 tests/test_health_ready.py
评估
安全与限流
内存限流(按进程)
/api/v1/predict 与登录防爆破均使用进程内内存计数:
| 机制 | 位置 | 局限 |
|---|---|---|
/predict 限流 |
_RateLimiter(内存) |
每 worker 独立计数,不共享 |
| 登录防爆破 | _fail_times(内存) |
同上 |
多 worker 部署时(如 uvicorn --workers 4),每进程各自计数,实际限额为 N × 单进程限制。
公网部署建议
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Nginx │────▶│ API │
│ │ │ 限流层 │ │ 内存限流 │
└─────────┘ └──────────┘ └──────────┘
推荐配置:
-
Nginx 层限流(第一道防线):
limit_req_zone $binary_remote_addr zone=predict:10m rate=10r/m; location /api/v1/predict { limit_req zone=predict burst=20 nodelay; proxy_pass http://api:8000; } -
TRUST_PROXY_HEADERS=True 时必须由可信反代设置
X-Forwarded-For:proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;- 若为
False: 仅用request.client.host,忽略X-Forwarded-For,防伪造 - 若为
True: 解析X-Forwarded-For第一个 IP,反代后方可信
- 若为
-
生产环境必须配置管理鉴权:
APP_ENV=production REQUIRE_ADMIN_AUTH=True ADMIN_PASSWORD=your_secure_password未配置时
/admin等管理接口返回 503。
参数调优
| 参数 | 默认 | 说明 |
|---|---|---|
_predict_limiter.max_requests |
10 | 每分钟每 IP 最大请求数 |
_predict_limiter.window_seconds |
60 | 滑动窗口时长 |
ADMIN_SESSION_TTL_HOURS |
168 | 管理会话有效期(天) |