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]>
This commit is contained in:
1 parent
8c6a922cfb
commit
34e8751b45
18 files changed
+918
-43
No files matched your search
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# 备份:pg_dump(自定义格式,整库,含 secret 表密文与任务引擎 schema)+ 映射清单 JSON + Agent 会话归档
|
||||
# → $DATA_DIR/backups/<UTC 时间戳>/。SECRET_MASTER_KEY 不在备份里,必须另行保管(没有它 secret 表无法解密)。
|
||||
#
|
||||
# 容器内:docker compose exec app umt backup
|
||||
# 环境变量:DATABASE_URL(必需)、DATA_DIR(缺省 /data)、BACKUP_KEEP(保留最近 N 份,缺省不轮转)、
|
||||
# UMT_BIN(缺省 /app/umt-server,开发机可指向 dist/umt-server)
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(id -u)" = "0" ] && command -v gosu >/dev/null 2>&1; then
|
||||
exec gosu "${PUID:-10001}:${PGID:-10001}" "$0" "$@"
|
||||
fi
|
||||
|
||||
: "${DATABASE_URL:?需要 DATABASE_URL}"
|
||||
DATA_DIR="${DATA_DIR:-/data}"
|
||||
BIN="${UMT_BIN:-/app/umt-server}"
|
||||
KEEP="${BACKUP_KEEP:-0}"
|
||||
ROOT="$DATA_DIR/backups"
|
||||
|
||||
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
dest="$ROOT/$ts"
|
||||
work="$ROOT/.$ts.partial"
|
||||
mkdir -p "$work"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
echo "backup: pg_dump → $dest/db.dump"
|
||||
pg_dump --format=custom --compress=6 --no-owner --no-privileges --file "$work/db.dump" "$DATABASE_URL"
|
||||
|
||||
echo "backup: 导出映射清单 → $dest/mappings.json"
|
||||
"$BIN" --role export-mappings --out "$work/mappings.json" >&2
|
||||
|
||||
sessions="${AGENT_SESSION_DIR:-$DATA_DIR/agent-sessions}"
|
||||
if [ -d "$sessions" ] && [ -n "$(ls -A "$sessions" 2>/dev/null)" ]; then
|
||||
echo "backup: 归档 Agent 会话 → $dest/agent-sessions.tar.gz"
|
||||
tar -C "$sessions" -czf "$work/agent-sessions.tar.gz" .
|
||||
fi
|
||||
|
||||
{
|
||||
echo "created_at=$ts"
|
||||
echo "pg_dump=$(pg_dump --version)"
|
||||
echo "server_version=$(psql "$DATABASE_URL" -Atqc 'show server_version' 2>/dev/null || echo unknown)"
|
||||
echo "master_key=NOT_INCLUDED(SECRET_MASTER_KEY 需另行保管)"
|
||||
} >"$work/manifest.txt"
|
||||
(cd "$work" && sha256sum db.dump mappings.json $([ -f agent-sessions.tar.gz ] && echo agent-sessions.tar.gz) >SHA256SUMS)
|
||||
|
||||
mv "$work" "$dest"
|
||||
trap - EXIT
|
||||
echo "backup: 完成 $dest ($(du -sh "$dest" | cut -f1))"
|
||||
|
||||
if [ "$KEEP" -gt 0 ] 2>/dev/null; then
|
||||
# 只轮转符合时间戳命名的目录,按名字(即时间)倒序保留最近 KEEP 份
|
||||
mapfile -t all < <(find "$ROOT" -mindepth 1 -maxdepth 1 -type d -name '[0-9]*T[0-9]*Z' -printf '%f\n' | sort -r)
|
||||
for old in "${all[@]:$KEEP}"; do
|
||||
echo "backup: 轮转删除 $ROOT/$old"
|
||||
rm -rf "${ROOT:?}/$old"
|
||||
done
|
||||
fi
|
||||
Reference in new issue
Block a user