Commit dc207003 by luoqi

feat(deploy): 文档站单独部署脚本 —— 固化 env-file 并硬校验 OpenAPI Server URL

【解决的问题】docs 镜像里 OpenAPI 的「Server URL」来自 build arg DOCS_API_URL:
  ${DOCS_API_URL:-${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3101}}
而 NEXT_PUBLIC_API_BASE_URL 在 apps/pac-web/.env 里,不在 pac-service/.env。
deploy-prod.sh 本来就传两个 --env-file,但它**不含 pac-docs**,所以改文档得手工重建;
手敲时少传一个 env-file 就回落成 localhost:3101 —— 而且**构建照样成功、不报任何错**,
只有点开 API 参考页才看得出来,对接方照着填会打不通。

2026-09-03 实际踩到:之前几次手工重建 docs 都只传了 pac-service/.env,
线上 API 参考页的 Server URL 一直是 localhost:3101,挂了几天没人发现,
直到对接方看文档时提出来。

【为什么不并进 deploy-prod.sh(先前的想法,量化后否掉)】
pac-docs 的 Dockerfile 是 `COPY . .`(整个仓库),任何代码改动都会让它的 build 缓存失效、
Next.js SSG 全量重跑。测试服实测:
  什么都没改       →   3 秒(17/17 层缓存命中)
  改了后端代码一行 → 111 秒(COPY 之后全失效)
而绝大多数部署都是改后端、文档没动 —— 并进主流程等于每次白花约 2 分钟买"不会忘"。
真正的问题不是"要不要自动跑",是"手敲参数容易写错",所以治后者。

【脚本做什么】
  - compose 组装与 deploy-prod.sh 完全一致(含 COMPOSE_MANAGED override + 两个 env-file)
  - 构建**前**先解析并打印将注入的地址,解析不到 / 是 localhost 直接 die(带修复提示)
  - 显式 build → force-recreate(同 deploy-prod.sh 不信 compose 重建判定的理由)
  - 构建**后**硬校验:读容器内 openapi/pac.json 的 servers[0].url,与期望值不等即失败
    (而不是只看构建日志 —— 日志对了不代表镜像里对)
  - 再验文档站 200

