Files
FruitServer/DEPLOY.md
T
jianghaifeng 7dd4b15804 docs(deploy): 按 specs 约束补全部署文档(新增 DEPLOY-CHECKLIST.md)
为什么:用户要求"写好文档,一定要根据 specs 文档约束好"。把 specs 里与部署相关的
硬约束逐条对照现状后发现 9 条里有 6 条未覆盖或只写了一半,需要一次补齐并如实标注工程缺口。

- 新增 DEPLOY-CHECKLIST.md:环境映射(办公室主机 = test)、域名/HTTPS/反代(含 nginx 与证书建议)、
  网络分区、发布与回滚规范(语义化 tag / release→main / 回滚演练)、备份与恢复(specs 目标 vs 现状差距)、
  监控与告警现状、密钥托管与轮换、**上线检查清单对照表(docs/12 逐条 ✅⚠️❌ + 责任方)**、
  风险/待确认(R1–R10)、上线前必补清单(P0–P2)
- DEPLOY.md 增加"本机属于 test 环境"与交叉引用,并单列"上线前必须补的四条"提示
- 对照表里的 ✅ 都带证据(如分区生效先实测 playback_ticket_202610 落分区、_default = 0)
- 只写文档,未改任何实现;mvn test 83 条全过、两入口 package 通过
2026-10-16 13:20:31 +08:00

13 KiB
Raw Blame History

FruitShorts 后端部署与回滚(DEPLOY)

依据:docs/04-部署架构.md(第一阶段 Docker Compose 单机、镜像即交付物;生产才上 K8s)、 docs/11-发布与回滚方案.md、docs/12-上线检查清单.md(播放签名生产必开、密钥走环境变量)。 目标机形态:办公室一台主机(§三 精简方案:app/api/DB/Redis 合并 1 台 8C32G,转码 worker 独立进程)。

本文只讲"怎么部署/怎么回滚"。要回答"我们离 specs 还差什么、上线前必补什么", 看 DEPLOY-CHECKLIST.md(环境映射、域名/HTTPS、备份恢复、监控、发布与回滚规范、 docs/12-上线检查清单 逐条对照表、风险/待确认)。

这台办公室主机属于 test 环境(04-部署架构§一:测试/联调、独立测试库、内网/VPN 访问); pre/prod 的规格与前置条件见 DEPLOY-CHECKLIST.md §一。

一、两条部署路径(先选一条)

路径 适用 命令主线
A. Docker Compose(推荐,文档口径) 目标主机装了 Docker + Compose;一条命令起全栈(含 PG 18 与 Redis) 见 §三
B. 裸 jar(兜底) 主机没有 Docker,或只想起单个进程调试 见 §四

两条路径的配置完全相同(同一批环境变量),只是"谁来跑进程"不同。

二、环境变量清单

复制 .env.example 为 .env 填值(.env 已在 .gitignore,不要提交)。 使用 --spring.profiles.active=prod(Compose 里已默认)时,下表标 🔴 的项缺失/仍是 dev 默认值/长度不足 32 位 → 启动直接失败(fail-fast,见 §五)。

变量 必填 缺失后果 说明
DB_URL ✅ 用默认 jdbc:postgresql://127.0.0.1:5432/fruitshorts,连不上就启动失败 Compose 里默认指向 db 服务
DB_USER / DB_PASSWORD ✅ 连库失败 Compose 的 db 容器用同一组值创建库
DB_NAME Compose 必填 默认 fruitshorts 只在 compose 里用到
JWT_ADMIN_SECRET 🔴 prod 拒绝启动 管理端令牌密钥,≥32 位,生成:openssl rand -base64 48
JWT_APP_SECRET 🔴 prod 拒绝启动 客户端令牌密钥。过去这个键没外置,一直用代码里的 dev 默认值,务必注入
MEDIA_SIGNATURE_SECRET 🔴 prod 拒绝启动 播放地址签名密钥(docs/12-上线检查清单)
MEDIA_SIGNATURE_REQUIRED 🔴 prod 下不是 true 就拒绝启动 生产必须开;本地调试保持 false
CORS_ALLOWED_ORIGINS 🔴 prod 下为空则拒绝启动 逗号分隔的正式来源,生产不许用 *
MEDIA_ROOT_DIR 建议 默认 ./data/uploads,容器里必须为 /data/uploads 媒资根目录(挂载卷)
SERVER_PORT Compose 已设 默认 8081/8080 app-api 8081 / admin-api 8080
SWAGGER_ENABLED 可选 默认 false(测试/联调请在 .env 里设 true,前端要看 /doc.html) 一次开关两个:true = /doc.html + /v3/api-docs 都开;false = 两个都关(生产不想暴露 UI 就设 false,机器可读契约也一并关闭)
STORE_VERIFIER / GOOGLE_PLAY_* 可选 默认 disabled 商店票据校验,没凭据就保持 disabled(对外返回"尚未配置")

密钥怎么生成:openssl rand -base64 48(每个密钥单独生成,别复用)。

三、路径 A:办公室单机 + Docker Compose(主线)

