Docker 基础体系 · 第 18/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker 数据备份与主机迁移:卷、数据库、一致性和恢复演练

Docker 容器本身通常是可替换的,真正需要保护的是容器外部的状态:数据库文件、上传文件、队列数据、证书、配置和恢复所需的镜像与版本信息。主机迁移也不是“把容器复制到另一台机器”,而是把运行所需的不可替代状态以可验证的方式导出,在目标主机重新创建运行环境,再通过恢复演练证明结果可用。

这类工作需要同时回答四个问题:

  1. 数据实际存放在哪里?
  2. 备份时是否处于一致状态?
  3. 新主机是否能重建相同的运行环境?
  4. 恢复后如何证明业务数据正确,而不是仅仅证明容器启动成功?

一、先区分容器、镜像、卷和主机文件

1. 容器文件系统不是可靠的数据存储

镜像由只读层组成,容器启动时再叠加一个可写层。容器内未挂载到外部存储的文件写入这个可写层:

镜像只读层
    +
容器可写层
    +
Volume / Bind Mount / tmpfs

删除容器时,容器可写层通常随容器一起删除。重新创建同一个镜像的容器,也不会恢复原容器可写层中的文件。

因此,下面这些位置的持久性不同:

存储位置 容器删除后 主机重启后 是否适合备份业务数据
镜像只读层 保留在镜像中 保留 只适合静态程序和依赖
容器可写层 丢失 通常保留 不适合
named volume 保留 保留 适合,但需单独备份
bind mount 保留在主机目录 保留 适合
tmpfs 丢失 丢失 不适合持久数据

这里的“保留”并不等于“已经备份”。named volume 和 bind mount 只是在本机仍然存在;磁盘损坏、误删、勒索软件或主机丢失仍会导致数据不可恢复。


二、三种常见挂载方式的备份边界

1. Volume

Docker 管理的 named volume 由 Docker volume driver 管理。Linux 默认 local driver 的数据通常位于:

/var/lib/docker/volumes/<volume-name>/_data

但这个路径是常见实现,不应当被当作跨 driver、跨发行版的稳定接口。使用 NFS、云盘或第三方 volume plugin 时,数据甚至可能根本不在本机这个目录下。

创建并查看 volume:

docker volume create wrblog_data
docker volume inspect wrblog_data

典型输出结构类似:

[
  {
    "Name": "wrblog_data",
    "Driver": "local",
    "Mountpoint": "/var/lib/docker/volumes/wrblog_data/_data",
    "Scope": "local"
  }
]

备份时应通过 Docker 挂载 volume,而不是直接假设其宿主机路径:

mkdir -p ./backup

docker run --rm \
  --mount type=volume,src=wrblog_data,dst=/data,readonly \
  --mount type=bind,src="$PWD/backup",dst=/backup \
  alpine:3.20 \
  sh -c 'tar czpf /backup/wrblog_data.tgz -C /data .'

命令的因果关系是:

  1. src=wrblog_data 让 Docker 找到 volume。
  2. /data 是容器内访问卷内容的路径。
  3. readonly 防止备份工具意外修改源数据。
  4. /backup 映射到主机目录,使归档文件离开 Docker volume。
  5. tar -C /data . 归档的是卷内文件内容,而不是 /data 目录本身。

恢复到一个新 volume:

docker volume create wrblog_data_restore

docker run --rm \
  --mount type=volume,src=wrblog_data_restore,dst=/data \
  --mount type=bind,src="$PWD/backup",dst=/backup \
  alpine:3.20 \
  sh -c 'tar xpf /backup/wrblog_data.tgz -C /data'

恢复后检查:

docker run --rm \
  --mount type=volume,src=wrblog_data_restore,dst=/data,readonly \
  alpine:3.20 \
  sh -c 'find /data -maxdepth 2 -type f -printf "%M %u:%g %s %p\n" | head'

这个方法适用于普通文件、上传目录和应用生成文件,但不自动保证数据库文件的一致性。数据库需要单独讨论。

