部署与发布
本地构建 → 上传静态产物 → Nginx 托管。永远不在生产服务器上构建。
为什么不在服务器上构建
香港服务器配置:2 vCPU / 4GB RAM + 4GB swap。
VitePress 构建会跑完整的 Vite 打包(esbuild + Rollup + SSR 渲染),是 Node 重负载任务。
历史事故(2026-10-02):此前在服务器上直接跑前端构建(vue-tsc 类型检查), 进程运行 90+ 分钟,服务器负载从 1–2 飙升到 27.60,内存和 swap 完全耗尽, SSH 和 HTTPS 服务无响应,触发阿里云「异常算力利用」告警,最终需要强制重启服务器才恢复。
强制规则:
- 永远不在生产服务器上运行前端/文档站构建
- 必须在本地或 CI 环境完成构建
- 只上传
dist/产物到服务器
构建
export PATH="$HOME/.local/node/bin:$PATH"cd fluxtoken-docsnpm ci # 首次或依赖变更时npm run build # 会自动先跑 build-facts.mjs 刷新数据构建产物在 docs/.vitepress/dist/。
npm run build 的实际流程:
node scripts/build-facts.mjs ← 先抓数据,失败则沿用已提交快照
vitepress build docs ← 再构建静态站数据抓取失败不会中断构建——build-facts.mjs 在拉不到线上数据时会沿用上一次的 docs/.vitepress/data/groups.json,只在控制台打一条 warning。 这样即使上游接口临时抖动,也不会卡住发布。
发布
首次部署(建站)
# 1. 服务器上建目录
ssh fluxtoken-hk "sudo mkdir -p /var/www/fluxtoken-docs && sudo chown -R \$USER:\$USER /var/www/fluxtoken-docs"
# 2. 上传构建产物
cd fluxtoken-docs
rsync -avz --delete docs/.vitepress/dist/ fluxtoken-hk:/var/www/fluxtoken-docs/随后按 deploy/nginx-docs.conf 配置 Nginx 并签发证书。
日常发布
cd fluxtoken-docs
npm run build
rsync -avz --delete docs/.vitepress/dist/ fluxtoken-hk:/var/www/fluxtoken-docs/--delete 会清理旧文件,避免残留已删除的页面。注意不要漏掉末尾的 /, 否则会把 dist 目录本身塞进目标路径。
如果不想直接用 rsync,项目里也提供了一键脚本:
cd fluxtoken-docs./deploy/publish.shNginx 配置
完整配置见 deploy/nginx-docs.conf(已在生产生效)。要点:
- 独立
server_name docs.fluxtoken.ai,纯静态,root /var/www/fluxtoken-docs - HTTP 跳转用 308,ACME 走 webroot
/var/www/acme(与 status / news 站一致) groups.json设为短缓存(5 分钟)+Access-Control-Allow-Origin: *- 工程文件(
*.json/*.md/*.conf等)一律 403 - 证书
/etc/letsencrypt/live/docs-fluxtoken-ai/,由服务器既有的new-api-hk-cert-renew.timer(Docker 版 certbot,挂载同一份/etc/letsencrypt与/var/www/acme)自动续期,无需额外配置
应用到服务器:
scp deploy/nginx-docs.conf fluxtoken-hk:/tmp/
ssh fluxtoken-hk "cp /tmp/nginx-docs.conf /etc/nginx/conf.d/fluxtoken-docs.conf && nginx -t && systemctl reload nginx"⚠️ CSP 的两个坑(改动前必读)
主站公共安全头片段 /etc/nginx/snippets/fluxtoken-security-headers.conf 里的 CSP 是 script-src 'self'。VitePress 每个页面都带 2 个内联 <script> (check-dark-mode 深色模式判定、check-mac-os),会被它拦掉,表现是深色模式失效。
处理方式是用 sha256 hash 精确放行,而不是放宽成 unsafe-inline——保持 CSP 强度。 两个 hash(所有页面完全一致):
sha256-2m0LKtL9TkM7m6BmMncU8A6pAb9ikh3OzrtPZXy+uhU=
sha256-La1r0VSk0Po4KFI0duEKhmPu+u0I416JW3oONqtdf4M=坑一:不能 include 那个公共片段。 它自带一条 CSP,我们也要发一条, 浏览器对多个 CSP 取交集,片段那条会把内联脚本拦掉。 所以配置里把片段里的四个头逐条重写了(除 CSP 加 hash 外内容一致)。
坑二:CSP 必须定义在 server 层,不能只放在 location ~* \.html$ 里。 本站开了 cleanUrls,页面实际地址是 /guide/billing 这类不带 .html 的路径, 命中 location / 而不是正则 location。只在正则里发 CSP 时, 实测 61 个页面里有 58 个拿不到 hash 版本。
另:nginx 的 add_header 不继承——任何自带 add_header 的 location 都会丢掉父级全部 add_header。所以配置里每个带 add_header 的 location 都重新声明了完整的四个头。
VitePress 升级后如果改了那两个内联脚本,hash 会失效。 重新计算:
cd fluxtoken-docsnode -e 'const fs=require("fs"),crypto=require("crypto"); const h=fs.readFileSync("docs/.vitepress/dist/index.html","utf8"); for(const m of h.matchAll(/<script(?![^>]*\\bsrc=)[^>]*>([\\s\\S]*?)<\\/script>/g)) console.log("sha256-"+crypto.createHash("sha256").update(m[1],"utf8").digest("base64"));'证书
不要用 certbot --nginx。 服务器上的续期是 Docker 版 certbot + webroot, 新签证书要沿用同一套,否则续期时会因为 authenticator 不一致而失败:
ssh fluxtoken-hk "certbot certonly --webroot -w /var/www/acme \
-d docs.fluxtoken.ai --cert-name docs-fluxtoken-ai \
--key-type ecdsa --non-interactive --agree-tos --email admin@fluxtoken.ai"首次签发前,需要先装一个只含 HTTP 段的配置让 ACME 校验能过 (完整配置引用的证书文件此时还不存在,nginx -t 会失败)。
验证续期配置正确(不必等它跑完):
ssh fluxtoken-hk "cat /etc/letsencrypt/renewal/docs-fluxtoken-ai.conf"应看到 authenticator = webroot 与 webroot_path = /var/www/acme。
DNS
docs.fluxtoken.ai 的 A 记录指向 47.243.105.230(已完成)。
验证
for u in / /guide/ /groups/pricing /cli/claude-code /faq/errors /data/groups.json; do printf '%-28s ' "$u" curl -s -o /dev/null -w "%{http_code}\n" --max-time 15 "https://docs.fluxtoken.ai$u"done# 工程文件应返回 403curl -s -o /dev/null -w "package.json -> %{http_code}\n" https://docs.fluxtoken.ai/package.json# HTTP 应 308 跳转curl -s -o /dev/null -w "http -> %{http_code}\n" http://docs.fluxtoken.ai/期望:各路径 200,工程文件 403,HTTP 请求 308 跳转。
检查 CSP 是否正确(关键:每个页面的 CSP数 都必须是 1 且 hash 为 1, 缺 hash 说明命中了没有 hash 的 location):
for p in / /guide/ /guide/billing /groups/pricing /faq/errors; do r=$(curl -sI --max-time 15 "https://docs.fluxtoken.ai$p") printf "%-24s CSP数=%s hash=%s\n" "$p" \ "$(echo "$r" | grep -ci 'content-security-policy')" \ "$(echo "$r" | grep -c 'sha256-')"done# 期望:每行都是 CSP数=1 hash=1回滚
静态站的回滚就是换回旧产物。发布前保留一份:
ssh fluxtoken-hk "cp -r /var/www/fluxtoken-docs /var/www/fluxtoken-docs.bak-\$(date +%Y%m%d-%H%M%S)"出问题时把备份目录换回去即可。Nginx 不用动,也不需要重启服务。
与主站发布的边界
这套文档站与 sub2api 主站完全解耦:
| 文档站 | 主站 | |
|---|---|---|
| 形态 | 纯静态,Nginx 直接托管 | Docker 容器 |
| 构建 | 本地/CI | 本地构建后传 dist 再打镜像 |
| 目录 | /var/www/fluxtoken-docs/ | /opt/sub2api-test-20260925/ |
| 镜像升级影响 | 无 | 会覆盖定制主题等 |
所以主站升级不需要碰文档站。但主站升级后,文档里引用的控制台路径、 开关状态和报错文案可能需要核对——见升级回归清单。