目标主机前置:Docker + Docker Compose(docker compose version 有输出即可),建议 8C32G。

# 0) 取代码(或拿打包好的目录)并进入 FruitServer
cd FruitServer

# 1) 配置环境变量
cp .env.example .env
openssl rand -base64 48        # 生成三个密钥,分别填进 JWT_ADMIN_SECRET / JWT_APP_SECRET / MEDIA_SIGNATURE_SECRET
vi .env                        # 还要填 DB_PASSWORD、CORS_ALLOWED_ORIGINS

# 2) 构建并启动(首次会自动建表 + 造种子数据)
docker compose up -d --build

# 3) 看状态与日志
docker compose ps                 # 预期:db/redis healthy,app-api/admin-api healthy,transcode-worker running
docker compose logs -f app-api    # 预期:出现 "生产配置校验通过" 与 "Started AppApiApplication"

# 4) 冒烟
tools/smoke.sh                    # 预期:全部 PASS,退出码 0

端口与访问:app-api 8081(对客户端开放)、admin-api 只绑 127.0.0.1:8080(管理端仅内网,公网通过 SSH 隧道或反代访问)、PG/Redis 不对宿主暴露。

数据库初始化:sql/*.sql 已挂进 docker-entrypoint-initdb.d(schema → schema-phase1 → partition → data → data-phase1), 只在数据卷为空时执行。改 SQL 后要重来:docker compose down -v(会删数据)再 up。

日常操作

docker compose ps                     # 状态
docker compose logs -f --tail=200     # 全部日志
docker compose restart app-api        # 重启单个服务
docker compose up -d --build app-api  # 只重建单个镜像
docker compose down                   # 停(保留数据卷)

四、路径 B:裸 jar(兜底)

目标主机前置:JDK 21;转码进程额外需要 ffmpeg/ffprobe(brew install ffmpeg 或 apt install ffmpeg)。 数据库:自己装 PostgreSQL 18 并导入 sql/*.sql(顺序同上)。

cd FruitServer
mvn -pl fruitshorts-app-api,fruitshorts-admin-api,fruitshorts-transcode-worker -am package -DskipTests

# 导出环境变量(示例;密钥务必换真实值)
export DB_URL=jdbc:postgresql://127.0.0.1:5432/fruitshorts DB_USER=fruitshorts DB_PASSWORD='***'
export JWT_ADMIN_SECRET="$(openssl rand -base64 48)"       # 生产请固定保存,不要每次重新生成
export JWT_APP_SECRET="$(openssl rand -base64 48)"
export MEDIA_SIGNATURE_SECRET="$(openssl rand -base64 48)"
export MEDIA_SIGNATURE_REQUIRED=true CORS_ALLOWED_ORIGINS='https://admin.example.com'
export MEDIA_ROOT_DIR="$PWD/data/uploads"

# 起三个进程(生产用 prod profile;本地调试去掉 --spring.profiles.active=prod 即可)
nohup java -jar fruitshorts-admin-api/target/fruitshorts-admin-api-1.0.0-SNAPSHOT.jar \
  --spring.profiles.active=prod > /tmp/admin.log 2>&1 &
nohup java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar \
  --spring.profiles.active=prod > /tmp/app.log 2>&1 &
nohup java -jar fruitshorts-transcode-worker/target/fruitshorts-transcode-worker-1.0.0-SNAPSHOT.jar \
  --spring.profiles.active=prod > /tmp/worker.log 2>&1 &

⚠️ 重新打包必须重启进程:spring-boot:repackage 会原地替换 jar,运行中的进程再去加载"没加载过的类" 会抛 NoClassDefFoundError,表现为请求挂住(S1 记录过这个坑)。顺序永远是"改完 → 打包 → 重启 → 验证"。

五、prod fail-fast 是什么、长什么样

prod profile 下 ProdSecretGuard 会校验:JWT 双密钥 + 媒体签名密钥(非空、不是 dev 默认值、≥32 位)、 signature-required=true、DB_URL 非空、CORS 非空。任一不满足 → 打印全部问题并终止启动:

ERROR c.f.framework.config.ProdSecretGuard : 生产配置校验未通过,拒绝启动(prod profile):
  - fruitshorts.jwt.admin-secret(环境变量 JWT_ADMIN_SECRET) 仍是代码里的 dev 默认值,必须换成生产密钥
  - fruitshorts.jwt.app-secret(环境变量 JWT_APP_SECRET) 仍是代码里的 dev 默认值,必须换成生产密钥
  - fruitshorts.media.signature-secret(环境变量 MEDIA_SIGNATURE_SECRET) 仍是代码里的 dev 默认值,必须换成生产密钥
  - fruitshorts.cors.allowed-origins 为空(环境变量 CORS_ALLOWED_ORIGINS)—— 生产不允许用通配 *,必须显式列出来源
请按 FruitServer/DEPLOY.md 的环境变量清单补齐后重启。

exit code = 1(部署脚本可以直接判失败)。没有 prod profile 时这套校验不生效,本地开发照旧开箱即用。

六、发布后验证(每次发版都做)

  1. docker compose ps:三个应用容器 Up (healthy)(worker 是 Up)。
  2. tools/smoke.sh:全 PASS、退出码 0。它检查 /healthz、/app/categories、/app/home/feed、 两个 /v3/api-docs、/doc.html,以及未授权路径是否按统一信封拒绝。 关于在线文档那三项:脚本会按 SWAGGER_ENABLED 自适应 —— true 时断言 200; false 时打印 SKIP(不算失败),因为 /doc.html 与 /v3/api-docs 都被配置关掉了。 没设这个变量时脚本自动探测("200 且含 openapi"算开,"统一信封 code=404"算关,其它响应→FAIL)。 联调环境想强制要求文档在线:SWAGGER_REQUIRED=true tools/smoke.sh(不满足直接失败)。
  3. curl -s http://127.0.0.1:8081/healthz:(从容器里或宿主机)应返回 {"code":0,...,"status":"UP"}。
  4. 业务抽查:curl -s http://127.0.0.1:8081/app/home/feed | head -c 200(有内容); 后台 http://127.0.0.1:8080/doc.html(需要 .env 里 SWAGGER_ENABLED=true,联调环境请打开)。
  5. 登录一个后台账号,确认能进后台首页(连通数据库与权限体系)。

七、回滚

镜像/代码回滚

# 用上一个版本的 tag(每次发版都打 tag:fruitshorts/app-api:2026-10-16-1)
docker compose stop app-api admin-api transcode-worker
docker tag fruitshorts/app-api:2026-10-15-2 fruitshorts/app-api:latest
docker compose up -d app-api admin-api transcode-worker
tools/smoke.sh

或用 git 回到上一个 tag 重新 up -d --build:

git fetch --tags && git checkout <上一个 release tag>
docker compose up -d --build

数据库回滚(谨慎):schema 脚本是幂等增量(ADD COLUMN IF NOT EXISTS 等),一般不需要回滚表结构; 真要回滚数据用备份:docker compose exec -T db pg_dump -U fruitshorts fruitshorts > backup.sql。 发布前先备份是硬要求:

docker compose exec -T db pg_dump -U "${DB_USER:-fruitshorts}" "${DB_NAME:-fruitshorts}" > backup-$(date +%F-%H%M).sql

回滚后必须重跑 tools/smoke.sh,并确认转码队列状态正常(后台「数据看板 → 转码看板」)。

八、未在本机验证的部分(如实说明)

本机没有 Docker / colima / podman(docker 命令不存在),所以:

  • Dockerfile、.dockerignore、docker-compose.yml 未经本机构建与运行验证(只做了语法与配置项静态检查)。
  • 本机真实验证过的是:裸 jar 路径(两个入口 + worker 起停、/healthz、冒烟脚本)、 prod fail-fast(缺密钥时退出码 1 + 明确报错)、配置外置(DB_URL 等改动后仍能起)。
  • 目标主机上有 Docker 之后,请先跑 §三 的 1)–4) 步确认;如报错把 docker compose logs 贴回来。

八之二、上线前必须补的(治理类,详见 DEPLOY-CHECKLIST.md)

以下四条不是"部署命令"层面的问题,但 docs/12-上线检查清单 把它们列为上线条件,此处给出醒目提示:

  1. HTTPS + 独立域名 + 反向代理(管理端/客户端 API/媒资分域名,04-部署架构§四 + 10-安全方案§二)—— 现在只有 IP:HTTP;
  2. 备份恢复:specs 要求每日全量×30 天 + WAL 归档 + 加密 + 异地 + RPO≤5min / RTO≤30min + 季度演练, 现在只有手动 pg_dump,且备份与数据库在同一台机器上(等于没备份);
  3. 监控告警:Prometheus/Grafana/Loki 未部署;第一阶段按 specs 允许用日志兜底,但"容器 unhealthy 要能通知到人";
  4. 商店与合规:隐私政策/Data Safety/SDK 清单/版权台账/包名统一,均未办(印尼还涉及 Kominfo PSE 注册)。

逐条状态与"上线前必补清单(按优先级)"见 DEPLOY-CHECKLIST.md。

九、故障速查

现象 先看 处理
容器起不来,日志有"生产配置校验未通过" §五 补 .env 里对应变量;密钥用 openssl rand -base64 48
/healthz 超时(不是快速 404/200) 是不是刚重新打包没重启? 重启进程(§四 的注意事项)
app-api healthy 但 /app/* 报连库失败 docker compose logs db 确认 DB_URL 指向 db:5432,DB_PASSWORD 与 db 容器一致
转码任务一直 QUEUED worker 日志 确认 transcode-worker 在跑、MEDIA_ROOT_DIR 与 API 相同、镜像里装了 ffmpeg

十、本地开发(不上 Docker)

mvn -pl fruitshorts-app-api,fruitshorts-admin-api -am package -DskipTests
java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar     # 不带 prod profile,开箱即用
tools/smoke.sh                                                                  # 本机一键冒烟