Skip to content

数据持久化与备份

AutoRouter 的运行状态分布在四个位置:PostgreSQL 数据库、autorouter-data 容器卷、CLIProxyAPI 的 cliproxy-authcliproxy-logs 卷(仅启用 sidecar 时存在)、磁盘上的流量录制目录。备份策略需要按位置分别处理,单独备份数据库无法恢复全部状态。本页给出每类持久化位置的备份与恢复样例,附带 named volume 与 bind mount 两种存储形态的变体。

不在本页范围内的内容:删除 sidecar 卷后 OAuth 凭据如何重建见 CLIProxyAPI 首次使用指南;升级 / 回滚的整体流程见 升级与回滚;流量录制本身的运行期配置见 请求录制

持久化位置清单

位置形态内容丢失后果
PostgreSQL 数据库(默认在 postgres-data 卷)docker compose 命名卷上游配置、客户端 Key、熔断状态、请求日志、计费快照、CLIProxy 实例与账号注册系统状态归零,需要重新登记上游与 Key
autorouter-datadocker compose 命名卷容器内 /app/data;生产 PG 部署默认包含 /app/data/traffic-recordings 录制文件,SQLite 模式还包含 dev.sqlite录制文件丢失会使详情 / 回放失效;SQLite 数据库丢失会导致本地数据归零
cliproxy-authdocker compose 命名卷(sidecar)Codex / Claude / Gemini 的 OAuth token 明文所有账号需要在 CLIProxyAPI 管理端重新 OAuth 登录
cliproxy-logsdocker compose 命名卷(sidecar)CLIProxyAPI 的运行日志仅丢历史日志,不影响运行
自定义流量录制目录(RECORDER_FIXTURES_DIR容器内目录或绑定挂载当 env 覆盖默认路径时的请求 / 响应 fixture;数据库 traffic_recordings 表仅存索引索引仍在,但 fixture_path 指向的文件已丢失,回放与详情查看失效
ENCRYPTION_KEY(不在卷里,但同等关键).env 文件Fernet 加密密钥,用于解密上游 API Key、CLIProxy 凭据等敏感字段数据库行还在,但所有加密字段都无法解密;上游配置必须逐条手工重填

备份策略必须覆盖 .env

.env 中的 ENCRYPTION_KEY 不存在于任何 named volume 中,标准的 docker volume 备份命令不会带上它。一旦 .env 丢失且没有离线副本,即使 PG 数据库完整恢复,所有上游凭据仍然不可读。.env 必须作为独立项纳入备份计划,建议在密码管理器或离线介质中保留至少一份。

docker named volume 与 bind mount 对照

docker-compose.yml(仓库内默认)使用 named volume:

yaml
volumes:
  autorouter-data:
  postgres-data:

services:
  autorouter:
    volumes:
      - autorouter-data:/app/data
  db:
    volumes:
      - postgres-data:/var/lib/postgresql/data

named volume 的实际路径由 Docker 管理,宿主机上通常位于 /var/lib/docker/volumes/<volume-name>/_data。Compose 启动时会自动在卷名前加 project 前缀,宿主机上看到的实际名为 <project>_<volume-name>/opt/autorouter 部署目录对应的 project 名通常是 autorouter,因此实际卷名形如 autorouter_postgres-data

若希望把卷数据直接放到宿主机指定目录(便于现有备份策略复用),改用 bind mount:

yaml
# docker-compose.override.yml
services:
  autorouter:
    volumes:
      - /var/lib/autorouter/data:/app/data
  db:
    volumes:
      - /var/lib/autorouter/postgres:/var/lib/postgresql/data

bind mount 的目录由运维方自行准备,权限由宿主机文件系统决定。PG 数据目录在大多数 Linux 发行版上需要属主 UID 999(容器内 postgres 用户的 UID),否则 db 容器会在启动期报权限错误:

bash
sudo mkdir -p /var/lib/autorouter/postgres
sudo chown -R 999:999 /var/lib/autorouter/postgres

docker-compose.override.yml 与主 docker-compose.ymldocker compose up 时按文件名顺序合并,无需再带 -f

PostgreSQL 备份

数据库是状态最丰富的位置,备份选 pg_dump 即可。下面给出三种典型场景。

方案 A:在主机上调 docker exec 执行 pg_dump

适用于「应用容器与 DB 容器都在同一台主机」的常见场景。

bash
# 1. 用容器内的 pg_dump 把整个数据库 dump 到主机
docker exec autorouter-db \
  pg_dump --clean --if-exists -U autorouter autorouter \
> /backup/autorouter-$(date +%Y%m%d-%H%M%S).sql

# 2. 压缩
gzip /backup/autorouter-*.sql

--clean --if-exists 让 dump 在 restore 时先 DROP 旧对象,避免恢复到非空库时冲突。autorouter 是用户名与数据库名,按 .env 实际值调整。

方案 B:定时备份(cron)

bash
# /etc/cron.d/autorouter-backup
0 3 * * *  root  /usr/local/bin/autorouter-backup.sh
bash
#!/bin/bash
# /usr/local/bin/autorouter-backup.sh
set -euo pipefail

BACKUP_DIR=/backup/autorouter
RETENTION_DAYS=14
DATE_TAG=$(date +%Y%m%d-%H%M%S)

mkdir -p "${BACKUP_DIR}"

docker exec autorouter-db \
  pg_dump --clean --if-exists -U autorouter autorouter \
  | gzip > "${BACKUP_DIR}/db-${DATE_TAG}.sql.gz"

# 同步备份 .env(因为 ENCRYPTION_KEY 在这里)
cp /opt/autorouter/.env "${BACKUP_DIR}/env-${DATE_TAG}.env"

# 清理超过保留期的旧备份
find "${BACKUP_DIR}" -name "db-*.sql.gz" -mtime +${RETENTION_DAYS} -delete
find "${BACKUP_DIR}" -name "env-*.env" -mtime +${RETENTION_DAYS} -delete

.env 必须随 dump 一起备份,否则 dump 恢复后所有加密字段无法解密。

方案 C:物理备份(停机)

只在「主机维护期、明确停机」时使用。直接 tar 整个 PG 数据目录:

bash
docker compose stop db
sudo tar czf /backup/autorouter-pgdata-$(date +%Y%m%d-%H%M%S).tar.gz \
  -C /var/lib/docker/volumes/autorouter_postgres-data _data
docker compose start db

物理备份的 restore 路径是「停机 → 解压回 _data → 启动」,跨 PG 主版本时不通用,平常不推荐。

PostgreSQL 恢复

bash
# 1. 准备一个空数据库(如果是全新机器,按 .env 先 docker compose up -d db 即可)
docker compose up -d db
docker exec -i autorouter-db \
  dropdb -U autorouter --if-exists autorouter
docker exec -i autorouter-db \
  createdb -U autorouter autorouter

# 2. 灌入备份
gunzip < /backup/autorouter-db-20260524-030001.sql.gz \
  | docker exec -i autorouter-db psql -U autorouter -d autorouter

# 3. 若 .env 也丢失,先把备份的 .env 还原
cp /backup/autorouter/env-20260524-030001.env /opt/autorouter/.env

# 4. 启动应用
docker compose up -d

docker exec -i-i 是必须的,否则 stdin 不会传入容器内的 psql

跨主版本 dump / restore

如果备份来源是 postgres:16、目标主机用了 postgres:17,建议先在目标主机用同版本的 pg_dump 再 dump 一遍(或直接迁数据库版本前先做 dump)。否则 dump 中包含的 pg_dump 版本声明与目标版本不一致时偶发警告。

CLIProxyAPI cliproxy-auth 备份

cliproxy-auth 存的是 OAuth token 明文,丢失等于「所有账号需要 CLIProxyAPI 管理端重新 OAuth 登录」。如果接入的账号比较多,备份它能省下大量重做登录的时间。

在线热备(推荐)

借助一次性容器把 named volume 的内容打包出来:

bash
docker run --rm \
  -v autorouter_cliproxy-auth:/source:ro \
  -v /backup:/backup \
  alpine \
  sh -c 'cd /source && tar czf /backup/cliproxy-auth-$(date +%Y%m%d-%H%M%S).tar.gz .'

参数解释:

参数作用
-v autorouter_cliproxy-auth:/source:ro以只读方式挂入实际的卷(注意:实际卷名带 <project>_ 前缀)
-v /backup:/backup挂入主机的备份目录
alpine + sh -c '...tar czf...'用临时容器打包;用 alpine 避免镜像膨胀

实际项目前缀按 docker volume ls --filter name=cliproxy 的输出取。

恢复

bash
# 1. 创建(或清空)目标卷
docker volume create autorouter_cliproxy-auth

# 2. 把备份回灌
docker run --rm \
  -v autorouter_cliproxy-auth:/target \
  -v /backup:/backup:ro \
  alpine \
  sh -c 'cd /target && tar xzf /backup/cliproxy-auth-20260524-030001.tar.gz'

# 3. 启动 sidecar
docker compose -f docker-compose.yml -f docker-compose.cliproxy.yml up -d cliproxyapi

流量录制目录备份

recordTrafficFixturesrc/lib/services/traffic-recorder.ts:533)把录制内容以 JSON 写到 RECORDER_FIXTURES_DIR 指向的目录。数据库 trafficRecordings 表只存元数据与 fixture_path 路径。这意味着:

  • 单独备份 PG 不足以恢复录制;恢复后详情页打开会找不到文件。
  • 单独备份录制目录也不够;查询索引、过滤、统计都依赖 PG。

