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
This commit is contained in:
2026-09-29 22:22:01 +08:00
parent cc2156b44d
commit 0131d21ea4
2 changed files with 133 additions and 1 deletions
+2 -1
View File
@@ -16,7 +16,7 @@
| # | 子项目 | 内容 | 涉及仓库 | 依赖 | 状态 |
|---|--------|------|----------|------|------|
| P0 | 已知缺陷清理 | ① CORS `Access-Control-Allow-Headers` 补 `If-None-Match`(解锁 e2e:stack Step5:revoke 410 + 降级)② 清理 compose 配置漂移(`BUNDLE_KEY_STORE` 环境变量、`bundle-keys-*` 卷——密钥自 server 0.6.0 起在 Postgres)③ `crearte-deploy` CHANGELOG 补账 | server, deploy | 无 | **完成**(server `a8ea755` / deploy `27044be`,2026-09-29 合并推送) |
| P1 | prod 写侧可用 | prod 栈补对象存储(prod MinIO 或外部 S3)+ `api-prod` 配 `STORAGE_S3_*` / `GAMES_BASE_DOMAIN` / `CORS_ALLOWED_ORIGINS`;密码默认值、TLS 起步。现状:prod 无对象存储时写侧路由(上传/投稿/审核/预览)不注册,生产只读 | deploy(少量 server) | P0 | 待启动 |
| P1 | prod 写侧可用 | prod 栈补对象存储(prod MinIO 或外部 S3)+ `api-prod` 配 `STORAGE_S3_*` / `GAMES_BASE_DOMAIN` / `CORS_ALLOWED_ORIGINS`;密码默认值、TLS 起步。现状:prod 无对象存储时写侧路由(上传/投稿/审核/预览)不注册,生产只读 | deploy(少量 server) | P0 | **spec 已出**(`docs/specs/2026-09-29-prod-write-side-design.md`,方案 A 本机演练 + 本轮跳过 TLS,待审阅转计划) |
| P2 | CI + 测试基线 | 后端 / 部署仓建 CI(gofmt/vet/test + `TEST_DATABASE_URL` 集成层 + e2e:stack 全链路);与前端已有 `validate.yml` 对齐。现状:后端/部署仓无 CI,Postgres 集成测试无 `TEST_DATABASE_URL` 时静默 skip | 三仓 | P0(e2e 依赖其修复) | 待启动 |
| P3 | 备份 + 可观测 | `pg_dump` 定时备份 + MinIO 数据保护 + 恢复演练 runbook;结构化 / 请求日志 + `/metrics`;限流器单实例问题记录权衡 | server, deploy | P1(拓扑定型) | 待启动 |
@@ -51,3 +51,4 @@
| 子项目 | spec | 实现计划 |
|--------|------|----------|
| P4 作者主页 | `docs/specs/2026-09-29-author-page-design.md` | `docs/plans/2026-09-29-author-page.md`(已执行,2026-09-29) |
| P1 prod 写侧可用 | `docs/specs/2026-09-29-prod-write-side-design.md` | 待撰写(spec 审阅通过后) |
@@ -0,0 +1,131 @@
# 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 收尾。