2. Bind mount

bind mount 直接把主机路径映射到容器:

services:
  app:
    image: example/app:1.0
    volumes:
      - type: bind
        source: /srv/wrblog/uploads
        target: /var/lib/app/uploads

它的优点是路径透明,主机上的备份工具、文件系统快照和权限管理都可以直接使用。缺点是环境耦合更强:

  • 目标主机必须创建相同或等价的目录;
  • UID、GID、ACL、SELinux 标签可能不同;
  • 相对路径受 Compose 项目工作目录影响;
  • 备份程序若运行在别的主机上,必须明确复制这个主机目录。

普通文件复制至少应考虑权限、符号链接、时间戳和 ACL。例如 Linux 上可以使用:

rsync -aHAX --numeric-ids \
  /srv/wrblog/uploads/ \
  ./backup/uploads/

其中:

  • -a 保留常见文件属性;
  • -H 保留硬链接;
  • -A 保留 POSIX ACL;
  • -X 保留扩展属性;
  • --numeric-ids 不把 UID/GID 映射成备份机上的用户名和组名。

目标主机恢复时:

rsync -aHAX --numeric-ids \
  ./backup/uploads/ \
  /srv/wrblog/uploads/

如果使用 SELinux,还需要根据目标主机策略重新确认文件标签;仅保留 Unix 权限并不保证容器能访问文件。

3. tmpfs

tmpfs 把文件放在内存或内核管理的临时存储中:

services:
  app:
    image: example/app:1.0
    tmpfs:
      - /tmp

容器停止、删除或主机重启后,tmpfs 中的数据都会消失。它适合:

  • 临时编译目录;
  • 缓存;
  • 短期会话文件;
  • 不希望写入磁盘的中间数据。

它不适合数据库、上传文件、队列持久化和恢复所需的状态。对 tmpfs 做 tar 备份只能备份“当前仍存在的临时内容”,不能改变其非持久属性。


三、卷名、Compose 项目和迁移时的命名问题

Compose 中声明:

volumes:
  db_data:

services:
  db:
    image: postgres:16
    volumes:
      - db_data:/var/lib/postgresql/data

实际卷名通常会受到 Compose project name 影响,例如项目目录名为 wrblog 时可能生成:

wrblog_db_data

因此在迁移前不要凭记忆写卷名,应查看实际绑定关系:

docker compose ps
docker compose config
docker inspect "$(docker compose ps -q db)"
docker volume ls

如果希望卷名不受项目目录影响,可以显式指定:

volumes:
  db_data:
    name: wrblog_postgres_data

这使实际卷名固定为 wrblog_postgres_data。但固定名称不是跨主机同步机制;目标主机仍然需要手工创建或恢复这个卷。

还要区分两种操作:

docker compose down

默认会删除容器和网络,但通常保留命名卷。

docker compose down -v

会删除 Compose 声明的命名卷,数据可能直接丢失。这个命令只能在已经确认有可用备份、且明确希望销毁卷时使用。


四、备份的核心:崩溃一致性和应用一致性

1. 什么是一致性

备份一致性可以抽象为:恢复后的数据状态应该满足应用或数据库定义的不变量。

设数据库状态为 SS,事务提交会把状态从 SiS_i 变为 Si+1S_{i+1}。一个一致备份应当对应某个合法状态:

B=SkB = S_k

或者包含能够把数据恢复到合法状态的日志:

Bdata+BlogSkB_{\text{data}} + B_{\text{log}} \Rightarrow S_k

如果备份同时复制了一个事务提交前的表文件和提交后的索引文件,就可能得到:

B{S0,S1,S2,}B \notin \{S_0, S_1, S_2, \ldots\}

这就是逻辑上不存在的混合状态。文件数量完整、tar 命令成功,都不能证明备份一致。

2. 崩溃一致性

崩溃一致性指的是:系统仿佛在某个瞬间突然断电,恢复后数据库依靠自身日志完成回滚或重做。