完整的录制备份必须 PG 与录制目录一起做。

生产默认路径

docker-compose.yml 默认将 RECORDER_FIXTURES_DIR 指向 /app/data/traffic-recordings,该目录位于 autorouter-data named volume 中。容器重建后,录制文件会随卷保留。

旧部署迁移

如果旧部署实际使用过 tests/fixtures(无论来自 .env 显式配置还是旧版本 Compose 默认值),不要直接重建容器。旧目录在容器的可写层中,容器重建后可能丢失;必须先备份仍存在的 fixture 文件和 PostgreSQL,再把文件迁入 autorouter-data,同步更新 PostgreSQL 中的 traffic_recordings.fixture_path,最后才切换配置。已经随旧容器丢失的文件无法由数据库索引恢复。

从旧 tests/fixtures 切换

以下步骤使用仓库默认的 service/container 名称;如果项目名不同,按 docker volume ls 的实际卷名替换 autorouter_autorouter-data。迁移前先停止应用,避免备份期间继续写入旧目录。pg_dumppsqldb 容器内读取 Compose 注入的 POSTGRES_USERPOSTGRES_DB,不依赖宿主 shell 是否导出了 .env

bash
mkdir -p backup/autorouter/legacy-fixtures
docker compose stop autorouter
docker cp autorouter:/app/tests/fixtures/. ./backup/autorouter/legacy-fixtures/
docker compose exec -T db \
  sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
