chore: add safe production deploy script

This commit is contained in:
2026-05-20 16:15:33 +08:00
parent 1618ac13f1
commit 85d365069b
3 changed files with 88 additions and 4 deletions

View File

@@ -31,14 +31,15 @@
- 文档 / 解析:`docs/source-analysis.html`(项目内独立文档,不公开挂主应用路由) - 文档 / 解析:`docs/source-analysis.html`(项目内独立文档,不公开挂主应用路由)
- 管理后台:待定 - 管理后台:待定
- 服务器目录:`/opt/skg-marketing-studio` - 服务器目录:`/opt/skg-marketing-studio`
- 生产启动:`docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build` - 生产部署唯一入口:`./scripts/deploy-prod-safe.sh`(先在服务器备份 `deploy/.env.production``data/jobs`、资源库和 `secrets`,再用受保护 rsync 同步代码,最后 Docker 重建并运行 `verify-prod-docker.sh`
- 生产容器重建命令:`docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build`;只允许脚本内部或明确只重启容器时使用,不允许再用裸 `rsync --delete` 手动同步。
- 生产架构:`web` 容器用 Nginx 承载 Next 静态导出;`/login/``/_next/``/assets/``/skg-logo-black.svg``/oasis-source/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/``/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`Traefik 通过 `coolify` 外部网络接入 80/443 - 生产架构:`web` 容器用 Nginx 承载 Next 静态导出;`/login/``/_next/``/assets/``/skg-logo-black.svg``/oasis-source/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/``/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`Traefik 通过 `coolify` 外部网络接入 80/443
- Web 验收必须以生产 Docker 形态为准:前端是 `next export` 静态产物 + Nginx不是 `next dev` / `next start`。任何 Web 改动部署后必须运行 `./scripts/verify-prod-docker.sh`,确认 `/login/``/_next/``/api/health`、本地 API 地址泄漏和 API 镜像 `.env` 污染检查通过;不能只用本地 `npm run build` 作为上线依据。 - Web 验收必须以生产 Docker 形态为准:前端是 `next export` 静态产物 + Nginx不是 `next dev` / `next start`。任何 Web 改动部署后必须运行 `./scripts/verify-prod-docker.sh`,确认 `/login/``/_next/``/api/health`、本地 API 地址泄漏和 API 镜像 `.env` 污染检查通过;不能只用本地 `npm run build` 作为上线依据。
- 当前音频解析:`https://ai.skg.com/azure/v1``gpt-4o-transcribe` 当前返回 `DeploymentNotFound`,且官方 Azure OpenAI transcription 路径探测也未返回可用部署;生产临时复制本地成功策略,直接使用容器内 `faster-whisper tiny.en` 真实转写,关闭 Gemini 多模态音频兜底。拿到真实 Azure ASR deployment 名后再恢复 `ASR_REMOTE_ENABLED=true` - 当前音频解析:`https://ai.skg.com/azure/v1``gpt-4o-transcribe` 当前返回 `DeploymentNotFound`,且官方 Azure OpenAI transcription 路径探测也未返回可用部署;生产临时复制本地成功策略,直接使用容器内 `faster-whisper tiny.en` 真实转写,关闭 Gemini 多模态音频兜底。拿到真实 Azure ASR deployment 名后再恢复 `ASR_REMOTE_ENABLED=true`
- 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`;全局资源中心持久化在 `./data/asset_library``./data/prompt_library``./data/_trash` - 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`;全局资源中心持久化在 `./data/asset_library``./data/prompt_library``./data/_trash`
- TikTok 下载登录态:公开视频默认不带 cookies 直接下载,生产环境变量必须显式保持 `YTDLP_COOKIES_FILE=``YTDLP_COOKIES_FROM_BROWSER=` 为空,防止容器读取不存在的浏览器 cookies。只有 TikTok 明确要求登录态时,才使用服务器私有 cookies 文件 `./secrets/tiktok_cookies.txt` 挂载到 API 容器 `/run/secrets/tiktok_cookies.txt` 并配置 `YTDLP_COOKIES_FILE=/run/secrets/tiktok_cookies.txt``yt-dlp` 会在任务结束时回写 cookies因此不要把该挂载设为只读不要使用云端浏览器读取方案也不要把 cookies 入库。生产容器严禁使用 `YTDLP_COOKIES_FROM_BROWSER=chrome` - TikTok 下载登录态:公开视频默认不带 cookies 直接下载,生产环境变量必须显式保持 `YTDLP_COOKIES_FILE=``YTDLP_COOKIES_FROM_BROWSER=` 为空,防止容器读取不存在的浏览器 cookies。只有 TikTok 明确要求登录态时,才使用服务器私有 cookies 文件 `./secrets/tiktok_cookies.txt` 挂载到 API 容器 `/run/secrets/tiktok_cookies.txt` 并配置 `YTDLP_COOKIES_FILE=/run/secrets/tiktok_cookies.txt``yt-dlp` 会在任务结束时回写 cookies因此不要把该挂载设为只读不要使用云端浏览器读取方案也不要把 cookies 入库。生产容器严禁使用 `YTDLP_COOKIES_FROM_BROWSER=chrome`
- 登录凭证:用户名写下方快捷登录;密码明文备份只放服务器 `/root/skg-marketing-studio-login.txt`,生产环境变量 `WEB_AUTH_PASSWORD` / `WEB_AUTH_SESSION_SECRET` 只放服务器 `deploy/.env.production` - 登录凭证:用户名写下方快捷登录;密码明文备份只放服务器 `/root/skg-marketing-studio-login.txt`,生产环境变量 `WEB_AUTH_PASSWORD` / `WEB_AUTH_SESSION_SECRET` 只放服务器 `deploy/.env.production`
- 手动 `rsync` 到服务器必须排除本机开发文件和真实生产 env`.git``.memory``.logs``.pids``data``jobs``secrets``api/.env``api/.env.local``api/.env.production``deploy/.env.production``web/node_modules``web/.next``web/out`。不要把本地 `api/.env``deploy/.env.production` 覆盖到 `/opt/skg-marketing-studio`否则会把开发 cookies / API 配置烤进生产镜像或清空生产登录模型配置。 - 禁止手动 `rsync --delete` 到服务器必须使用 `./scripts/deploy-prod-safe.sh`。如遇极端情况必须手动同步,命令必须同时包含 protect/exclude`.git``.memory``.logs``.pids``data``jobs``secrets``api/jobs``api/.env``api/.env.local``api/.env.production``deploy/.env.production``web/node_modules``web/.next``web/out`。不要把本地 `api/.env``deploy/.env.production` 覆盖到 `/opt/skg-marketing-studio`也不要删除服务器 `data/jobs`,否则会清空案例、登录模型配置。
## 快捷登录 ## 快捷登录
- 登录地址:`https://marketing.skg.com/login/` - 登录地址:`https://marketing.skg.com/login/`

View File

@@ -540,8 +540,8 @@
</tr> </tr>
<tr> <tr>
<td>生产部署</td> <td>生产部署</td>
<td><code>docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build</code></td> <td><code>./scripts/deploy-prod-safe.sh</code></td>
<td>服务器目录为 <code>/opt/skg-marketing-studio</code>;后端任务文件挂载到 <code>./data/jobs</code>,全局资源中心挂载到 <code>./data/asset_library</code><code>./data/prompt_library</code><code>./data/_trash</code>,真实 Key 只放服务器 <code>deploy/.env.production</code>Web 上线验收必须按 Docker 静态形态跑 <code>./scripts/verify-prod-docker.sh</code>,不能只用本地 <code>npm run build</code> 替代</td> <td>服务器目录为 <code>/opt/skg-marketing-studio</code>;后端任务文件挂载到 <code>./data/jobs</code>,全局资源中心挂载到 <code>./data/asset_library</code><code>./data/prompt_library</code><code>./data/_trash</code>,真实 Key 只放服务器 <code>deploy/.env.production</code>生产部署唯一入口是 <code>deploy-prod-safe.sh</code>:先备份服务器 env、案例和资源库再用 protect/exclude 保护 <code>data/</code><code>jobs/</code><code>secrets/</code><code>deploy/.env.production</code> 后同步代码,最后 Docker 重建并跑 <code>verify-prod-docker.sh</code>。禁止再用裸 <code>rsync --delete</code> 手动同步</td>
</tr> </tr>
<tr> <tr>
<td>前端开发服务</td> <td>前端开发服务</td>
@@ -1131,6 +1131,18 @@ ProductRefStateItem {
<h2>变更记录</h2> <h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p> <p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog"> <div class="changelog">
<article class="change">
<header>
<h3>2026-05-20 · 生产部署增加数据保护脚本</h3>
<span class="tag amber">Deploy</span>
<span class="tag rose">Safety</span>
</header>
<div class="body">
<p><strong>问题:</strong>手动 <code>rsync --delete</code> 如果没有排除服务器 <code>data/jobs</code> 和真实 <code>deploy/.env.production</code>,会把生产案例、资源库或登录配置删掉。</p>
<p><strong>改动:</strong>新增 <code>scripts/deploy-prod-safe.sh</code> 作为生产部署唯一入口。脚本部署前会在服务器创建 <code>/opt/skg-marketing-studio-backups/skg-marketing-preserve-*.tgz</code>,备份真实 env、案例、资源库和 secrets同步时用 <code>rsync --filter='P ...'</code> 和 exclude 双重保护 <code>data/</code><code>jobs/</code><code>secrets/</code><code>api/jobs</code><code>deploy/.env.production</code> 和本地开发文件。</p>
<p><strong>影响:</strong>后续发布不再手写裸 <code>rsync --delete</code>;脚本会自动 Docker 重建并调用 <code>verify-prod-docker.sh</code>。若误操作,先从最新 <code>skg-marketing-preserve-*.tgz</code> 恢复。</p>
</div>
</article>
<article class="change"> <article class="change">
<header> <header>
<h3>2026-05-20 · 转换层改为提示词确认后生成</h3> <h3>2026-05-20 · 转换层改为提示词确认后生成</h3>

71
scripts/deploy-prod-safe.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail
HOST="${HOST:-root@76.13.31.179}"
APP_DIR="${APP_DIR:-/opt/skg-marketing-studio}"
BACKUP_DIR="${BACKUP_DIR:-/opt/skg-marketing-studio-backups}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
if [[ "${1:-}" == "--no-build" ]]; then
BUILD_FLAG=""
else
BUILD_FLAG="--build"
fi
echo "==> Preflight: creating remote data/env backup"
ssh "$HOST" "set -euo pipefail
cd '$APP_DIR'
mkdir -p '$BACKUP_DIR'
stamp=\$(date +%Y%m%d%H%M%S)
tar -czf '$BACKUP_DIR/skg-marketing-preserve-'\$stamp'.tgz' \
deploy/.env.production \
data/jobs \
data/asset_library \
data/prompt_library \
data/_trash \
secrets 2>/tmp/skg-backup-warnings.log || {
cat /tmp/skg-backup-warnings.log >&2 || true
exit 1
}
find '$BACKUP_DIR' -name 'skg-marketing-preserve-*.tgz' -type f -printf '%T@ %p\n' | sort -nr | tail -n +8 | cut -d' ' -f2- | xargs -r rm -f
echo backup:\$(ls -t '$BACKUP_DIR'/skg-marketing-preserve-*.tgz | head -1)
"
echo "==> Syncing code with production data protected"
rsync -az --delete \
--filter='P /data/***' \
--filter='P /jobs/***' \
--filter='P /secrets/***' \
--filter='P /deploy/.env.production' \
--filter='P /api/jobs/***' \
--filter='P /api/.env' \
--filter='P /api/.env.local' \
--filter='P /api/.env.production' \
--exclude='/.git/' \
--exclude='/.memory/' \
--exclude='/.logs/' \
--exclude='/.pids/' \
--exclude='/data/' \
--exclude='/jobs/' \
--exclude='/secrets/' \
--exclude='/api/jobs/' \
--exclude='/api/.env' \
--exclude='/api/.env.local' \
--exclude='/api/.env.production' \
--exclude='/deploy/.env.production' \
--exclude='/web/node_modules/' \
--exclude='/web/.next/' \
--exclude='/web/out/' \
--exclude='/node_modules/' \
--exclude='内部分享-口播脚本.md' \
./ "$HOST:$APP_DIR/"
echo "==> Rebuilding production containers"
ssh "$HOST" "cd '$APP_DIR' && docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d $BUILD_FLAG"
echo "==> Verifying production"
"$ROOT_DIR/scripts/verify-prod-docker.sh" "$HOST"
echo "==> Done"