它通常依赖:

  • 数据库的 WAL、redo log 或 journal;
  • 文件系统或存储快照的原子语义;
  • 数据库对 fsync 和持久化顺序的正确使用。

Docker 本身不提供数据库一致性。docker pause 也不是数据库快照协议;它只是冻结容器进程,不能替代数据库的 flush、checkpoint 或快照协调。

3. 应用一致性

应用一致性要求备份反映应用认可的业务状态。例如订单系统中:

订单已标记为 paid
但支付流水表没有对应记录

即使两个表在文件层面都没有损坏,业务上仍然是不一致的。

获得应用一致性的常见方式是:

  1. 停止所有写入;
  2. 等待正在执行的请求完成;
  3. 执行数据库原生备份;
  4. 备份上传文件等外部状态;
  5. 记录版本、时间点和校验信息;
  6. 恢复写入。

对于可以短暂停机的系统,这是最容易验证的方案。无法停机时,则需要数据库原生的一致性快照、事务快照或存储层快照协议。


五、数据库不能简单当作普通目录复制

1. 为什么在线复制数据库目录危险

以 PostgreSQL 为例,数据目录包括表文件、索引、控制文件、WAL 和其他元数据。数据库运行时,一个事务可能经历:

  1. 修改数据页;
  2. 写入 WAL;
  3. 更新索引;
  4. 提交事务;
  5. 延迟刷盘。

如果此时直接执行:

tar czf postgres-data.tgz /var/lib/postgresql/data

可能复制到不同时间点的数据页和索引页。即使 PostgreSQL 启动时能完成恢复,也不能把未经支持的文件复制当作可靠的物理备份方法。

正确选择通常是:

  • 使用 pg_dumppg_dumpall 等逻辑备份;
  • 使用 PostgreSQL 支持的物理备份和 WAL 归档方案;
  • 使用已与数据库协同的存储快照;
  • 或先停止数据库,再复制完整数据目录。

2. PostgreSQL 逻辑备份示例

假设 Compose 文件如下:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: wrblog
      POSTGRES_DB: wrblog
      POSTGRES_PASSWORD: change-this-in-secret-management
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:
    name: wrblog_postgres_data

先确认数据库可连接:

docker compose exec -T db \
  pg_isready -U wrblog -d wrblog

预期结果类似:

/var/run/postgresql:5432 - accepting connections

导出数据库:

mkdir -p backup/2025-01-01

docker compose exec -T db \
  pg_dump -U wrblog -d wrblog --format=custom \
  > backup/2025-01-01/wrblog.dump

--format=custom 生成 PostgreSQL 自定义格式,适合使用 pg_restore 选择性恢复和并行恢复。这个文件包含数据库对象和数据,但不自动包含整个 PostgreSQL 集群的全局对象,例如角色和表空间定义。

导出全局对象:

docker compose exec -T db \
  pg_dumpall -U wrblog --globals-only \
  > backup/2025-01-01/globals.sql

检查归档是否可读取:

pg_restore --list backup/2025-01-01/wrblog.dump | head

如果主机没有 PostgreSQL 客户端,可以使用一次性容器:

docker run --rm \
  --network container:"$(docker compose ps -q db)" \
  -v "$PWD/backup/2025-01-01:/backup" \
  -e PGPASSWORD='change-this-in-secret-management' \
  postgres:16 \
  pg_dump -h 127.0.0.1 -U wrblog -d wrblog \
  --format=custom -f /backup/wrblog.dump

生产环境不应把真实密码直接写入 shell 历史;示例中的密码只表示连接所需的前置条件,应改用受控的 secret 文件、凭据注入或短生命周期环境变量。

3. 逻辑备份的一致性边界

pg_dump 在数据库内部建立一致性快照,因此导出期间其他事务通常可以继续运行。它得到的是某个数据库级一致视图,而不是简单逐表复制。

