Files
crearte-monorepo/docs/specs/2026-09-29-prod-write-side-design.md
T
XingfenD 0131d21ea4 docs(spec): P1 prod write-side design (self-contained local drill, option A)
- docs/specs/2026-09-29-prod-write-side-design.md: minio-prod on 9001,
  api-prod STORAGE_S3_*/GAMES_BASE_DOMAIN/CORS env, POSTGRES_PASSWORD
  default removed chain-wide, nginx wildcard block templated + /api/
  proxy (guarded repo-yaml.test.ts updated), TLS explicitly out of scope
- docs/ROADMAP.md: P1 status -> spec written, doc index registered
2026-09-29 22:22:01 +08:00

132 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P1 prod 写侧可用(本机自包含演练)设计 / Prod Write-Side Design
- **状态**:设计已批准(2026-09-29,方案 A + 跳过 TLS),待 spec 审阅后转实现计划。
- **路线图**:`docs/ROADMAP.md` P1(第一波)。
- **范围定性**:纯 compose/nginx/配置编排改动;**零后端代码改动、零前端应用代码改动**(仅 `deploy/` 资产与其守卫测试)。
## 1. 背景与意图
prod 栈的 `api-prod` 未配置任何 `STORAGE_S3_*`,server 启动时 `storageEnabled=false`,上传/投稿/审核/预览路由整体不注册——生产只读。本设计在**本机 compose 内**把 prod 栈补齐到与 dev 同等的写侧+游玩能力,作为日后迁真实服务器的定型演练。
已确认的两个上游决策(2026-09-29):
1. **形态**:本机自包含演练——MinIO 入 prod 栈,域名体系用 `*.localhost`。
2. **TLS**:本轮跳过,全程 `http://localhost:8080`;README 记录迁真机时的 TLS 步骤。
成功标准(prod profile 起来后):注册→上传 bundle→投稿→审核上架→站内游玩(virtual 子域运行时 + external 外链)全链路可用;`docker compose --profile prod up -d --build` 后按 AGENTS.md 验证四步全绿。
## 2. 方案选型
| 方案 | 内容 | 取舍 |
|---|---|---|
| **A. 镜像 dev 直连法(已选)** | `minio-prod`(主机端口 9001),浏览器跨源直连 MinIO 拉密文/封面;nginx 通配块参数化 + 补 `/api/` 反代 | 复用 dev 已验证的跨源模式(`MINIO_API_CORS_ALLOW_ORIGIN` + `STORAGE_S3_PUBLIC_BASE_URL`);改动最小 |
| B. nginx 同源反代 `/objects/` | 只暴露 8080,形态更近真 prod | 重写两个 server 块代理/改写 + MinIO 匿名策略变化,验证面大;上真服务器时再迁 |
| C. 只做写侧不验游玩 | 范围最小 | P1 明文含 `GAMES_BASE_DOMAIN`,游玩链路是"写侧可用"的完整验收,砍掉不闭合 |
## 3. 设计详述
### 3.1 prod 端口与命名基线
- prod MinIO 主机端口 **`${MINIO_PROD_PORT:-9001}`**(避开 dev 的 9000,误起两栈时冲突面最小化);容器名 `minio-prod`,卷 `minio-data-prod`(新卷,不碰任何既有卷名)。
- 桶名沿用 `${MINIO_BUCKET:-crearte}`;路径风格 `STORAGE_S3_FORCE_PATH_STYLE=true`;公开基址 `STORAGE_S3_PUBLIC_BASE_URL=http://localhost:${MINIO_PROD_PORT:-9001}/${MINIO_BUCKET:-crearte}`。
### 3.2 compose 改动(crearte-deploy/)
`api-prod` 补环境变量(其余不动):
```yaml
depends_on:
db-prod: { condition: service_healthy }
minio-prod: { condition: service_started }
environment:
<<: *api-env
DATABASE_URL: postgres://crearte:${POSTGRES_PASSWORD:?…}@db-prod:5432/crearte?sslmode=disable
STORAGE_S3_ENDPOINT: http://minio-prod:9000
STORAGE_S3_BUCKET: ${MINIO_BUCKET:-crearte}
STORAGE_S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER:?…}
STORAGE_S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:?…}
STORAGE_S3_FORCE_PATH_STYLE: "true"
STORAGE_S3_PUBLIC_BASE_URL: http://localhost:${MINIO_PROD_PORT:-9001}/${MINIO_BUCKET:-crearte}
GAMES_BASE_DOMAIN: localhost # server 默认即 localhost,显式写出以表意;真域名时改此值
CORS_ALLOWED_ORIGINS: ${HOST_ORIGIN:-http://localhost:8080}
```
`minio-prod` 服务:镜像 `pgsty/minio:latest`,`command: ["server", "/data"]`,`restart: unless-stopped`,端口 `${MINIO_PROD_PORT:-9001}:9000`,环境 `MINIO_ROOT_USER/MINIO_ROOT_PASSWORD`(同上 `:?`)+ `MINIO_API_CORS_ALLOW_ORIGIN: "*"`,卷 `minio-data-prod:/data`。
`web-prod` 补两项:
```yaml
build:
args:
VITE_API_BASE_URL: /
VITE_GAMES_BASE_DOMAIN: ${PUBLIC_GAMES_HOST:-localhost:8080} # host:port,含端口
VITE_HOST_ORIGIN: ${HOST_ORIGIN:-http://localhost:8080}
environment:
GAMES_SERVER_NAME: ${GAMES_SERVER_NAME:-*.localhost} # nginx 模板渲染变量
```
> 命名注意:`PUBLIC_GAMES_HOST`(客户端用,`localhost:8080` 带端口)与 server 的 `GAMES_BASE_DOMAIN`(CORS 后缀匹配用,纯主机名 `localhost`)是两个不同值,compose 变量必须分开,防止一处改动串坏另一处。
### 3.3 POSTGRES_PASSWORD 去默认值(全链)
- `x-postgres-env` 与两条 `DATABASE_URL` 中 `${POSTGRES_PASSWORD:-crearte}` 一律改 `${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}`。
- **数据风险与迁移**:pgdata 卷内口令已固化;现存 `.env` 没写该变量的用户必须显式补 `POSTGRES_PASSWORD=crearte` 才能保住旧卷,README「数据与配置」加一条迁移警告(dev/prod 同理)。
- `.env.example`:`POSTGRES_PASSWORD=crearte`(dev 开箱即用)+ 注释"prod 务必改强口令";新增 `MINIO_ROOT_USER=`/`MINIO_ROOT_PASSWORD=` 空占位(注释说明 dev 可留默认 minioadmin 需显式写值、prod 必填)。注意 `.env.example` 值与 compose `:?` 守卫同步。
### 3.4 nginx 模板化(crearte/deploy/)
现状两处死点:通配块 `server_name *.games.example.com` 硬编码;通配块不提供 `/api/` 反代(子域运行时取 bundle-key 会 404——dev 靠 vite 顺带代理才没暴露)。
改法:
1. `deploy/nginx.conf` → `deploy/nginx.conf.template`,主块保持 `server_name _;`,通配块改 `server_name ${GAMES_SERVER_NAME};`。nginx 官方镜像 entrypoint(`20-envsubst-on-templates.sh`)按**已定义环境变量**渲染到 `/etc/nginx/conf.d/`,`$uri`/`$host` 等 nginx 内建变量不受影响(非环境变量)。
2. 通配块新增 `location /api/`,逐字照抄主块的 resolver + `proxy_pass http://$api_upstream` + `Host`/`X-Forwarded-For` 写法;其余维持"三件套 + `location / { return 404; }`"骨架不变。子域页面因此**同源**取 API,bundle-key 链路不再依赖跨源。
3. Dockerfile:`COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf` → `COPY deploy/nginx.conf.template /etc/nginx/templates/default.conf.template`。
4. **守卫测试同步**(`src/scripts/repo-yaml.test.ts`):读取路径改 `deploy/nginx.conf.template`;通配块查找键改 `server_name ${GAMES_SERVER_NAME};`;新增断言通配块含 `location /api/` 且 proxy 指向上游写法;"no /data/ no /assets/" 断言保留。`dev-game-runtime.ts` 头注里"对齐生产 nginx 通配 server block"的语义仍然成立,不动。
### 3.5 一次性桶初始化(prod)
与 dev 同型、端口不同,README「使用」prod 小节新增一行:
```bash
docker compose --profile prod exec minio-prod sh -c 'mc alias set local http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" && mc mb --ignore-existing local/crearte && mc anonymous set download local/crearte'
```
**单引号是刻意的**:宿主机 shell 不展开 `$MINIO_ROOT_USER/$MINIO_ROOT_PASSWORD`(`.env` 不会自动 export 到用户 shell),由容器内 shell 用 compose 注入的同名环境变量完成展开——与 dev 版(写死 minioadmin)行为等价但不再复制凭据字面量。不引入自动初始化——保持"卷首建需手动一次性初始化"的既有约定。
## 4. 验证(AGENTS.md 四步,逐条执行)
1. `docker compose config -q` 四个 profile 全过(dev/prod/debug/mock)。
2. `--profile prod up -d --build` + `ps` 健康全绿;跑 prod MinIO 桶初始化。
3. 冒烟链:`localhost:8080` 注册→`docker compose exec api-prod /app/crearte-server user set-role <email> admin`→登录→上传→投稿→管理端 approve→开 `<hash>.localhost:8080` 玩 virtual 作品(bootstrap→sw→bundle-key→解密→可玩);external 作品外链正常;匿名只读目录/详情 200。
4. 回归:dev 栈 `up -d --build` 再起一次确认 9001/9000 无端口冲突、写侧未受 POSTGRES_PASSWORD 改动影响(`.env` 补值后);`repo-yaml.test.ts` 等前端单测在容器内全绿。
5. `down`(**绝不 `-v`**)。
## 5. 影响文件(预估)
| 仓库 | 文件 | 改动 |
|---|---|---|
| crearte-deploy | `docker-compose.yml` | minio-prod 服务、api-prod 补 env、web-prod 补 args/env、POSTGRES_PASSWORD 去默认、卷 minio-data-prod |
| crearte-deploy | `.env.example` | POSTGRES_PASSWORD、MINIO_ROOT_* 占位与注释 |
| crearte-deploy | `README.md` | prod 写侧用法、桶初始化、迁移警告、真机 TLS/域名 checklist |
| crearte-deploy | `docs/CHANGELOG.md` | 0.5.0 条目 |
| crearte | `deploy/nginx.conf.template`(新,替代 nginx.conf) | 通配块 server_name 参数化 + `/api/` 反代 |
| crearte | `deploy/Dockerfile` | 模板 COPY 改路径 |
| crearte | `src/scripts/repo-yaml.test.ts` | 读模板、断言更新 |
| crearte | `docs/CHANGELOG.md` | 0.15.0 条目 |
| wrapper | `docs/ROADMAP.md` | P1 状态、文档索引登记本 spec |
## 6. 明确不做(YAGNI / 出圈)
- 不做 TLS、不装反代层(真机时:Caddy/certbot + 泛解析 + 通配证书,README 记步骤即可)。
- 不动 server/前端应用代码;不引 nginx `/objects/` 同源反代(方案 B,留待真服务器)。
- 不改任何卷名;不新增 compose 全局网络拓扑。
- P1 的"密码默认值"仅指 `POSTGRES_PASSWORD` 链(S3 root 凭据入 `:?` 见 3.2/3.3);`AUTH_TOKEN_SECRET`/`BUNDLE_KEK_k1` 已有守卫,不动。
## 7. 分支与提交
- crearte:`feat/prod-nginx-wildcard-api`,完成后 `--no-ff` 合入 master 并删分支。
- crearte-deploy:`feat/prod-write-side`,同上。
- wrapper:spec/ROADMAP/CHANGELOG 记录可直提 master。
- 提交顺序:crearte(模板+测试)→ crearte-deploy(compose+文档,验证依赖前者)→ wrapper 收尾。