很多人说自己“有备份”,其实只是每天把文件打个包丢到云盘。
真出事时才发现:备份文件能下载,但数据库导不进去;Docker 容器能启动,但 volume 里少了上传文件;docker-compose.yml 找不到;restic 密码存在原 VPS 上,机器一没,密码也没了。
所以备份最关键的不是“有没有跑脚本”,而是:你能不能在一台干净 VPS 上,把业务恢复出来。
这篇不讲大公司那套复杂灾备体系,只讲普通 VPS 用户最该做的一件事:每隔一段时间做一次恢复演练。
恢复演练前,先别急着敲命令。你要先给自己定两个指标。
第一个是 RPO,也就是最多能丢多少数据。比如:
- 个人博客:丢 24 小时文章和评论可以接受;
- 小型电商:最多只能丢几分钟订单;
- Gitea / 私有 Git:最好一条 commit 都别丢;
- 监控系统:历史数据可以丢一部分,但配置不能丢。
第二个是 RTO,也就是多长时间内要恢复服务。
如果你的目标是“2 小时内恢复一个 WordPress 小站”,那演练就要真的计时:从新 VPS 开机,到网站能打开、数据库正常、后台能登录、图片能访问,总共花多久。
没有这两个指标,备份只是心理安慰。
普通 VPS 最容易漏的是配置和密钥,不是数据本身。
我建议先列一张清单:
| 类型 | 常见路径 / 内容 | 演练时怎么验证 |
|---|---|---|
| 网站文件 | /var/www、/opt/app、上传目录 | 首页、后台、图片是否正常 |
| 数据库 | MySQL、MariaDB、PostgreSQL dump | 能导入,表数量和关键数据正确 |
| Docker 数据 | named volume、bind mount | 容器重建后数据还在 |
| 配置文件 | docker-compose.yml、Nginx、Caddy、systemd unit | 服务能按原方式启动 |
| 证书和域名 | TLS 证书、DNS、Cloudflare 配置 | HTTPS 是否正常 |
| 密钥 | .env、API key、restic 密码、rclone config | 不依赖原 VPS 也能解密恢复 |
有些目录不用备:
/tmp/proc/sys/run/dev/var/cache- 项目的
node_modules - 可以重新构建的镜像和临时文件
真正要备的是“不能从源码或包管理器重新生成的东西”。
这是很多 VPS 备份翻车的第一名。
MySQL、PostgreSQL 这种数据库正在运行时,直接打包 /var/lib/mysql 或 PostgreSQL data directory,不一定能得到一致数据。你以为备份了,实际恢复时可能崩。
MySQL / MariaDB 用:
mysqldump --all-databases \
--single-transaction \
--quick \
--routines \
-u root -p \
| gzip > /var/backups/db/mysql-all-$(date +%F-%H%M).sql.gz
PostgreSQL 用:
sudo -u postgres pg_dumpall | gzip > /var/backups/db/postgres-all-$(date +%F-%H%M).sql.gz
如果是单库,也可以用:
pg_dump -U app_user -d app_db -Fc > /var/backups/db/app_db-$(date +%F-%H%M).dump
这里的重点不是命令多漂亮,而是演练时要真的导入一次。
MySQL 恢复测试:
gunzip < mysql-all-2026-06-11-0200.sql.gz | mysql -u root -p
PostgreSQL 恢复测试:
createdb app_db_test
pg_restore -U app_user -d app_db_test app_db-2026-06-11-0200.dump
只要导入失败,就说明你的备份方案不合格。
Docker 项目最容易让人误判:容器可以删,镜像可以重新拉,但 volume 里的数据不行。
先列出 volume:
docker volume ls
看某个 volume 的实际位置:
docker volume inspect myapp_data
如果这个 volume 只是上传文件、静态附件、配置状态,可以用临时容器打包:
docker run --rm \
-v myapp_data:/source:ro \
-v /var/backups/docker:/backup \
alpine:latest \
tar czf /backup/myapp_data-$(date +%F-%H%M).tar.gz -C /source .
恢复到新 volume:
docker volume create myapp_data
docker run --rm \
-v myapp_data:/target \
-v /var/backups/docker:/backup:ro \
alpine:latest \
tar xzf /backup/myapp_data-2026-06-11-0200.tar.gz -C /target
但如果 volume 里是数据库数据,不要直接 tar 运行中的 volume。优先用 mysqldump、pg_dump、应用自带导出工具。
如果必须文件级备份,至少要停容器或进入维护窗口。
rclone copy 很适合把备份文件传到对象存储,但它本身不是完整备份系统。更稳的做法是:
- restic 负责加密、去重、快照、保留策略;
- rclone 负责连接不同云存储;
- 数据库先 dump;
- Docker volume 按类型处理;
- 定期 restore 测试。
初始化 restic 仓库:
export RESTIC_REPOSITORY=s3:https://s3.example.com/vps-backups
export RESTIC_PASSWORD='请放到密码管理器,不要只放在 VPS 上'
restic init
执行备份:
restic backup \
/var/www \
/opt/app \
/etc/nginx \
/etc/caddy \
/etc/systemd/system \
/var/backups/db \
/var/backups/docker \
--exclude 'node_modules' \
--exclude '.git'
设置保留策略:
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 3 \
--prune
检查仓库:
restic check
restic snapshots
一个常见错误是只执行 forget,不执行 --prune。这样旧快照从列表里消失了,但底层数据可能还没真正释放,存储账单照样涨。
最小可行演练不需要影响生产环境。
你可以新开一台临时 VPS,或者用本地虚拟机,按下面顺序跑:
- 安装基础环境:Docker、Docker Compose、数据库、Nginx/Caddy;
- 配置 restic/rclone 访问备份仓库;
restic restore latest --target /tmp/restore-test;- 还原网站文件、配置文件、数据库 dump、Docker volume;
- 用临时域名或 hosts 文件访问;
- 验证登录、上传、查询、后台、定时任务;
- 记录从开始到恢复可用的总时间。
恢复最新快照:
restic restore latest --target /tmp/restore-test
只恢复某个路径:
restic restore latest --include /var/www --target /
恢复后不要只看文件是否存在,还要跑业务检查:
curl -I https://test.example.com
curl -s https://test.example.com/health
sudo systemctl status nginx --no-pager
docker compose ps
如果是 WordPress,至少检查:
- 首页能打开;
- 后台能登录;
- 媒体库图片能显示;
- 数据库里文章数量正确;
- 插件没有因为路径或权限报错。
如果是 Gitea,至少检查:
- 仓库列表;
- git clone;
- SSH 端口;
- runner / Actions 是否需要重新注册。
如果是自建应用,至少检查:
.env是否齐;- 数据库迁移状态;
- 队列是否启动;
- 上传目录是否可写;
- 定时任务是否恢复。
恢复演练做完后,要留下文档。否则下次真出事,还是靠记忆。
一个最小 runbook 至少包含:
# VPS 恢复演练记录
- 演练日期:2026-06-11
- 备份快照:restic snapshot abc123
- 备份时间点:2026-06-11 02:00
- 测试机器:Ubuntu 24.04 / 2C4G
- 恢复耗时:58 分钟
- 恢复内容:网站文件、Nginx 配置、MySQL dump、Docker volumes
- 验证结果:首页 OK、后台 OK、上传 OK、数据库记录数 OK
- 发现问题:.env 缺少第三方 API key,已补入密码管理器
- 下次改进:把 docker-compose.yml 放入 Git 仓库
你会发现,第一次演练通常都会暴露问题。比如:
- 备份脚本没有备
.env; - restic 密码只存在原机器上;
- rclone config 没有异地保存;
- 数据库 dump 文件为空;
- Docker volume 名称和 compose 文件对不上;
- Nginx 配置引用了不存在的证书路径;
- 对象存储 bucket 权限没恢复。
这些问题在演练中发现,是好事;在故障当天发现,才是灾难。
cron 没报错,不代表备份成功。它可能根本没执行,也可能执行到一半失败。
备份脚本里至少要做三件事:
set -euo pipefail
这样脚本遇到错误会直接退出,不会假装成功。
然后检查最后一次备份时间:
restic snapshots | tail
如果你有监控,可以给“备份超过 24 小时没更新”加告警。没有监控,至少用邮件、Telegram、Uptime Kuma push monitor 之类的方式提醒。
你已经有监控系统的话,可以结合这篇:
每季度做一次就够很多小站用了:
- 从 restic/rclone 仓库列出最新快照;
- 在临时目录 restore 最新快照;
- 抽查关键配置文件和上传文件;
- 把 MySQL/PostgreSQL dump 导入测试库;
- 还原至少一个 Docker volume;
- 用 compose 在测试环境启动业务;
- 访问首页、后台、健康检查接口;
- 记录恢复耗时;
- 检查缺失的密钥、配置、证书;
- 更新 runbook。
如果你的业务比较重要,可以每月做轻量演练,每季度做完整演练。
不够。快照适合变更前快速回滚,不适合替代异地备份。商家账号被封、机器误删、区域故障、快照损坏时,快照和机器可能一起没。
如果你还分不清快照、备份、镜像,可以先看:
不行。备份成功只说明“写入了某些数据”,不说明这些数据能恢复成可用业务。真正的验证是 restore + 启动 + 访问 + 数据校验。
不建议。运行中的数据库文件可能处于不一致状态。MySQL/MariaDB 用 mysqldump --single-transaction,PostgreSQL 用 pg_dump 或 pg_dumpall 更稳。
不要只放在 VPS 上。至少放到密码管理器里,再准备一个离线备份。restic 仓库是加密的,密码丢了,备份基本等于没了。
不一定。轻量演练可以用测试域名或 hosts 文件。完整灾难恢复演练可以模拟切 DNS,但要注意不要影响生产流量。
VPS 备份不是“每天跑一次脚本”就结束了。
真正可靠的链路应该是:数据库先导出,文件和配置进入加密备份,备份仓库放到异地,保留策略可控,最后定期在干净环境里恢复一次。
如果你现在只能做一件事,不是换工具,也不是买更贵的存储,而是找一台临时 VPS,跑一次完整恢复演练。跑通以后,你才知道自己的备份到底是不是备份。