但是它仍有边界:

  • pg_dump 不备份角色等集群全局对象;
  • 大对象、扩展、外部表和外部依赖需要单独确认;
  • 备份完成后新增的数据不在该备份中;
  • 数据库外的上传目录、对象存储和消息队列不在其中;
  • 跨数据库的业务事务不能仅靠单库 pg_dump 获得全局一致性。

如果业务同时写 PostgreSQL 和上传目录,必须设计业务级关联。例如先写入临时文件,数据库事务提交后再把文件标记为可见;恢复时对“无数据库记录的孤儿文件”和“数据库有记录但文件缺失”分别检查。

4. MySQL 类数据库的差异

MySQL 的 mysqldump --single-transaction 适合事务型 InnoDB 表,因为它建立一致性读视图;但它不能自动解决所有表引擎和 DDL 并发问题:

  • MyISAM 等非事务表不能通过事务快照获得同样保证;
  • 导出期间执行 ALTER TABLE 可能影响结果或阻塞;
  • 触发器、事件、存储过程和用户权限需要确认导出选项;
  • 对大规模数据库,物理备份工具可能比逻辑导出更适合。

因此,“使用数据库官方备份工具”不是一个固定命令,而是要根据数据库引擎、版本、复制拓扑和恢复目标选择对应机制。


六、可接受的文件级卷备份流程

对于非数据库文件,或者已经停止数据库后的完整卷复制,可以使用以下顺序。

1. 先停止写入者

docker compose stop app

如果要复制数据库数据目录,再停止数据库:

docker compose stop db

查看状态:

docker compose ps

停止命令成功只表示 Docker 已向容器发送停止请求并完成等待,不等于应用内部一定完成了业务 flush。应检查日志:

docker compose logs --tail=100 app db

若应用捕获 SIGTERM 并优雅退出,日志中通常会出现关闭连接、完成 flush 或数据库正常停止等信息。若容器被强制终止,文件级备份的可靠性需要重新评估。

2. 导出卷并计算校验和

docker run --rm \
  --mount type=volume,src=wrblog_postgres_data,dst=/data,readonly \
  --mount type=bind,src="$PWD/backup/2025-01-01",dst=/backup \
  alpine:3.20 \
  sh -c 'tar czpf /backup/postgres-volume.tgz -C /data .'

sha256sum backup/2025-01-01/postgres-volume.tgz \
  > backup/2025-01-01/SHA256SUMS

校验和用于检测传输损坏和文件替换,不用于证明内容具有数据库一致性。一个被错误复制但没有损坏的 tar 文件,仍然会产生正确的 SHA-256。

3. 重新启动服务

docker compose start db
docker compose start app

如果数据库使用的是逻辑备份,通常不需要再同时依赖一个在线复制的数据目录。若采用停机后的物理目录恢复,则必须保证目标数据库镜像版本、架构、配置、权限和数据目录格式兼容。


七、主机迁移需要迁移的不只是卷

迁移对象至少包括以下几类状态:

应用镜像
Compose 文件与环境变量
密钥和证书
named volume / bind mount 数据
数据库逻辑或物理备份
反向代理和网络配置
定时任务、监控和日志策略
恢复所需的 Docker 与数据库版本

镜像不包含卷,卷也不包含镜像。执行:

docker image save myapp:1.4 -o myapp-1.4.tar

只会导出镜像层和镜像元数据,不会导出容器挂载的 volume 或 bind mount 文件。

更常见的方式是把镜像推送到镜像仓库:

docker buildx build \
  --platform linux/amd64 \
  -t registry.example.com/wrblog/app:1.4 \
  --push .

BuildKit 可以提供可重复构建、缓存和多平台镜像构建能力,但构建缓存不是业务数据备份。缓存丢失只会让构建变慢,不能替代数据库和上传文件备份。

如果没有镜像仓库,也可以离线传输:

docker save registry.example.com/wrblog/app:1.4 \
  | gzip > backup/2025-01-01/wrblog-app-1.4.tar.gz

目标主机导入:

gunzip -c backup/2025-01-01/wrblog-app-1.4.tar.gz \
  | docker load

