架构设计见 docs/architecture/23-ops.md 第 8 节。此处是 Docker Compose 产品的操作手册,供未编写本文档的人按照步骤操作;如果某个步骤不清楚,说明本文档需要修正。
保护哪些内容以及如何保护
| 资产 | 方式 | 位置 |
|---|---|---|
| 数据库 | 从首次启动开始持续归档 WAL,间隔最多 60 秒 | pgwal 卷 |
| 数据库 | 默认每日使用 pg_basebackup 创建基础备份(backup-scheduler) |
pgbackup 卷 |
| 数据库 | 每五分钟创建一次基础备份和 WAL 的加密副本(backup-offsite) |
您指定的独立存储 |
| 文件 | files 卷。可用主机备份工具复制,或使用启用版本控制的对象存储 |
files 卷 |
| 密钥 | docker/.env,尤其是 QUIRE_MASTER_KEY(以及仍在使用的 QUIRE_MASTER_KEY_RETIRED)、QUIRE_BACKUP_ENCRYPTION_KEY 和 docker/secrets/audit-signing-key.pem |
在此主机之外保存副本 |
| 搜索索引、缓存、媒体版本 | 不备份,之后重新生成 |
目标:发生故障后最多丢失 60 秒数据,并在 60 分钟内恢复 500 GB 数据库。
常见错误有两种。数据库恢复后未恢复文件,页面就会显示损坏。数据库恢复后没有 QUIRE_MASTER_KEY,则无法解密其中保存的 SSO、webhook 和集成凭据;在主密钥轮换完成且没有未解决项之前(参见 key-rotation.md),还包括退休密钥。这些都属于备份内容。
创建备份
创建整个集群的基础备份:
docker compose -f docker/compose.yaml --profile backup run --rm backup系统会保留最新的 QUIRE_BACKUP_KEEP 个基础备份(默认值为 5),并清理最旧备份不再需要的 WAL,因此归档不会无限增长。请在主机上通过 cron 或 systemd 定时器每日安排备份:
15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1也可以让服务栈自动安排备份:backup 配置档案会启动 backup-scheduler,每隔 QUIRE_BACKUP_INTERVAL_HOURS 小时(默认 24 小时)创建一次基础备份,并启动下文所述的 backup-offsite。
docker compose -f docker/compose.yaml --profile backup up -d异地加密副本
这两个卷和数据库位于同一主机上;故障主机上的备份不算真正的备份。backup-offsite 会通过存储端口,将每份基础备份和归档 WAL 段加密后复制到独立存储,并按保留规则保存:
- 加密:使用
QUIRE_BACKUP_ENCRYPTION_KEY(或QUIRE_BACKUP_ENCRYPTION_KEY_FILE指定的文件)进行 AES-256-GCM 加密,密钥为 32 字节,可用openssl rand -hex 32生成。每个文件都有自己的 nonce 和认证标签,因此没有密钥就无法读取副本,任何更改也会被检测到。请将此密钥与QUIRE_MASTER_KEY一起保存在此主机和备份存储之外。没有该密钥就无法恢复。 - 位置:
QUIRE_BACKUP_STORAGE_DRIVER可设为s3、azure或local(挂载到QUIRE_BACKUP_STORAGE_ROOT的远程磁盘)。设置与文件存储相同,但前缀为QUIRE_BACKUP_:例如QUIRE_BACKUP_S3_ENDPOINT、QUIRE_BACKUP_S3_BUCKET、QUIRE_BACKUP_S3_ACCESS_KEY_ID等。请使用不同的存储桶,最好也使用与文件存储不同的账户;如服务提供商允许,凭据应能写入但不能删除。 - 保留期限:保留最新的
QUIRE_BACKUP_OFFSITE_KEEP个基础备份(默认值取QUIRE_BACKUP_KEEP,否则为 7),以及最旧基础备份所需的 WAL;存储中的旧备份集和片段会被删除。 - 频率:每隔
QUIRE_BACKUP_SHIP_INTERVAL_SECONDS秒发送一次(默认 300 秒)。传输具有幂等性:已存储的内容会跳过;只有在最后写入清单后,基础备份才算存储完成。
也可以手动运行相同命令:
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts ship
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify要在新主机恢复,请先取回一组备份,然后按照下文步骤操作;将下载的目录作为 pgbackup 卷的替代,并将下载的 wal-archive 作为 pgwal 的替代:
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore恢复到某个时间点
发生数据丢失后使用此流程,例如导入错误、课程误删,或需要撤销的契约迁移。此操作会替换线上数据库,因此请先按下方演练进行排练。
- 选择目标时间,使用 UTC,并选在损坏发生之前,例如
2026-09-24 09:30:00+00。通常可从审计日志(/admin/audit)查看具体时间。 - 停止所有写入服务:
docker compose -f docker/compose.yaml stop web content worker scheduler collab。 - 保留受损集群,直到确认恢复成功:
docker compose -f docker/compose.yaml stop postgres docker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/ - 将早于目标时间的最新基础备份解压到数据卷,并请求定向恢复:
docker run --rm -v quire_pgbackup:/backups:ro -v quire_postgres18-data:/var/lib/postgresql postgres:18-alpine sh -euc ' base="$(ls -1d /backups/base-* | sort | tail -n 1)" # or the one before the target rm -rf /var/lib/postgresql/18/docker && mkdir -p /var/lib/postgresql/18/docker tar -xzf "$base/base.tar.gz" -C /var/lib/postgresql/18/docker touch /var/lib/postgresql/18/docker/recovery.signal chown -R postgres:postgres /var/lib/postgresql/18/docker && chmod 700 /var/lib/postgresql/18/docker' - 执行恢复:通过 Compose 覆盖文件使用恢复设置启动一次 Postgres,避免更改常规配置文件:
覆盖文件会替换整个命令,因此必须重复恢复所需的两项设置:# docker/compose.recover.yaml services: postgres: command: [postgres, -c, "restore_command=cp /var/lib/postgresql/wal-archive/%f %p", -c, "recovery_target_time=2026-09-24 09:30:00+00", -c, recovery_target_action=promote, -c, archive_mode=off, -c, max_connections=200, -c, hba_file=/etc/postgresql/pg_hba.conf]max_connections不得低于主库的设置(否则恢复会因 “insufficient parameter settings” 中止),以及挂载的pg_hba.conf。docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgres docker compose -f docker/compose.yaml logs -f postgres # wait for "database system is ready" - 检查结果后再允许用户访问:验证审计链(
docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts),并确认丢失的数据已恢复。 - 恢复正常运行:
docker compose -f docker/compose.yaml up -d。这会重新启用归档并重启 Postgres,同时开始新的 WAL 时间线。请立即创建新的基础备份。
每个卷名称前都有项目名 quire;运行 docker volume ls 可查看准确名称。
定期验证演练
backup-offsite 还会每隔 QUIRE_BACKUP_DRILL_INTERVAL_HOURS 小时运行一次演练(默认 168,即每周一次);演练失败后,也会在下一轮再次执行。它会获取最新的异地主基础备份及之后的所有 WAL 片段,逐个解密(证明密钥仍可解开文件且内容未被更改),将每个文件与清单比较,确认归档是 Postgres 数据目录,并检查从基础备份开始的 WAL 是否连续。报告会写入存储中的 reports/drill-<time>.json 和服务日志;演练失败时会指出文件名或第一个缺失的片段。
恢复演练
从未恢复过的备份不算备份。演练会将早于目标时间的最新完整基础备份及 WAL 归档恢复到与线上环境完全隔离的临时 Postgres 中,并验证结果:
docker/scripts/restore-drill.sh # to ninety minutes ago
docker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"目标必须使用 UTC,并严格采用此格式。演练需要早于目标时间的基础备份以及晚于目标时间的归档 WAL:新安装时,请先创建基础备份并等待下一个 WAL 片段归档(有写入时最长约一分钟),再选择晚于备份的目标时间。主机只需要安装 Docker 和 bash。
任一环节失败都会导致演练失败:
- 可恢复性:临时集群可以重放到目标时间并启动。
- 完整性:对比每张表在当前数据库中的行数(
tooling/restore-drill)。当前数据库的数据晚于目标时间,因此任一方向的差异允许达到 500 行或表行数的十分之一(取较大值;写入会使恢复结果落后,删除会使其保留较多记录);目标时间之后创建的分区不算丢失表。较繁忙的安装可用QUIRE_DRILL_MAX_BEHIND和QUIRE_DRILL_MAX_DRIFT_RATIO放宽范围。表缺失或被清空则失败。 - 完整性校验:还原副本中的审计哈希链通过验证。
- 可用性:应用角色可通过行级安全机制读取数据。
- 耗时:从启动到成功的时间满足
QUIRE_DRILL_RTO_SECONDS(默认 3600)。
演练绝不会向线上数据库或其卷写入数据:备份和 WAL 卷以只读方式挂载,临时集群无论成功还是失败都会在结束时删除。
设置 QUIRE_DRILL_REPORT 为某个路径,可在成功或失败后都写入 JSON 报告;您可以从 Docker 主机安排定期运行:
30 3 1 * * cd /srv/quire && QUIRE_DRILL_REPORT=/var/log/quire-drill.json docker/scripts/restore-drill.sh >> /var/log/quire-drill.log 2>&1每月运行一次,并在每次升级前运行。演练失败会阻止升级。每季度请让一位没有撰写本操作手册的人仅根据本文档,在备用主机上执行真正的时间点恢复。
文件
本地文件保存在 files 卷中。请与数据库同时备份,并在恢复时一起还原:
docker run --rm -v quire_files:/files:ro -v "$PWD":/out alpine tar -czf /out/files-$(date -u +%Y%m%d).tar.gz -C /files .使用对象存储时,请启用存储桶版本控制并保留 35 天的非当前版本;这样,文件的时间点恢复由存储桶自身负责。