消除 DEPLOYMENT.md §1.5 列出的 6 个「照抄即部署」缺口(仅动部署面文件,不改应用源码):
1. apps/api/Dockerfile 补 `COPY alembic.ini ./`,容器内可直接 alembic upgrade head。
2. base compose api 注入 CREDENTIAL_ENC_KEY(${VAR:-dev 占位},自动读根 .env 覆盖)+
APP_ENV/LOG_JSON/CORS_ORIGINS,无 .env 也能起,不再因缺 Fernet key 启动失败。
3. base compose web 设 API_BASE_INTERNAL=http://api:8000,RSC 直连 api 容器。
4. web Dockerfile 加 ARG/ENV NEXT_PUBLIC_API_BASE(默认 localhost:8000),compose
web.build.args 传入,构建期可注入公网 API 基址。
5. api/web Dockerfile 内置 HEALTHCHECK(api 用 urllib 打 /health、web 用 node 打 /)。
6. 新增 docker-compose.prod.yml 覆盖层做生产加固:pg 强口令 ${POSTGRES_PASSWORD}、
pg 仅绑 127.0.0.1、APP_ENV=prod、LOG_JSON=true、密钥/连接串/CORS/公网 API 全从
env 且必填(:? 缺失拒起)、restart: unless-stopped。base 保持 dev 便利不破坏。
附带修复:web Dockerfile 在 install 前补 COPY pnpm-workspace.yaml,使容器内 install
看到 minimumReleaseAgeExclude/onlyBuiltDependencies,避免误拒 @xyflow 等近期发布依赖;
新增 .dockerignore(根 + apps/web)排除宿主 node_modules/.next/.venv 等,避免平台原生
二进制经 COPY . . 覆盖容器版导致 next build 失败,并加速构建上下文、保证可复现。
验证:docker compose config(base)与 prod 合并 config 均通过;docker compose build
两镜像均成功(api 378MB / web 296MB);自定义 NEXT_PUBLIC_API_BASE 已确认内联进前端
bundle。DEPLOYMENT.md §1.5 回写为「已修复」并补 §2.6 生产 env 清单与正确部署命令。
28 KiB
部署指南(DEPLOYMENT)
本文是把「AI 辅助中文网文写作工作流」部署到服务器的实操指南。 所有命令/端口/服务名均以仓库当前
docker-compose.yml、两份Dockerfile、.env.example、packages/config/ww_config/settings.py、apps/api/ww_api/main.py为准。遇到与你手上 compose 不一致处,一律以实际docker-compose.yml为准。⚠️ 重要前提:本产品当前是「单用户原型」,没有任何认证 / 多租户(
users/owner_id是桩)。这直接决定了安全部署的红线——见 §6 安全红线。 不要在没有网络层保护的情况下把它裸暴露到公网。
1. 架构与前置条件
1.1 三个组件
部署单元由三个容器组成(服务名即 docker-compose.yml 中的 key):
| 服务名 | 角色 | 镜像来源 | 容器端口 | compose 发布端口 | 健康检查 |
|---|---|---|---|---|---|
pg |
Postgres 16 数据库(唯一真源) | postgres:16-alpine(拉取) |
5432 | 5432:5432 |
✅ pg_isready -U writer(interval 5s / timeout 3s / retries 10) |
api |
FastAPI 后端(async + SSE + LangGraph 编排) | 由 apps/api/Dockerfile 构建,构建上下文 = 仓库根 . |
8000 | 8000:8000 |
✅ Dockerfile HEALTHCHECK:python urllib 打 /health(interval 30s / timeout 5s / start-period 20s / retries 3) |
web |
Next.js 前端(standalone 产物) | 由 apps/web/Dockerfile 构建,构建上下文 = ./apps/web |
3000 | 3000:3000 |
✅ Dockerfile HEALTHCHECK:node http 打 /(interval 30s / timeout 5s / start-period 20s / retries 3) |
依赖关系(compose depends_on):
api依赖pg,且等 pg 健康后才起(condition: service_healthy)。web依赖api,但只是启动顺序(无condition)——web 可能先于 api 就绪,属可容忍的运行期竞态。
数据持久化:命名卷 pgdata 挂到 pg 的 /var/lib/postgresql/data。备份/迁移数据 = 备份这个卷(见 §7)。
1.2 镜像怎么来
两个应用镜像都从源码 Dockerfile 本地构建(仓库未发布预构建镜像):
apps/api/Dockerfile:python:3.12-slim+ 从ghcr.io/astral-sh/uv拷入uv;COPY pyproject.toml uv.lock* / packages / apps/api;uv sync --no-dev --frozen(失败回退非 frozen);CMD uv run uvicorn ww_api.main:app --host 0.0.0.0 --port 8000。apps/web/Dockerfile:多阶段。build 阶段node:22-slim+ corepack(pnpm),pnpm install --frozen-lockfile→pnpm build(Nextoutput: "standalone",见apps/web/next.config.mjs);run 阶段只拷.next/standalone+.next/static+public,CMD node server.js。
web 镜像在构建期消费已提交的 OpenAPI TS 客户端
apps/web/lib/api/schema.d.ts(该文件在 git 里,仅openapi.json被 gitignore)。因此 web 构建不需要联网跑pnpm gen:api。但:后端 schema 改动后,必须先cd apps/web && pnpm gen:api重生成并提交schema.d.ts,再构建 web 镜像,否则前后端契约漂移。
1.3 需要的环境变量(逐项)
变量来源 = packages/config/ww_config/settings.py(后端)+ apps/web/lib/api/config.ts(前端)。
后端(api 容器):
| 变量 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|
DATABASE_URL |
建议显式设 | postgresql+asyncpg://writer:writer@localhost:5432/writer |
异步连接串(asyncpg)。运行时 + alembic upgrade head 都读它(packages/db/migrations/env.py 用它建 async engine 跑迁移)。容器内主机名用 compose 服务名 pg。 |
DATABASE_URL_SYNC |
建议显式设 | postgresql+psycopg://writer:writer@localhost:5432/writer |
同步连接串(psycopg)。.env.example 与 compose 均定义;供同步工具链使用。 |
APP_ENV |
否 | dev |
环境名。生产设 prod。 |
LOG_JSON |
否 | false |
结构化日志是否输出 JSON。生产设 true(structlog JSON,便于采集)。 |
CREDENTIAL_ENC_KEY |
✅ 必填 | 空(SecretStr("")) |
provider 凭据对称加密密钥(Fernet, urlsafe-base64 32B)。api 启动即校验(main._lifespan 调 _fernet(...)):缺失/非法 → 进程直接启动失败,不带病接流量。生产必须用新密钥,见 §2.3。 |
CORS_ORIGINS |
生产强烈建议 | ["http://localhost:3000","http://127.0.0.1:3000"] |
浏览器跨源白名单,逗号分隔覆盖。启动断言不含通配 *(与 allow_credentials=True 不兼容,main.create_app 里 assert "*" not in cors_origins)。生产填前端真实来源,如 https://writer.example.com。 |
前端(web 容器):
| 变量 | 是否必填 | 默认值(config.ts 回退) |
说明 |
|---|---|---|---|
NEXT_PUBLIC_API_BASE |
生产建议 | http://localhost:8000 |
浏览器端访问 API 的基址。⚠️ Next.js 的 NEXT_PUBLIC_* 在构建期内联进客户端 bundle——只在运行期设它不会改已构建产物。web Dockerfile 现声明 ARG NEXT_PUBLIC_API_BASE(默认 http://localhost:8000),base/prod compose 的 web.build.args 均传入它;生产在构建期用 docker compose build 注入前端可达的公网 API 地址(通常反代后的 https://writer.example.com 之类)。 |
API_BASE_INTERNAL |
容器编排建议 | 回退到 NEXT_PUBLIC_API_BASE |
**服务端组件(RSC)**取数用的内网基址。base/prod compose 的 web.environment 已设为 http://api:8000(compose 服务名),RSC 直连 api 容器。 |
1.4 LLM provider key 不进环境变量
不要把任何 LLM 供应商密钥(DeepSeek/Kimi/OpenAI/Anthropic/Qwen/GLM/Gemini 等)写进 env 或 compose。
本产品的 provider 凭据是部署后在应用「设置」页录入,由后端用 CREDENTIAL_ENC_KEY(Fernet)
加密后存进 Postgres(provider_credentials 表 / settings_providers 路由)。
Kimi Code 走 OAuth 订阅授权(kimi_oauth 路由),同样落库。
推论:CREDENTIAL_ENC_KEY 是解开所有已存凭据的唯一钥匙——丢了它 = 库里所有 provider 凭据不可解密,只能重新录入。务必备份保管(见 §2.3 / §6)。
1.5 部署面缺口——已修复(现可照抄部署)
此前审阅 Dockerfile/compose 发现的 6 个「照抄即部署」缺口均已在部署面文件消除(不改应用源码)。现状如下:
- api 镜像已拷
alembic.ini✅:apps/api/Dockerfile在COPY apps/api之后新增COPY alembic.ini ./(迁移目录packages/db/migrations随COPY packages进镜像)。因此可直接在 api 容器内跑docker compose run --rm api uv run alembic upgrade head(§2.4)。 - compose
api已提供CREDENTIAL_ENC_KEY✅:basedocker-compose.yml的api.environment现注入CREDENTIAL_ENC_KEY(${CREDENTIAL_ENC_KEY:-<dev 占位>},compose 自动读仓库根.env覆盖该默认)、APP_ENV/LOG_JSON/CORS_ORIGINS。base compose 现可直接up起 api(无.env也走 dev 占位 key 起来;生产用 §2.2 覆盖文件强制真值)。 - compose
web已设API_BASE_INTERNAL✅:base 的web.environment设API_BASE_INTERNAL=http://api:8000,RSC 服务端取数直连 api 容器。 NEXT_PUBLIC_API_BASE支持构建期注入 ✅:webDockerfile在pnpm build前声明ARG NEXT_PUBLIC_API_BASE=http://localhost:8000+ENV;base/prod compose 的web.build.args传入它。生产docker compose -f ... -f docker-compose.prod.yml build即把公网 API 基址内联进 bundle。- api/web 已内置 HEALTHCHECK ✅:
apps/api/Dockerfile用 stdliburllib打/health、apps/web/Dockerfile用node http打/(均 interval 30s / timeout 5s / start-period 20s / retries 3,镜像无 curl 故用运行时自带解释器)。编排/反代可直接读容器健康态。 - base 弱口令/裸暴露——生产用覆盖层加固 ✅:base
docker-compose.yml保持 dev 便利(pgwriter/writer、5432:5432、APP_ENV=dev);新增docker-compose.prod.yml覆盖层做生产加固——pg 强口令${POSTGRES_PASSWORD}(:?缺失即拒起)、pg 仅绑127.0.0.1:5432、APP_ENV=prod、LOG_JSON=true、CREDENTIAL_ENC_KEY/DATABASE_URL*/CORS_ORIGINS/NEXT_PUBLIC_API_BASE全从 env 且必填、restart: unless-stopped。
构建面同时补了两个
.dockerignore(仓库根 +apps/web):排除宿主node_modules/.next/.venv/.git/缓存,避免宿主平台原生二进制(如 macOS 的 next-swc)经COPY . .覆盖容器内 Linux 版导致pnpm build失败,并加速构建上下文传输、保证可复现。校验:
docker compose config(base)与docker compose -f docker-compose.yml -f docker-compose.prod.yml config(prod 合并)均通过;docker compose build两镜像均成功构建。
2. 方案 A:单机 docker compose(推荐起步)
适合:单台服务器 / VPS,起步验证。前提:服务器已装 Docker(含 Docker Compose v2 插件)。
2.1 装 Docker + 拉代码
# 服务器上(以 Ubuntu 为例)安装 Docker Engine + compose 插件
curl -fsSL https://get.docker.com | sh
docker --version && docker compose version # 确认 compose v2 可用
# 拉代码
git clone <你的仓库地址> writer-work-flow
cd writer-work-flow
git checkout main # 或你的发布分支
2.2 准备生产配置(覆盖 base compose 的 dev 缺省)
base docker-compose.yml 是开发导向(pg 弱口令 writer/writer、5432 对外发布、APP_ENV=dev、
CREDENTIAL_ENC_KEY 用 dev 占位默认)。生产加固的覆盖文件 docker-compose.prod.yml 已随仓库提供(无需手写),
与 base 叠加使用即可。它把安全项全部改为从宿主 env 强制注入(缺失即拒绝启动):
pg:POSTGRES_PASSWORD=${POSTGRES_PASSWORD}强口令、端口仅绑127.0.0.1:5432、restart: unless-stopped。api:APP_ENV=prod、LOG_JSON=true、CREDENTIAL_ENC_KEY/CORS_ORIGINS/DATABASE_URL*(用同一强口令)全从 env。web:NEXT_PUBLIC_API_BASE(构建期 build arg + 运行期 env)指向公网 API、API_BASE_INTERNAL=http://api:8000。
你只需在仓库根准备一个 .env(compose 自动读取做 ${...} 插值;chmod 600、勿入库,已被 .gitignore 忽略):
# .env(compose 变量插值用;chmod 600,勿入库)
POSTGRES_PASSWORD=<强随机口令>
CREDENTIAL_ENC_KEY=<§2.3 生成的 Fernet key>
CORS_ORIGINS=https://writer.example.com # 前端真实来源,逗号分隔;禁通配 *
NEXT_PUBLIC_API_BASE=https://writer.example.com # 浏览器可达的公网 API(经反代)
prod 覆盖层对
POSTGRES_PASSWORD/CREDENTIAL_ENC_KEY/CORS_ORIGINS/NEXT_PUBLIC_API_BASE用${VAR:?...}(必填)语义——任一缺失,docker compose ... config/up直接报错拒起,从设计上杜绝 「带 base 弱口令/占位 key 上生产」。完整清单见 §2.6 生产 env 清单。
2.3 生成并妥善保管 CREDENTIAL_ENC_KEY
⚠️ 这是最关键的一步。用 Fernet 生成一把新密钥(.env.example 里那把是开发占位,生产禁用):
# 任一装了 cryptography 的环境(也可用仓库 uv 环境)
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# 或纯 python:python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
把输出的一行密钥填进 §2.2 的 .env 的 CREDENTIAL_ENC_KEY,并离线备份(密码管理器/密钥库)。
丢失 = 灾难:所有已录入的 provider 凭据(存在 Postgres 里、用它加密)将永久不可解密, 只能清空后重新录入。轮换密钥需先用旧钥解密全部凭据、再用新钥重加密(当前无自动轮换脚本, 见 §6)。切勿复用
.env.example里的占位 key,那等于凭据未加密。
2.4 构建镜像 + 跑迁移 + 起服务
# 在仓库根,带上覆盖文件
export COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
# 1) 构建两个应用镜像
$COMPOSE build
# 2) 先起数据库
$COMPOSE up -d pg
# 等 pg 健康(compose 已配 healthcheck)
$COMPOSE ps # 看 pg 是否 healthy
# 3) 跑数据库迁移(含 LangGraph checkpointer 建表)—— 见下方两种方式
迁移的关键事实:alembic upgrade head 一条命令会做两件事——
(a) 建/升级 16 张业务表;(b) 迁移 d3e4f5a6b7c8_langgraph_checkpoint_setup 会调用 LangGraph 的
checkpointer.setup() 建 checkpoints/checkpoint_blobs/checkpoint_writes/checkpoint_migrations
四张检查点表。checkpointer 的建表只在迁移里发生,绝不在应用运行时(符合 CLAUDE.md「LangGraph」纪律)。
所以部署时必须先把迁移升到 head,应用才能正常写章/审稿/续跑。
api 镜像现已随包 alembic.ini(§1.5 已修复 1),推荐直接在 api 容器里跑迁移(无需宿主装 uv):
方式一(推荐)——在 api 容器里跑迁移:
$COMPOSE run --rm api uv run alembic upgrade head
$COMPOSE run --rm api uv run alembic check # 可选:确认无漂移
该临时容器与 api 服务共享同一 env(含
DATABASE_URL*/CREDENTIAL_ENC_KEY),直连 compose 内网的pg,无需暴露端口。
方式二(备选)——从宿主机 checkout 跑迁移(宿主已装 uv 时可用;prod 覆盖层把 pg 绑到 127.0.0.1:5432):
# 宿主机装 uv:curl -LsSf https://astral.sh/uv/install.sh | sh; export PATH="$HOME/.local/bin:$PATH"
cd writer-work-flow && uv sync
DATABASE_URL="postgresql+asyncpg://writer:${POSTGRES_PASSWORD}@127.0.0.1:5432/writer" \
DATABASE_URL_SYNC="postgresql+psycopg://writer:${POSTGRES_PASSWORD}@127.0.0.1:5432/writer" \
CREDENTIAL_ENC_KEY="${CREDENTIAL_ENC_KEY}" \
uv run alembic upgrade head
4) 起全部服务:
$COMPOSE up -d
$COMPOSE ps # api/web 应为 running(注意它们无 healthcheck,看不到 healthy)
2.5 健康检查
# API:根 + health 端点(均返回 200 JSON)
curl -f http://127.0.0.1:8000/ # {"service":"ww-api","status":"ok"}
curl -f http://127.0.0.1:8000/health # {"status":"ok"}
# Web:首页
curl -fI http://127.0.0.1:3000/
# 看日志(api 启动会校验 CREDENTIAL_ENC_KEY、seed 单用户桩、reap 僵尸 job)
$COMPOSE logs -f api
若 api 启动即退出,最常见原因是 CREDENTIAL_ENC_KEY 非法(base compose 已注入 dev 默认;
生产覆盖层要求经 env 提供合法 Fernet key,见 §2.3)——日志会显示 CredentialKeyError 相关堆栈。
部署完成后,打开前端「设置」页录入 LLM provider 凭据(§1.4),产品才能真正调用模型。
2.6 生产 env 清单
生产用 docker-compose.prod.yml 覆盖层,以下变量必须在仓库根 .env(compose 自动读取做 ${...} 插值;
chmod 600、勿入库)或宿主环境变量提供。带 :? 者缺失即 config/up 报错拒起。
| 变量 | 必填 | 消费方 | 说明 |
|---|---|---|---|
POSTGRES_PASSWORD |
✅ | pg + api(DATABASE_URL*) |
pg 强随机口令,替换 base 弱口令 writer/writer。api 连接串复用它。 |
CREDENTIAL_ENC_KEY |
✅ | api | Fernet key(§2.3 新生成,勿用 .env.example 占位值)。api 启动即校验;解开全部已存 provider 凭据的唯一钥匙,须离线备份。 |
CORS_ORIGINS |
✅ | api | 浏览器跨源白名单,逗号分隔,禁通配 *(如 https://writer.example.com)。 |
NEXT_PUBLIC_API_BASE |
✅ | web(构建期内联 + 运行期) | 浏览器可达的公网 API 基址(经反代)。改它必须重新 docker compose ... build web(构建期内联进 bundle)。 |
APP_ENV |
覆盖层已固定 prod |
api | 无需手设(prod 层写死)。 |
LOG_JSON |
覆盖层已固定 true |
api | 无需手设(prod 层写死)。 |
API_BASE_INTERNAL |
覆盖层已固定 http://api:8000 |
web(RSC) | 无需手设(prod 层写死)。 |
LLM provider key(DeepSeek/Kimi/OpenAI…)永不进 env——部署后在应用「设置」页录入、加密落库(§1.4)。
生成 .env 骨架(示例):
cat > .env <<'EOF'
POSTGRES_PASSWORD=<强随机口令>
CREDENTIAL_ENC_KEY=<uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" 的输出>
CORS_ORIGINS=https://writer.example.com
NEXT_PUBLIC_API_BASE=https://writer.example.com
EOF
chmod 600 .env
3. 反向代理 + HTTPS
生产应在应用前放一层反代做 TLS 终止、单一入口、并(配合 §6)做访问控制。
拓扑:浏览器 → 反代(443) → web:3000(页面/RSC)与 api:8000(/api 或独立子域)。
注意
NEXT_PUBLIC_API_BASE的构建期内联:浏览器直接打 API 的地址在构建 web 镜像时就定死了。 webDockerfile已暴露ARG NEXT_PUBLIC_API_BASE、prod compose 的web.build.args会传入它——改这个基址后必须重新docker compose ... build web才生效(重启不改已构建 bundle)。 若走「同源/api前缀反代」,构建时把NEXT_PUBLIC_API_BASE设为该前缀;若走「api 独立子域」(如api.example.com),同理构建期注入。
3.1 Caddy(推荐,自动证书)
# Caddyfile —— 自动申请/续签 Let's Encrypt 证书
writer.example.com {
# 后端 API:同源 /api/* 反代到 api 容器(去掉 /api 前缀按你的前端约定调整)
handle_path /api/* {
reverse_proxy api:8000
}
# 其余流量到 Next 前端
reverse_proxy web:3000
# SSE 长连接:关掉缓冲,避免审稿/写章流被缓存卡住
reverse_proxy api:8000 {
flush_interval -1
}
}
将 Caddy 也放进 compose(与 api/web 同网络,用服务名访问),暴露 80/443。
3.2 Nginx(手动 / certbot 证书)
server {
listen 443 ssl;
server_name writer.example.com;
ssl_certificate /etc/letsencrypt/live/writer.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/writer.example.com/privkey.pem;
# 前端
location / {
proxy_pass http://web:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 后端 API(含 SSE:写章/四审流)
location /api/ {
proxy_pass http://api:8000/;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection ""; # SSE:保持长连接
proxy_buffering off; # SSE:关缓冲,事件即时下发
proxy_read_timeout 3600s; # 长任务/流式容忍
}
}
无论 Caddy/Nginx,SSE 必须关代理缓冲(写章、四审都是事件流),否则前端进度条会卡住。
4. 方案 B:PaaS / 托管平台
容器化已就绪(两个 Dockerfile + standalone 前端),可托管到 PaaS。共性:用托管 Postgres,
在平台面板设 env(尤其 CREDENTIAL_ENC_KEY、CORS_ORIGINS、DATABASE_URL*),
部署后照 §2.4 跑一次 alembic upgrade head。provider key 仍在应用设置页录入(不进平台 env)。
4.1 Fly.io
- 两个 app:
fly launch分别为 api(apps/api/Dockerfile,构建上下文=仓库根)与 web(apps/web/Dockerfile,上下文=apps/web)。 - Postgres 用
fly postgres create(或外部托管 PG),把DATABASE_URL/DATABASE_URL_SYNC设成其连接串(注意驱动前缀+asyncpg/+psycopg,Fly 给的是原生postgres://,需自行加前缀)。 fly secrets set CREDENTIAL_ENC_KEY=... CORS_ORIGINS=... POSTGRES_PASSWORD=...。- 迁移:镜像已含
alembic.ini(§1.5 已修复 1),可用 Fly[deploy] release_command = "uv run alembic upgrade head"在每次发布前自动跑迁移,或fly ssh console手动跑。 - web 的
NEXT_PUBLIC_API_BASE在构建期注入(Fly build args → DockerfileARG NEXT_PUBLIC_API_BASE,已暴露)。 - api 内部端口 8000、web 内部端口 3000,在
fly.toml的internal_port对应。
4.2 Render
- 两个 Web Service(Docker 运行时),分别指向两个 Dockerfile;web 的 Docker build context 设
apps/web,api 设仓库根。 - Postgres 用 Render Managed Postgres,把内部连接串填进 api 的
DATABASE_URL*(加+asyncpg/+psycopg前缀)。 - 在 api service 的 Environment 填
CREDENTIAL_ENC_KEY/CORS_ORIGINS/APP_ENV=prod/LOG_JSON=true。 - 迁移:用 Render 的 Pre-Deploy Command 跑
uv run alembic upgrade head(镜像已含alembic.ini,§1.5 已修复 1)。 - web 与 api 的互通用 Render 的内部地址;
API_BASE_INTERNAL指向 api 内部 URL,NEXT_PUBLIC_API_BASE(构建期 build arg)指向 api 公网 URL。
两个平台的共同注意点:
NEXT_PUBLIC_API_BASE仍是构建期内联(改它须重构 web 镜像);alembic.ini已随镜像,迁移可在容器/release 命令里跑。 HEALTHCHECK 已内建于镜像,平台存活探测可直接复用或按平台惯例另配/health(api)//(web)。
5. 迁移与升级流程
5.1 常规升级
export COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
git pull # 拉新代码(含新迁移 / schema.d.ts)
$COMPOSE build # 重建 api/web 镜像
# 先升迁移到 head(含新增业务表 + 可能的 checkpointer 变更),再滚动重启(见 §2.4)
$COMPOSE run --rm api uv run alembic upgrade head # 在 api 容器内跑迁移(镜像已含 alembic.ini)
$COMPOSE up -d # 用新镜像重启 api/web(pg 不动)
# 冒烟
curl -f http://127.0.0.1:8000/health && curl -fI http://127.0.0.1:3000/
顺序要点:先迁移、后换应用镜像——新代码通常假设 schema 已到 head。若后端 schema 有变,
记得升级前已在开发侧 pnpm gen:api 重生成并提交了 apps/web/lib/api/schema.d.ts(§1.2),
否则 web 构建/类型会漂移。
5.2 回滚
# 应用回滚:切回上一个 tag/commit 重建重启
git checkout <上一个可用 tag>
$COMPOSE build && $COMPOSE up -d
# 数据库回滚(谨慎):按需降级一版
$COMPOSE run --rm api uv run alembic downgrade -1
⚠️ DB 降级可能丢列/丢表数据,且
d3e4f5a6b7c8的downgrade()会DROP四张 checkpoint 表 (在跑的写章/链任务会丢控制流状态)。回滚前先备份(§7.3)。业务表回滚同理不可逆,优先「向前修复」。
6. 安全红线(单用户原型的硬约束)
⚠️ 本产品没有认证、没有多租户、没有登录(users/owner_id 是桩,见 CLAUDE.md「Prototype scope」)。
任何拿到 URL 的人都能读写全部小说数据、录入/触发用花你钱的 LLM 凭据。因此:
- 绝不裸暴露到公网。 至少满足其一:
- 只在 VPN / 内网 / SSH 隧道内访问;
- 反代前加 IP 白名单 或 HTTP Basic Auth(Caddy
basic_auth/ Nginxauth_basic); - 或先补一层认证再上公网(当前无,属产品级改造)。
- Postgres 不对公网开放。 base compose 把
pg发布到5432:5432(全网卡)。生产必须改成 仅回环127.0.0.1:5432:5432(§2.2)或删掉端口映射只走 compose 内网。改掉 base 里的弱口令writer/writer。 - CORS 显式白名单,禁通配。
CORS_ORIGINS必须是精确来源;后端已在启动断言不含*(allow_credentials=True下*会被浏览器拒且语义危险)。别为图省事放宽。 CREDENTIAL_ENC_KEY的密钥管理与轮换:- 用 §2.3 生成的新 Fernet key,绝不用
.env.example占位值; - 存进密钥库 / 平台 secrets,
.envchmod 600且不入库; - 备份它(丢了 = 所有 provider 凭据不可解密);
- 轮换需「旧钥解密全部凭据 → 新钥重加密」(当前无自动脚本,轮换前规划停机 + 备份)。
- 用 §2.3 生成的新 Fernet key,绝不用
- provider key 只走应用设置页(加密落库),永不写进 env/compose/日志(§1.4)。
- 日志脱敏已内建:structlog 结构化日志按 CLAUDE.md「Logging」纪律不记全 prompt/正文(记长度/哈希)、 不记 API key。保持这一约束,别在自建反代/中间件里回退成明文全量日志。
- HTTPS 强制(§3),别让凭据/正文走明文。
7. 可观测性与运维
7.1 健康端点
GET /→{"service":"ww-api","status":"ok"}GET /health→{"status":"ok"}
两者均无鉴权、返回 200,可作反代/编排的存活探测目标。
api/web 的 Dockerfile 已内置 HEALTHCHECK(§1.5 已修复 5):api 用 stdlib urllib 打 /health、web 用 node http 打 /——docker compose ps 直接显示 healthy,反代/平台亦可复用。
7.2 结构化日志
- 后端 structlog;生产设
LOG_JSON=true输出 JSON,便于日志系统采集/检索。 - 每个请求带
request_id(贯穿 assemble→write→四审→accept 一条链),也进错误响应信封—— 用户报错时可用它端到端 grep。 - 采集:
docker compose logs,或把容器 stdout 接到你的日志栈(Loki/ELK/云日志)。
7.3 备份 Postgres(含凭据库)
数据全在 pgdata 卷 + Postgres 里,包含加密后的 provider 凭据。定期逻辑备份:
export COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
# 逻辑备份(推荐)
$COMPOSE exec pg pg_dump -U writer -d writer -Fc > backup_$(date +%F).dump
# 恢复
$COMPOSE exec -T pg pg_restore -U writer -d writer --clean < backup_YYYY-MM-DD.dump
备份数据库还不够:
CREDENTIAL_ENC_KEY要单独异地备份——DB 里的凭据是密文, 没有这把钥匙备份也解不出(§2.3 / §6)。两者分开保管、都要备。
7.4 成本账本(LLM 调用)
每次 LLM 调用都按 ARCHITECTURE §4.8 的字段集记结构化日志(provider/model/tier/token 数/耗时/request_id 等),
喂成本账本。运维时用这些日志:
- 核对每章/每次生成的 token 与花费;
- 排查慢/失败调用(网关的路由/回退/熔断也在此可见);
- 用
request_id关联「一次写章」的整条链。
采集方式同 §7.2(JSON 日志接入日志栈后按字段聚合)。
附:核心事实速查
- 服务/端口:
pg(5432, 唯一健康检查pg_isready -U writer) ·api(8000) ·web(3000)。 - 镜像:均本地构建;api 上下文=仓库根,web 上下文=
apps/web(standalone)。 - 必填 env:
CREDENTIAL_ENC_KEY(启动即校验,缺失/非法则 api 起不来)。 - 迁移:
alembic upgrade head同时建业务表 + LangGraph checkpointer 表;checkpointer 只在迁移建表,不在运行时。 - provider key:不进 env,部署后在设置页录入、加密落库。
- 部署面缺口已全部修复(§1.5):api 镜像含
alembic.ini;base composeapi注入CREDENTIAL_ENC_KEY/APP_ENV/LOG_JSON/CORS_ORIGINS、web设API_BASE_INTERNAL+NEXT_PUBLIC_API_BASEbuild arg;api/web 内置 HEALTHCHECK;生产加固走docker-compose.prod.yml覆盖层(强口令/回环 pg/prod env/restart)。 - 部署命令:
docker compose build(dev)或docker compose -f docker-compose.yml -f docker-compose.prod.yml build(prod)→... run --rm api uv run alembic upgrade head→... up -d。 - 红线:单用户无认证 → 不裸暴露公网;pg 不对外;CORS 禁
*;CREDENTIAL_ENC_KEY备份+轮换。