From 0131d21ea4b1f28f9a1295970cf146f9356f17a3 Mon Sep 17 00:00:00 2001 From: XingfenD Date: Tue, 29 Sep 2026 22:22:01 +0800 Subject: [PATCH] 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 --- docs/ROADMAP.md | 3 +- .../2026-09-29-prod-write-side-design.md | 131 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 docs/specs/2026-09-29-prod-write-side-design.md diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8019f71..8f254c3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 审阅通过后) | diff --git a/docs/specs/2026-09-29-prod-write-side-design.md b/docs/specs/2026-09-29-prod-write-side-design.md new file mode 100644 index 0000000..4807edf --- /dev/null +++ b/docs/specs/2026-09-29-prod-write-side-design.md @@ -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 admin`→登录→上传→投稿→管理端 approve→开 `.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 收尾。