deploy/README.md 登记脚本 + 说明为何分开、为何不要手敲 compose。
parent 5431fc3c
......@@ -10,6 +10,7 @@
|---|---|---|
| `deploy/gen-env.sh` | 生成 `.env`(密钥随机 + 生产开关 + 业务值注入 + 缺失报告) | 拒覆盖已有(`FORCE=1` 强制) |
| `deploy/deploy-prod.sh` | 拉码 + build + 重建容器 + 硬验证(镜像/迁移/health) | ✅ 反复跑 |
| `deploy/deploy-docs.sh` | **文档站单独部署**(pac-docs 不在 deploy-prod.sh 里)+ 硬验证 OpenAPI Server URL | ✅ 反复跑 |
| `deploy/first-import.sh` | 数据首灌:seed + 存量 + 重算画像/计划 | ✅ 可重跑 |
---
......@@ -34,6 +35,25 @@ build 阶段不影响服务(老容器一直跑),停机只发生在重建那一
> 改了 `NEXT_PUBLIC_*`(前端 build-time 变量)必须走本脚本重 build;只改后端 `.env` 用 `docker compose -f docker-compose.prod.yml restart pac-service` 即可。
### 改了文档(`apps/pac-docs/`)要另外跑一条
`deploy-prod.sh` **只构建 pac-migrate / pac-service / pac-web,不含 pac-docs** —— 文档站要单独部署:
```bash
bash deploy/deploy-docs.sh
```
**为什么分开**:pac-docs 的 Dockerfile 是 `COPY . .`(整个仓库),任何代码改动都会让它的
build 缓存失效、Next.js SSG 全量重跑。实测(2026-09-03 测试服):什么都没改 **3 秒**,
改了后端一行代码 **111 秒**。而绝大多数部署都是改后端、文档没动 —— 并进主流程等于每次白花约 2 分钟。
⚠️ **不要手敲 `docker compose build pac-docs`**。docs 镜像里 OpenAPI 的「Server URL」来自
build arg `DOCS_API_URL`,它回落到 `NEXT_PUBLIC_API_BASE_URL`,而后者在 **`apps/pac-web/.env`**
里(不在 `pac-service/.env`)。少传一个 `--env-file` 就会变成 `http://localhost:3101`,
**且构建照样成功、不报任何错** —— 只有点开 API 参考页才看得出来,对接方照着填会打不通
(2026-09-03 实际踩到,线上挂了几天没人发现)。`deploy-docs.sh` 固化了两个 env-file
并在构建后硬校验 servers 地址,写错立刻失败。
---
## 2. 新环境从零上线(换服务器 / 首次生产)
......
#!/usr/bin/env bash
# PAC 文档站(pac-docs)单独部署
#
# 【为什么不并进 deploy-prod.sh】
# pac-docs 的 Dockerfile 里是 `COPY . .`(整个仓库),所以**任何**代码改动都会让它的
# build 缓存失效、Next.js SSG 全量重跑。实测(2026-09-03,测试服):
# 什么都没改 → 3 秒 (17/17 层缓存命中)
# 改了后端代码一行 → 111 秒(COPY 之后全失效)
# 而绝大多数部署都是改后端、文档没动 —— 并进主流程等于每次白白多花约 2 分钟。
# 故保持分离:改了 apps/pac-docs/ 下的东西才跑本脚本。
#
# 【为什么要有这个脚本,而不是手敲 docker compose】
# docs 镜像里的 OpenAPI「Server URL」来自 build arg DOCS_API_URL,而 compose 里它是
# ${DOCS_API_URL:-${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3101}}
# NEXT_PUBLIC_API_BASE_URL 在 **apps/pac-web/.env** 里(不在 pac-service/.env)。
# 手敲时少传一个 --env-file 就会一路回落到 localhost:3101 —— 而且**构建照样成功、
# 不报任何错**,只有点开 API 参考页才看得出来,对接方照着填就打不通。
# 2026-09-03 实际踩到:之前几次手工重建 docs 都只传了 pac-service/.env,
# 线上 API 参考页的 Server URL 一直是 localhost:3101。
# → 本脚本固化两个 env-file,并在构建后**硬校验** servers 地址,写错立刻失败而非静默上线。
#
# 用法(服务器上,~/pac):
# bash deploy/deploy-docs.sh # 部署文档站
# COMPOSE_MANAGED=1 bash deploy/deploy-docs.sh # 托管 DB 形态的机器(同 deploy-prod.sh)
# bash deploy/deploy-docs.sh --no-pull # 跳过 git 拉取(代码已就位时)
set -euo pipefail
log() { printf '\n\033[1;36m== %s ==\033[0m\n' "$*"; }
die() { printf '\033[1;31mFAIL: %s\033[0m\n' "$*" >&2; exit 1; }
main() {
cd "$(dirname "$0")/.."
if [[ "${1:-}" != "--no-pull" ]]; then
log "git 拉取($(git rev-parse --abbrev-ref HEAD))"
git pull --ff-only
fi
# compose 组装与 deploy-prod.sh 保持一致 —— 两个 env-file 缺一不可(见文件头说明)
local COMPOSE=(docker compose -f docker-compose.prod.yml)
[[ "${COMPOSE_MANAGED:-0}" == "1" ]] && COMPOSE+=(-f docker-compose.managed.yml)
COMPOSE+=(--env-file apps/pac-service/.env --env-file apps/pac-web/.env)
# 构建前先把将要注入的地址打出来,肉眼可核
local api_url
api_url=$("${COMPOSE[@]}" config 2>/dev/null | grep -m1 'DOCS_API_URL:' | sed 's/.*DOCS_API_URL: *//')
log "OpenAPI Server URL 将注入为:${api_url:-(空)}"
[[ -n "$api_url" ]] || die "解析不到 DOCS_API_URL —— 检查 apps/pac-web/.env 是否有 NEXT_PUBLIC_API_BASE_URL"
case "$api_url" in
*localhost*|*127.0.0.1*)
die "DOCS_API_URL 解析成了本地地址($api_url)。
多半是 apps/pac-web/.env 缺 NEXT_PUBLIC_API_BASE_URL,或该文件不存在。
这么构建出来的 API 参考页,对接方照着填会打不通。" ;;
esac
log "build pac-docs(显式,不和 up 混 —— 同 deploy-prod.sh 的理由)"
"${COMPOSE[@]}" build pac-docs
log "force-recreate(不信 compose 的重建判定)"
"${COMPOSE[@]}" up -d --force-recreate pac-docs
# ── 验证 1/2:镜像里的 spec 确实带对了地址(而不是只看构建日志) ──
log "验证 1/2:容器内 openapi spec 的 servers"
local got
got=$(docker exec "$("${COMPOSE[@]}" ps -q pac-docs)" \
node -e 'const d=require("/app/apps/pac-docs/openapi/pac.json");process.stdout.write(String((d.servers&&d.servers[0]&&d.servers[0].url)||""))' 2>/dev/null || true)
[[ "$got" == "$api_url" ]] \
|| die "spec 里的 servers 是「$got」,期望「$api_url」"
printf ' servers OK (%s)\n' "$got"
# ── 验证 2/2:站点起得来 ──
log "验证 2/2:文档站可访问"
local code
for _ in $(seq 1 20); do
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3102/ || true)
[[ "$code" == "200" ]] && break
sleep 2
done
[[ "$code" == "200" ]] || die "文档站未就绪(最后一次 HTTP $code)"
printf ' docs 200(loopback)\n'
log "文档站部署成功 ✅ $(git log --oneline -1)"
}
main "$@"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment