备份与恢复
CPAMP 的请求历史、配置和加密凭证都在本机。备份时最容易犯的错,是只复制 usage.sqlite,漏掉 WAL/SHM、data.key 或安装目录里的 secret 文件。
必备备份文件
至少把这些文件作为一组备份:
usage.sqliteusage.sqlite-walusage.sqlite-shmdata.keyusage-archives/(若存在;包括原始请求归档及清单,不可只备份 SQLite)usage.sqlite-journal(若存在)
如果部署目录还有自定义配置文件,也应一起备份。使用一键安装脚本时,至少额外备份安装目录中的 secrets/ 和 data/;成功导入后通常不会再有 secrets/cpa-management-key,但升级失败或 CPAMP_SKIP_EXECUTE=1 时该临时文件可能仍需保留以便重试。手动 env/secret 部署仍应备份对应 secret 文件。
为什么必须备份 data.key
通过 setup 或面板保存的 CPA 连接,会把 CPA Management Key 使用 data.key 加密后保存到 SQLite。
- 只有
usage.sqlite泄露时,攻击者不能直接读出 CPA Management Key。 usage.sqlite和data.key同时泄露时,CPA Management Key 可被解密。- 丢失
data.key时,已经保存的 CPA Management Key 无法恢复,只能重新保存 CPA 连接配置。
如果 CPA 连接由手动环境变量或 secret 文件管理,CPA Management Key 可能不写入 SQLite;请把对应的 secret 文件和数据目录作为一组备份。一键安装器的 env 输入会在成功后迁移到 SQLite,不应只备份一次性输入文件。
v2 Beta:Embedded 全栈备份与恢复
v2 Beta 从 v1.14.10 离线升级,目标 CPA 最低 v8.0.55;备份不是迁移授权,仍须通过 升级预检与对账。Manager 的完整备份集合是 usage.sqlite、WAL/SHM(存在时)、data.key、usage-archives/(存在时)及自定义配置/secret。丢失 data.key 无法通过重置 Admin Key 修复。
Embedded 还必须备份整个 Runtime 卷:实际路径为 /runtime/gateway/config.yaml、/runtime/gateway/auth/(单数 auth)、/runtime/gateway/plugins/(存在时)和 /runtime/supervisor/(含 operations.sqlite、WAL/SHM、artifacts/active 状态)。不要只复制 CPA 配置。保存 transport secret 卷 /run/cpamp/runtime-secret、同版 Compose/配对记录、公开 origin 与环境配置;备份应加密并限制访问。
在发布 Compose 所在目录、同一项目下,将以下完整子 shell 块作为一次操作执行。mktemp -d 创建私有唯一目录,容器内 set -C 拒绝覆盖已有归档;set -eu 在任一步失败时立即停止,不自动重启服务。记录打印的备份路径:
(
set -eu
umask 077
BACKUP_DIR="$(mktemp -d "$PWD/cpampd-backup.XXXXXX")"
printf 'Backup directory: %s\n' "$BACKUP_DIR"
docker compose -f docker-compose.v2-embedded.yml stop
docker compose -f docker-compose.v2-embedded.yml run --rm --no-deps \
-v "$BACKUP_DIR":/backup --entrypoint sh cpamp-manager -ec '
set -C
tar -czpf - -C /data . > /backup/manager.tgz
gzip -t /backup/manager.tgz
tar -tzf /backup/manager.tgz > /dev/null
'
docker compose -f docker-compose.v2-embedded.yml run --rm --no-deps \
-v "$BACKUP_DIR":/backup --entrypoint sh cpamp-runtime -ec '
set -C
tar -czpf - -C /runtime . > /backup/runtime.tgz
gzip -t /backup/runtime.tgz
tar -tzf /backup/runtime.tgz > /dev/null
'
docker compose -f docker-compose.v2-embedded.yml run --rm --no-deps \
-v "$BACKUP_DIR":/backup --entrypoint sh cpamp-runtime -ec '
set -C
tar -czpf - -C /run/cpamp/runtime-secret . > /backup/runtime-secret.tgz
gzip -t /backup/runtime-secret.tgz
tar -tzf /backup/runtime-secret.tgz > /dev/null
'
docker compose -f docker-compose.v2-embedded.yml up -d
)只有三份归档各自写入、gzip 完整性检查及 tar 目录读取全部成功,才会运行最后的 up -d。任一步失败时保留目录及错误信息,服务保持停止,排除原因后重新创建新的备份目录;不在半成品目录重写归档。命令复用已固定 Digest 的镜像,离线期间不能有其他 Manager/CPA 写入原卷。
恢复到三个全新、已核对为空的卷
先准备备份对应的 Compose/配对记录,并恢复原公开 origin/环境配置,保留原卷不动。以下 BACKUP_DIR 必须替换为上一节成功备份的真实绝对目录。其余 RESTORE_* 仅是本段 shell/Compose override 的变量,不是新的 CPAMP 配置项。脚本先生成并校验合法的恢复项目名、验证合并后的 Compose 配置和新卷名;这些检查全部通过后,才停止原项目、创建三个新卷。通过 override 显式绑定新卷,确认空目录后才解压。不要删除原项目的卷来制造“空卷”。
(
set -eu
umask 077
BACKUP_DIR='/absolute/path/to/successful/cpampd-backup.XXXXXX'
test -d "$BACKUP_DIR"
RESTORE_DIR="$(mktemp -d "$PWD/cpampd-restore.XXXXXX")"
RESTORE_PROJECT="$(basename "$RESTORE_DIR" | tr '[:upper:].' '[:lower:]-')"
case "$RESTORE_PROJECT" in
''|[!a-z0-9]*|*[!a-z0-9_-]*)
printf 'Invalid restore project name: %s\n' "$RESTORE_PROJECT" >&2
exit 1 ;;
esac
RESTORE_MANAGER_VOLUME="$RESTORE_PROJECT-manager"
RESTORE_RUNTIME_VOLUME="$RESTORE_PROJECT-runtime"
RESTORE_SECRET_VOLUME="$RESTORE_PROJECT-secret"
printf 'Restore project: %s\nRestore directory: %s\n' "$RESTORE_PROJECT" "$RESTORE_DIR"
cat > "$RESTORE_DIR/volumes.yml" <<YAML
volumes:
cpamp-manager-data:
external: true
name: "$RESTORE_MANAGER_VOLUME"
cpamp-runtime-data:
external: true
name: "$RESTORE_RUNTIME_VOLUME"
cpamp-runtime-transport-secret:
external: true
name: "$RESTORE_SECRET_VOLUME"
YAML
restore_compose() {
docker compose -p "$RESTORE_PROJECT" -f docker-compose.v2-embedded.yml \
-f "$RESTORE_DIR/volumes.yml" "$@"
}
restore_compose config
for volume in "$RESTORE_MANAGER_VOLUME" "$RESTORE_RUNTIME_VOLUME" "$RESTORE_SECRET_VOLUME"; do
if docker volume inspect "$volume" > /dev/null 2>&1; then
printf 'Refusing existing volume: %s\n' "$volume" >&2
exit 1
fi
done
docker compose -f docker-compose.v2-embedded.yml stop
for volume in "$RESTORE_MANAGER_VOLUME" "$RESTORE_RUNTIME_VOLUME" "$RESTORE_SECRET_VOLUME"; do
docker volume create "$volume"
done
restore_compose run --rm --no-deps --entrypoint sh cpamp-manager -ec '
entries="$(find /data -mindepth 1 -maxdepth 1 -print -quit)"
test -z "$entries"
'
restore_compose run --rm --no-deps --entrypoint sh cpamp-runtime -ec '
for root in /runtime /run/cpamp/runtime-secret; do
entries="$(find "$root" -mindepth 1 -maxdepth 1 -print -quit)"
test -z "$entries"
done
'
restore_compose run --rm --no-deps -v "$BACKUP_DIR":/backup:ro \
--entrypoint sh cpamp-manager -ec '
for archive in manager runtime runtime-secret; do
gzip -t "/backup/$archive.tgz"
tar -tzf "/backup/$archive.tgz" > /dev/null
done
'
restore_compose run --rm --no-deps -v "$BACKUP_DIR":/backup:ro \
--entrypoint tar cpamp-manager -xzpf /backup/manager.tgz -C /data
restore_compose run --rm --no-deps -v "$BACKUP_DIR":/backup:ro \
--entrypoint tar cpamp-runtime -xzpf /backup/runtime.tgz -C /runtime
restore_compose run --rm --no-deps -v "$BACKUP_DIR":/backup:ro \
--entrypoint tar cpamp-runtime -xzpf /backup/runtime-secret.tgz -C /run/cpamp/runtime-secret
restore_compose up -d
)脚本只在三个空卷检查、三份归档校验和三次解压全部成功后启动恢复项目;失败则不启动,保留原卷及恢复目录以便诊断,再从另一组新空卷重试。后续必须保留打印的恢复项目名及 volumes.yml,始终使用同一 -p 和两份 -f 参数管理恢复实例;直接运行原项目命令会使用原卷,不是恢复卷。两个项目不能同时占用同一公开端口。
保留归档属主、权限及隔离,不把整个数据目录递归改为 CPA 身份。核对 Admin 登录、CPA 连接、Client Key、历史/coverage 与 Gateway;/health 单独成功不代表全部恢复。回退 1.x 必须使用迁移前的原 1.x 卷,不能使用 v2 归档或恢复卷。
Manager 离线快照的边界
manager-data-snapshot 在停止 Manager 后为数据库、WAL/SHM、rollback journal 和 data.key 创建带完整性清单的快照;不包含 usage-archives/、自定义配置、Runtime 或外部 CPA,不能替代全量备份。在原生 v2 包根目录执行以下备份命令(快照目录必须尚不存在):
sudo ./bin/cpa-manager-plus manager-data-snapshot create \
--db-path "$PWD/data/usage.sqlite" --data-key-path "$PWD/data/data.key" \
--snapshot-dir "$PWD/backup/manager-snapshot"只有需要恢复时才运行:
sudo ./bin/cpa-manager-plus manager-data-snapshot restore \
--db-path "$PWD/data/usage.sqlite" --data-key-path "$PWD/data/data.key" \
--snapshot-dir "$PWD/backup/manager-snapshot"锁被占用时命令拒绝,先停止实际 Manager。Docker 可用同一 Compose 的 run --rm --no-deps cpamp-manager manager-data-snapshot ...,指定 /data/usage.sqlite、/data/data.key 和已挂载的持久化快照目录。Native 完整备份/恢复还要包括同次的 data/runtime/ 及 run/ 中的 secret,保留属主/权限。
External 的 CPA 配置、OAuth 凭证及进程由操作者独立备份/恢复;Manager 快照不会备份远端主机。只保留配置的导出也不是 v2 完整升级路径。
1.x Stable:备份示例
Docker 备份示例
如果使用 named volume,可以先停止容器,再用临时容器导出:
docker stop cpa-manager-plus
docker run --rm \
-v cpa-manager-plus-data:/data:ro \
-v "$PWD":/backup \
alpine \
tar czf /backup/cpa-manager-plus-data.tgz -C /data .
docker start cpa-manager-plus如果使用宿主机目录挂载:
docker stop cpa-manager-plus
cp -a /srv/cpa-manager-plus-data /srv/cpa-manager-plus-data.backup
docker start cpa-manager-plus原生包备份
停止进程后复制数据目录:
cp -a ./data ./data.backupWindows PowerShell:
Copy-Item -Recurse .\data .\data.backup恢复
- 停止 CPAMP。
- 恢复完整数据目录。
- 确认
usage.sqlite和data.key来自同一次备份。 - 如果使用 env/secret 管理 CPA 连接,同时恢复安装目录里的
secrets/。 - 启动 CPAMP。
- 登录后检查配置、监控数据和采集器状态。
如果恢复后出现解密失败,优先检查 data.key 是否和 SQLite 匹配。
不保留请求历史,只迁移 Manager 配置
如果旧 usage.sqlite 很大且请求历史不需要保留,可以让新实例使用空数据目录,然后通过现有 Manager 配置 API 导出和导入非敏感的 CPA 连接地址、采集器、Codex 巡检与 External Usage Service 配置。该方式不会复制 usage_events、rollup、巡检运行历史、模型价格、API 密钥别名或账号处理策略,也不会导出 CPA Management Key。
在旧实例仍可访问时导出:
export OLD_CPAMP_URL='http://old-host:18317'
export OLD_CPAMP_ADMIN_KEY='cpamp_...'
curl -fsS \
-H "Authorization: Bearer ${OLD_CPAMP_ADMIN_KEY}" \
"${OLD_CPAMP_URL}/usage-service/config" \
| jq '{config: .config}' \
> manager-config.json
chmod 600 manager-config.json新版本的 manager-config.json 不包含 CPA Management Key;仍应按配置文件管理,避免把其他敏感配置提交到版本库或发送到 Issue。来自旧版本的导出文件可能包含明文密钥,必须立即按 secret 处理并在迁移后删除。
停止旧实例并准备新实例的空数据目录。先在 Manager Server 未运行时用离线命令重新提供 CPA Management Key:
cpa-manager-plus store-cpa-connection \
--cpa-base-url 'http://cpa:8317' \
--management-key-file '/secure/cpa-management-key' \
--db-path './data/usage.sqlite' \
--data-key-path './data/data.key'该命令要求先停止 Manager Server;它会把密钥加密写入 SQLite,命令输出不会回显密钥。
连接记录的 authority 规则是:完整的 manager_config_v1 权威;它与过期或冲突的旧 setup 同时存在时,启动和导入会保留 manager 连接并 canonicalize setup,不需要修复。manager 只有 partial 数据而完整的旧 setup 与其已有字段兼容时,setup 会补全 manager。只有在没有完整 authority 且 partial 记录彼此冲突,或解析器无法判断持久化状态时,上面的命令才会拒绝写入并在错误信息中给出修复方式。确认要以显式提供的连接为准时,追加 --repair-conflict 修复:
cpa-manager-plus store-cpa-connection \
--repair-conflict \
--cpa-base-url 'http://cpa:8317' \
--management-key-file '/secure/cpa-management-key' \
--db-path './data/usage.sqlite' \
--data-key-path './data/data.key'--repair-conflict 只用于修复解析器无法信任的历史状态:互相冲突的 manager_config_v1/setup 记录,或与请求冲突且没有权威方的 partial 记录。它把你显式提供的连接在单个事务里同时写入 manager_config_v1 与旧 setup 镜像(密钥加密存储),并保留采集器设置与其他数据;对完整且一致的已存连接仍然要求输入完全匹配,不会静默改绑。修复完成后再正常启动,连接存储迁移会照常完成。然后再启动新实例并导入其余配置:
export NEW_CPAMP_URL='http://new-host:18317'
export NEW_CPAMP_ADMIN_KEY='cpamp_...'
curl -fsS \
-X PUT \
-H "Authorization: Bearer ${NEW_CPAMP_ADMIN_KEY}" \
-H 'Content-Type: application/json' \
--data-binary @manager-config.json \
"${NEW_CPAMP_URL}/usage-service/config"导入时会校验 CPA Management API;成功后检查采集器状态和相关开关。确认恢复完成后安全删除导出文件和临时密钥文件。
如果旧实例仍由环境变量或 secret 文件管理,API 返回的 source 为 env,连接字段不能通过 API 导入覆盖;应先用上面的离线命令把 CPA 连接写入新 SQLite,或在新实例 setup 中重新填写。管理员登录凭证也不属于 Manager 配置导出:新实例使用新生成或显式设置的 CPA_MANAGER_ADMIN_KEY。