Files
UHD_Media_Tool/docs/runbook/deploy.md
T
rootandClaude Opus 5.5 34e8751b45 feat(ops): W4-B single-container packaging, DATA_DIR, backup/restore
- DATA_DIR (/data: agent-sessions, backups, exports, tmp); entrypoint chowns it
  to PUID/PGID then drops privileges via gosu; /app binary stays root read-only
- compile with --allow-read --allow-write so mounted library roots are writable
- preflight: DATA_DIR/session dir must be writable (fatal); enabled library
  roots and downloader path mappings checked read/write (warn + audit)
- --role export-mappings (mapping manifest JSON, --compare row counts) and
  --role read-only-on (automation.readOnlyReconcile=true + audit)
- scripts/backup.sh (pg_dump custom + mappings + agent sessions, SHA256SUMS,
  BACKUP_KEEP rotation) and scripts/restore.sh (checksum, refuses with live
  connections, psql single-transaction replay compatible with PG16, read-only
  reconcile on, row-count verification)
- compose: app + postgres only, data volume, env example with key generation
- docs: runbook deploy.md, backup-restore.md with verification record

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 21:27:55 +08:00

149 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署运行手册(W4-B)
形态:**除 PostgreSQL 外全部在一个容器里**。镜像 `uhd-media-tool` 内一个 `deno compile` 出的二进制 `umt-server`,
容器里同时跑 api 与 worker 两个进程,api 同端口托管 web 静态产物(`WEB_ROOT=/app/public`),对外只有 **4380** 一个端口。
compose 只有 `app` + `postgres` 两个服务。
## 1. 构建镜像
```sh
docker build -t uhd-media-tool:latest . # 或 docker compose build
APP_VERSION=v0.1.0 docker compose build # 版本号写进 web 的 version.json
```
- 多阶段:`denoland/deno:2.9.7`(装依赖 → web 构建 → server 编译)→ `debian:trixie-slim` 运行镜像。
- 运行镜像只额外装 `gosu`(降权)与 `postgresql-client`(备份/恢复用 pg_dump/pg_restore/psql,17 版客户端兼容 16 版服务端)。
- `/app` 下的二进制、静态产物、脚本全部 root 属主只读;运行用户改不了它们。
- 二进制编译时固定权限:`--allow-net --allow-env --allow-sys --allow-read --allow-write`,不给 `--allow-run`
(库根/下载目录由用户挂载,位置不固定,所以读写不按路径限制;写保护靠只读 `/app` + 非 root 运行)。
## 2. 启动
```sh
mkdir -p /opt/umt && cd /opt/umt # 部署目录(放 docker-compose.yml 与 .env)
cp <仓库>/docker/compose.env.example .env
sed -i "s/^SECRET_MASTER_KEY=.*/SECRET_MASTER_KEY=$(openssl rand -hex 32)/" .env
sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$(openssl rand -hex 16)/" .env
docker compose up -d --build
docker compose ps # app 变成 healthy 即可访问
curl -s localhost:4380/api/v1/health
```
**SECRET_MASTER_KEY** 生成一次后不再更换:站点 Cookie、下载器密码、模型 API Key 都用它加密存在 `secret` 表。
把它存进密码管理器或离线介质,**和数据库备份分开保管**;备份里不含它,丢了它备份中的凭证无法解密。
容器启动顺序(`docker/entrypoint.sh`):
1. 以 root 启动:建 `DATA_DIR`(`/data`)下的 `agent-sessions/ backups/ exports/ tmp/`,属主不是 `PUID:PGID` 时 `chown -R`,
然后 `gosu PUID:PGID` 降权重跑自己。`PUID=0` 直接拒绝。
2. `--role migrate`:业务表迁移 + 任务引擎 schema 迁移。
3. `--role preflight`:密钥不是占位符、非 root、数据库可达、`WEB_ROOT/index.html` 存在、**DATA_DIR 各子目录与 Agent 会话目录可写**
(失败即退出);再逐个检查已启用的**库根**与**下载器路径映射**(`view_kind=downloader` 的 local 前缀)可读可写——
这些是用户挂载的目录,不可用只打 `warn` 日志并写审计 `preflight.path_unavailable`,不阻止启动。
4. 同时起 `--role api` 与 `--role worker`;任一退出就停掉另一个并以非 0 退出,由 `restart: unless-stopped` 拉起。
健康检查:镜像 `HEALTHCHECK` 用二进制自带 fetch 打 `127.0.0.1:4380/api/v1/health`(`--role probe`),不装 curl。
## 3. 首次初始化管理员
api 首次启动时在日志里打印一次性 setup token(重启会换新的,旧的作废):
```sh
docker compose logs app | grep 'setup token'
```
浏览器打开 `http://<主机>:4380`,登录页进入初始化模式,填 token、用户名、密码(≥10 字符)。
命令行等价:`POST /api/v1/auth/setup {setupToken, username, password}`。之后在 设置 → API Key 创建给外部程序用的 Key。
## 4. 卷与路径约定
| 容器内路径 | 来源 | 用途 | 权限要求 |
|---|---|---|---|
| `/data` | 命名卷 `umt-data`(或宿主目录) | `agent-sessions/`(Pi JSONL 会话,崩溃恢复点)、`backups/`、`exports/`(映射清单)、`tmp/`(`TMPDIR`) | entrypoint 自动 chown 给 `PUID:PGID` |
| `/media/...` | 用户 bind mount | 媒体库根:控制台 设置 → 媒体库根 登记的 `localPath` | 需对 `PUID:PGID` 可读写;**不会被自动 chown** |
| `/downloads/...` | 用户 bind mount | 下载缓存:路径映射的 local 前缀(下载器报告的 remote 路径 → 容器内路径) | 同上 |
| `/var/lib/postgresql/data`(postgres) | 命名卷 `umt-pg-data` | 数据库 | — |
- 媒体目录在 `docker-compose.yml` 的 `app.volumes` 里取消注释或另写 `docker-compose.override.yml`:
```yaml
services:
app:
volumes:
- /srv/media:/media
- /srv/downloads:/downloads
```
- bind mount 的相对路径按**第一个 compose 文件所在目录**解析,不是按当前目录;不确定时写绝对路径。
源目录不存在时 Docker 会以 root 属主新建它,库根登记就会因哨兵写不进而失败。
- 路径映射:下载器容器里看到的是 `/downloads/complete/...`,本容器也挂成 `/downloads` 时 remote 前缀与 local 前缀相同;
挂到别处就登记 `remote_prefix=/downloads`、`local_prefix=<本容器内路径>`。硬链接要求下载目录与库根在同一文件系统、同一挂载点内。
- 登记库根时服务端会在目录里写哨兵文件 `.uhd-root`,写不进去直接 400(提示检查挂载与属主)。
- **PUID/PGID**:设成宿主媒体目录的属主(`stat -c '%u:%g' /srv/media`)。改 PUID/PGID 后重启容器,entrypoint 会把 `/data` 重新 chown。
也可以 `docker run --user` 直接指定(此时跳过 chown,`/data` 需事先可写)。
- Agent 会话目录缺省 `DATA_DIR/agent-sessions`,可用 `AGENT_SESSION_DIR` 覆盖(api 与 worker 在同一容器里,天然共享)。
## 5. 常用环境变量
| 变量 | 缺省 | 说明 |
|---|---|---|
| `SECRET_MASTER_KEY` | 必填 | 凭证加密主密钥,`openssl rand -hex 32` |
| `POSTGRES_PASSWORD` | 必填 | compose 拼进 `DATABASE_URL` |
| `UMT_PORT` / `UMT_BIND` | `4380` / `0.0.0.0` | 宿主映射端口与绑定地址;放在反代后面时绑 `127.0.0.1` |
| `PUID` / `PGID` | `10001` | 运行用户 |
| `DATA_DIR` | `/data` | 应用数据目录(开发机缺省 `./data`) |
| `AGENT_SESSION_DIR` | `DATA_DIR/agent-sessions` | Agent 会话目录 |
| `BACKUP_KEEP` | `14` | 备份保留份数,0 不轮转 |
| `LOG_LEVEL` | `info` | pino 级别 |
## 6. 升级
```sh
cd <仓库> && git pull
docker compose exec app umt backup # 升级前先备份
docker compose up -d --build app # 重建镜像并替换容器;启动时自动迁移
docker compose ps && docker compose logs --tail=50 app
```
迁移只前滚。升级失败要回退时:用旧镜像 + 升级前的备份恢复(见 [backup-restore.md](backup-restore.md)),不要用旧镜像直接连新库。
## 7. 日志与排障
- `docker compose logs -f app`:pino JSON,一行一条;`role` 字段区分 api / worker。json-file 驱动,单文件 10 MB × 5 份。
- entrypoint 自身的输出(`entrypoint: ...`)说明哪个进程先退出、退出码多少。
- 自检失败(`check=data_dir`):`/data` 卷不可写,检查是否挂了只读卷或 `--user` 与卷属主不一致。
- 挂载路径告警(`挂载路径不可用`):看审计页 `preflight.path_unavailable` 事件,核对 bind mount 与 PUID/PGID。
- 进容器排查:`docker compose exec app bash`(root);以运行用户执行命令用 `gosu 10001:10001 <cmd>`。
- 一次性子命令:`docker compose exec app umt migrate|preflight|export-mappings|backup`、
`docker compose run --rm --no-deps app restore <备份>`。
## 验证记录
2026-09-27,W4-B,Docker 29 / compose v2,在干净 worktree(HEAD 8c6a922 + 本次改动)构建,
临时项目名 `umtw4b`、app 端口 127.0.0.1:4390、pg 端口 127.0.0.1:55440,`/media` 绑定一个属主 10001 的临时目录。
| 步骤 | 结果 |
|---|---|
| `docker build --build-arg APT_MIRROR=mirrors.ustc.edu.cn` | 成功(缓存命中 2 分 24 秒);镜像 489 MB(压缩后 107 MB):二进制 213 MB、gosu+postgresql-client 79 MB、web 2 MB |
| `compose up -d` | postgres healthy 后 app 启动,约 6 秒 healthy;21 个迁移 + 引擎迁移,`启动自检通过 dataDir=/data sessionDir=/data/agent-sessions` |
| 进程 | 两个 `umt-server`(api、worker)uid/gid 均为 10001;`/data/*` 属主 10001;`/app/umt-server` 为 `r-xr-xr-x root`,运行用户写入 → Permission denied |
| `GET /` 与 `/console/settings` | 200,SPA 由同端口托管 |
| setup token(日志)→ `POST /auth/setup` → `POST /auth/login` | 成功,拿到会话与 csrf cookie |
| 会话 + csrf `POST /api-keys {scopes:[admin]}` | 返回 `umt_…` 明文一次 |
| API Key `GET /organization-records` | 200 |
| API Key `POST /library-roots {localPath:/media}` | 成功,宿主目录出现 `.uhd-root`(属主 10001);容器内 root 属主目录登记 → 400 `库根不可写 … EACCES` |
| `PATCH /settings {pi:{provider:fake}}` → `POST /agent/runs {mode:plan_only}` | run `succeeded`,会话文件 `/data/agent-sessions/<runId>.jsonl`(18.7 KB,属主 10001) |
| 宿主 `chmod 555` 媒体目录后 `umt preflight` | 退出码 0,`warn 挂载路径不可用: verify-media 不可写: EACCES`,审计 `preflight.path_unavailable` 一条;恢复权限后 warnings=0 |
| `umt backup` | `db.dump` 245 KB + `mappings.json` + `agent-sessions.tar.gz` + `manifest.txt` + `SHA256SUMS`;pg_dump 17.11 对服务端 16.14 |
| 清库 | `drop schema public cascade` + 删 `openworkflow` schema → 0 张表 |
| app 运行中 `umt restore` | 拒绝:`数据库还有 13 个其它连接` |
| `compose stop app` → `compose run --rm --no-deps app restore <ts> --yes` | 退出码 0;63 张表恢复;抽查 admin_user 1 / api_key 1 / library_root 1 / media_entity 3 / external_identity 3 / agent_run 1 / agent_run_event 9 与备份前一致(audit_event +1 为恢复审计、settings +1 为 automation 行);映射清单逐表比对"一致";`automation={"readOnlyReconcile": true}`,审计 `ops.read_only_reconcile_on` |
| 恢复后 `up -d app` | healthy;`GET /settings` → `automation {enabled: true, readOnlyReconcile: true}` |
| `BACKUP_KEEP=3` 连续备份 3 次 | 最旧一份被轮转删除,剩 3 份 |
| `PUID=1234 PGID=1234` 空卷启动 | `/data` 及子目录 chown 为 1234,进程 uid 1234;`PUID=0` → 退出码 2 |
| `compose stop app` | 0.8 秒,api/worker 均收到 SIGTERM 正常退出 |
| 清理 | `compose down -v`、删除测试镜像;未 prune 全局 build cache |
首次演练发现:镜像里的 pg_restore 17 直连 16 版服务端会因 `SET transaction_timeout` 失败(单事务回滚,库未被破坏),
已改为 pg_restore 输出 SQL、过滤该行后由 psql 单事务回放,复测通过。