目标主机应先确认:

docker version
docker compose version
docker info

重点检查:

  • CPU 架构是否匹配;
  • Docker Engine 是否支持所使用的 Compose 字段和存储 driver;
  • volume driver 是否已安装;
  • bind mount 目录是否存在;
  • 防火墙、端口和 DNS 是否一致;
  • 数据库镜像主版本是否与备份方案兼容。

八、一个完整的迁移时序

典型的单主机迁移可以表示为:

sequenceDiagram
    participant Old as 旧主机
    participant Store as 备份存储
    participant New as 新主机
    participant Check as 验证环境

    Old->>Old: 固定代码、镜像和 Compose 配置版本
    Old->>Old: 停止写入或建立数据库一致性窗口
    Old->>Store: 导出数据库逻辑备份
    Old->>Store: 导出上传目录和其他卷
    Old->>Store: 上传校验和、配置清单和版本信息
    New->>Store: 下载备份并校验 SHA-256
    New->>New: 导入镜像、创建卷和目录
    New->>New: 恢复文件、初始化数据库
    New->>Check: 启动隔离验证环境
    Check->>Check: 执行健康检查和业务数据校验
    Check-->>New: 验证通过
    Old->>Old: 停止旧主机对外写入
    New->>New: 切换流量
    New->>Check: 记录恢复耗时和验证结果

关键是先在新主机恢复并验证,再切换流量。直接“备份后立刻改 DNS”会把恢复错误暴露给真实用户,且回滚时可能出现新旧主机双写。

一个保守的切换顺序是:

  1. 降低 DNS TTL 或准备负载均衡切换;
  2. 关闭旧主机写入;
  3. 完成最后一次数据库备份和文件同步;
  4. 在新主机恢复;
  5. 执行健康检查和业务校验;
  6. 切换流量;
  7. 保留旧主机为只读或停机状态,直到观察窗口结束。

九、恢复数据库的端到端示例

以下示例采用新卷恢复 PostgreSQL,而不是覆盖原卷。这样可以降低误操作风险。

1. 创建并恢复到新卷

docker volume create wrblog_postgres_restore

docker run --rm \
  --mount type=volume,src=wrblog_postgres_restore,dst=/data \
  --mount type=bind,src="$PWD/backup/2025-01-01",dst=/backup \
  postgres:16 \
  bash -c 'tar xpf /backup/postgres-volume.tgz -C /data'

如果归档来自停机后的 PostgreSQL 数据目录,恢复文件后要确认目录属主。官方 PostgreSQL 镜像通常要求数据目录由容器内的 postgres 用户拥有,但具体 UID 应以镜像为准。可以通过临时容器查看:

docker run --rm \
  --mount type=volume,src=wrblog_postgres_restore,dst=/var/lib/postgresql/data \
  postgres:16 \
  bash -c 'id postgres; stat -c "%U:%G %a %n" /var/lib/postgresql/data'

不要仅凭宿主机上的用户名判断权限,因为容器内的 UID/GID 才是内核实际检查的数值。

2. 使用逻辑备份恢复

更可控的方式是创建一个全新的 PostgreSQL 数据目录,让数据库先正常初始化,再导入逻辑备份。

启动临时数据库:

docker run -d --name wrblog-pg-restore \
  -e POSTGRES_USER=wrblog \
  -e POSTGRES_PASSWORD='restore-only-password' \
  -e POSTGRES_DB=wrblog \
  -p 55432:5432 \
  postgres:16

等待数据库就绪:

until docker exec wrblog-pg-restore \
  pg_isready -U wrblog -d wrblog
do
  sleep 1
done

恢复全局对象和数据库:

cat backup/2025-01-01/globals.sql \
  | docker exec -i \
      -e PGPASSWORD='restore-only-password' \
      wrblog-pg-restore \
      psql -U wrblog -d postgres

然后恢复数据库对象:

