为什么:用户要求"写好文档,一定要根据 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 通过
209 lines
13 KiB
Markdown
209 lines
13 KiB
Markdown
# 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](./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。
|
||
|
||
```bash
|
||
# 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`。
|
||
|
||
**日常操作**
|
||
|
||
```bash
|
||
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`(顺序同上)。
|
||
|
||
```bash
|
||
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. 登录一个后台账号,确认能进后台首页(连通数据库与权限体系)。
|
||
|
||
## 七、回滚
|
||
|
||
**镜像/代码回滚**
|
||
|
||
```bash
|
||
# 用上一个版本的 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`:
|
||
|
||
```bash
|
||
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`。
|
||
**发布前先备份**是硬要求:
|
||
|
||
```bash
|
||
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](./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)
|
||
|
||
```bash
|
||
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 # 本机一键冒烟
|
||
```
|