> backup/autorouter/postgres-before-recording-migration.sql

把仍存在的 fixture 文件复制到持久化卷,并保留 tests/fixtures/ 后面的目录结构:

bash
docker run --rm \
  -v autorouter_autorouter-data:/target \
  -v "$PWD/backup/autorouter/legacy-fixtures:/source:ro" \
  alpine \
  sh -c 'mkdir -p /target/traffic-recordings && cp -a /source/. /target/traffic-recordings/'

确认旧路径格式后,将数据库索引改为新绝对路径:

bash
docker compose exec -T db \
  sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
UPDATE traffic_recordings
SET fixture_path = CASE
  WHEN fixture_path LIKE 'tests/fixtures/%' THEN
    '/app/data/traffic-recordings/' ||
    substring(fixture_path from length('tests/fixtures/') + 1)
  WHEN fixture_path LIKE '/app/tests/fixtures/%' THEN
    '/app/data/traffic-recordings/' ||
    substring(fixture_path from length('/app/tests/fixtures/') + 1)
  ELSE fixture_path
END
WHERE fixture_path LIKE 'tests/fixtures/%'
   OR fixture_path LIKE '/app/tests/fixtures/%';

最后将 .env 改为 RECORDER_FIXTURES_DIR=/app/data/traffic-recordings 并执行 docker compose up -d。如果暂时必须保留旧 override,就必须另行把宿主机目录挂载到 /app/tests/fixturesautorouter-data:/app/data 不会持久化容器可写层中的旧路径。

bash
docker run --rm \
  -v autorouter_autorouter-data:/source:ro \
  -v /backup:/backup \
  alpine \
  sh -c 'cd /source && tar czf /backup/autorouter-data-$(date +%Y%m%d-%H%M%S).tar.gz .'

不需要长期保留录制时,可在管理后台「系统 → 请求录制」面板配置 retention_days 让后台清理任务自动处理。

bind mount 形态下的备份

把所有 named volume 都改成 bind mount 后,备份命令变得跟普通文件备份没区别:

bash
# PG 数据 + 应用数据 + sidecar
sudo tar czf /backup/autorouter-$(date +%Y%m%d-%H%M%S).tar.gz \
  -C / \
  var/lib/autorouter/postgres \
  var/lib/autorouter/data \
  var/lib/autorouter/cliproxy-auth \
  opt/autorouter/.env

bind mount 的好处:与现有备份系统(borg / restic / 普通 rsync)无缝衔接、.env 可以放在同一棵子树内一起备份。代价:宿主机权限策略需要自己维护,PG 数据目录的 UID 要对齐。

不要在备份中包含 cliproxy-logs

cliproxy-logs 仅是日志,丢了不影响业务,长期备份反而浪费空间。备份脚本中可以明确排除:

bash
tar --exclude='*/cliproxy-logs/*' ...

验证备份

备份能成功生成不等于能成功恢复。建议每月做一次完整恢复演练:

  1. 在备用机或同主机的另一个 project(用 COMPOSE_PROJECT_NAME=autorouter-restore)启动一套空栈。
  2. 按上述步骤恢复 PG dump、.envcliproxy-auth、录制目录。
  3. 启动栈,登录管理后台,验证:上游列表、客户端 Key 列表、CLIProxyAPI 实例「连通性检测」、最近一笔请求日志详情都能正常打开。

只跑命令不验证恢复结果,遇到真实故障时常会发现备份缺一项。

来源对照

  • docker-compose.ymlautorouter-datapostgres-data 卷声明
  • docker-compose.cliproxy.ymlcliproxy-authcliproxy-logs 卷声明(带 sidecar 时)
  • src/lib/services/traffic-recorder.tssrc/lib/services/traffic-recording-service.ts:录制目录的实际写入位置
  • src/lib/db/schema-pg.tstraffic_recordings 表的 fixture_path 字段:解释为何数据库与目录必须同步备份
  • .github/workflows/deploy-personal.yml:远端 .env 由 CI 首次生成并维护,备份必须独立纳入

Released under the AGPL-3.0 License.