docker exec -i \
  -e PGPASSWORD='restore-only-password' \
  wrblog-pg-restore \
  pg_restore -U wrblog -d wrblog \
  --no-owner --exit-on-error \
  < backup/2025-01-01/wrblog.dump

--exit-on-error 使出现错误时立即失败,而不是导入一部分后仍返回成功。--no-owner 适用于恢复环境中的角色与源环境不同的场景;如果权限模型要求保留原属主,则应先创建对应角色,并谨慎决定是否去掉该选项。

恢复后检查:

docker exec \
  -e PGPASSWORD='restore-only-password' \
  wrblog-pg-restore \
  psql -U wrblog -d wrblog \
  -c 'SELECT count(*) FROM important_table;'

验证不能只执行 SELECT 1。至少应检查:

  • 关键表行数或业务计数;
  • 最近一条订单、文章或任务;
  • 外键和唯一约束;
  • 应用登录;
  • 写入一条测试数据并读取;
  • 上传文件与数据库记录的对应关系;
  • 定时任务和后台消费者是否能正常运行。

十、恢复演练不是“把备份解压一次”

恢复演练的目标是测量并验证两个指标:

  • RPO(Recovery Point Objective):最多能接受丢失多长时间的数据;
  • RTO(Recovery Time Objective):故障发生后,最多允许多长时间恢复服务。

例如:

业务要求:RPO ≤ 15 分钟,RTO ≤ 60 分钟
实际演练:最后备份距故障 8 分钟,服务恢复耗时 42 分钟
结果:满足目标

如果只有备份任务成功日志,没有真正恢复并启动业务,RPO 和 RTO 都只是猜测。

一次可重复的恢复演练

在与生产隔离的 Linux 主机或虚拟机上执行:

mkdir restore-drill
cd restore-drill

sha256sum -c ../backup/2025-01-01/SHA256SUMS

预期应看到:

postgres-volume.tgz: OK
wrblog.dump: OK

然后按生产 Compose 文件部署,但使用独立的:

  • 项目名;
  • 端口;
  • 数据库卷名;
  • 密钥;
  • 外部依赖地址。

例如:

docker compose -p wrblog-drill up -d

恢复完成后记录:

date -Is
docker compose -p wrblog-drill ps
docker compose -p wrblog-drill logs --tail=200

再执行业务验证。演练应记录以下中间结果:

  1. 备份文件大小和校验和;
  2. 下载、解压和导入耗时;
  3. 数据库恢复开始与结束时间;
  4. 每个服务的启动时间;
  5. 关键数据校验结果;
  6. 失败时的日志和恢复动作;
  7. 最终 RPO 与 RTO。

如果验证中途发现备份缺少角色、证书、上传目录或配置,不应把问题归因于“演练环境不同”。这通常说明备份清单不完整。


十一、权限、属主和安全属性

容器进程使用的是 Linux 内核看到的 UID/GID,而不是容器中显示的用户名。假设宿主机目录属于 UID 1001:

sudo chown -R 1001:1001 /srv/wrblog/uploads

容器内应用也必须以 UID 1001 运行,或者通过组权限获得访问权。恢复后出现:

Permission denied

常见诊断顺序是:

docker inspect <container>
docker exec <container> id
docker exec <container> ls -ln /path
namei -l /srv/wrblog/uploads

其中 ls -ln 显示数字 UID/GID,避免用户名映射造成误判;namei -l 可以检查路径中每一级目录是否允许遍历。

备份本身也包含敏感信息:

  • 数据库中的用户资料;
  • API token;
  • session;
  • 私钥和证书;
  • 业务源代码;
  • 可能包含密码的配置文件。

因此备份应至少做到:

