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

9.3 KiB
Raw Blame History

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 补环境变量(其余不动):

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 补两项:

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 小节新增一行:

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 收尾。