chmod 600 backup/2025-01-01/*

实际生产系统还应使用加密归档、受限对象存储、密钥轮换、保留周期和删除保护。加密密钥不能与备份放在同一个故障域,否则主机丢失时可能同时丢失数据和解密能力。


十二、常见失败方式及其原因

1. 只备份镜像

镜像只能恢复程序和基础文件,不能恢复:

  • named volume;
  • bind mount;
  • 数据库提交状态;
  • 运行时生成的上传文件;
  • 外部密钥和证书。

表现通常是容器能启动,但数据库为空或用户上传文件全部消失。

2. 直接复制 /var/lib/docker

直接复制整个 Docker 数据根目录并不是通用迁移方法。它依赖:

  • Engine 版本和存储驱动;
  • overlay2 元数据;
  • volume driver;
  • 容器 ID 和网络状态;
  • 主机路径、权限和内核行为。

正确方法是重新部署镜像和 Compose 配置,再通过受控方法恢复卷数据。

3. 在线 tar 数据库目录

tar 本身成功只表示文件读取成功,不代表数据库在逻辑上处于一致状态。失败表现可能包括:

  • 数据库启动恢复失败;
  • 索引损坏;
  • 启动后查询报错;
  • 更隐蔽的数据缺失或业务关系不一致。

数据库应使用原生备份、支持的物理备份,或先停止数据库再复制。

4. 恢复后直接删掉旧卷

如果把恢复数据写入旧卷,恢复失败后很难区分:

  • 原始数据;
  • 已恢复数据;
  • 部分导入数据;
  • 人工修复产生的数据。

使用新卷或新 Compose project 先验证,确认通过后再切换,是更容易回滚的状态转换。

5. 只检查容器健康状态

健康检查可能只执行:

curl -f http://localhost:8080/health

这只能说明进程和基础接口可用,不能说明:

  • 数据库记录完整;
  • 外部文件存在;
  • 消息队列没有重复消费;
  • 权限模型正确;
  • 新写入能持久化。

恢复验证必须包含业务数据和持久化路径。


十三、备份方案的选择

可以用下面的判断逻辑区分方案:

数据是否持久化?
├─ 否:tmpfs 或容器可写层,不纳入持久备份
└─ 是
   ├─ 数据库?
   │  ├─ 可停机:停止写入后做原生备份或文件级备份
   │  └─ 不可停机:逻辑备份、物理备份或协调快照
   └─ 普通文件?
      ├─ Volume:通过临时容器挂载后归档
      └─ Bind mount:使用保留属性的主机文件工具

小型服务常见的可接受方案是:

  • 镜像进入版本化仓库;
  • Compose 文件进入 Git;
  • PostgreSQL 使用定期 pg_dump
  • 上传目录使用 tar、rsync 或对象存储版本;
  • 每份备份带 SHA-256;
  • 每月在隔离主机恢复一次。

大型数据库或严格低 RPO 系统则可能需要:

  • 连续 WAL/binlog 归档;
  • 增量物理备份;
  • 复制节点;
  • 存储快照;
  • 定期时间点恢复验证。

复制不是备份:误删数据可能迅速复制到所有副本。快照也不是自动一致:没有数据库协调时,快照可能只是崩溃一致性。高可用解决的是服务连续性,备份恢复解决的是数据回到过去某个可用状态,两者不能互相替代。


十四、最终应交付一份可执行的恢复手册

一份真正可用的运行手册不应只写“执行备份脚本”,而应包含:

Docker Engine 和 Compose 版本
镜像名称、标签和架构
Compose 文件及配置来源
每个 Volume、Bind Mount、tmpfs 的用途
数据库备份命令和恢复命令
备份存储位置、保留策略和加密方式
UID/GID、ACL、SELinux 等权限要求
外部依赖:对象存储、DNS、证书、邮件、队列
校验和验证方法
切换流量和回滚步骤
最近一次恢复演练结果

备份策略的完成标准不是“定时任务没有报错”,而是:

可恢复性=备份存在内容一致环境可重建恢复步骤可执行业务验证通过\text{可恢复性} = \text{备份存在} \land \text{内容一致} \land \text{环境可重建} \land \text{恢复步骤可执行} \land \text{业务验证通过}

缺少其中任一项,主机迁移时都可能出现“服务启动了,但系统已经不是原来的系统”。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。