家里的备份之前一直靠 rsync 增量备份,每天把重要目录同步到 NAS 和一块 USB 硬盘上。这解决了“误删”和“单块盘坏掉”的问题,但所有副本都在同一个屋檐下,万一遇上火灾、进水或者失窃,就全军覆没了。按照 3-2-1 原则(三份副本、两种介质、一份异地),还缺异地的那一份。

上个月整理完 compose 目录之后,要备份的东西也清楚了:Docker 虚拟机的 /srv/compose,配置和数据都在里面。NAS 上的文档目录也用同样的方法另跑一份,下面以 Docker 虚拟机为例。总量不到 100G,放在对象存储上一个月也就几块钱。

为什么选 restic

  • 客户端加密:数据在本地加密后才上传,对象存储那边只能看到一堆密文块。
  • 去重和压缩:每次备份都是一个完整的快照,但只上传变化的部分。
  • 原生支持 S3 协议:大多数对象存储都兼容 S3 接口,换服务商只需要改仓库地址。
  • 单个可执行文件,没有依赖。恢复时在任何机器上下载一个文件就能用。

Debian 12 仓库里的 restic 版本比较老,我直接用了官方发布的二进制(写这篇时是 0.18),下载 restic_0.18.0_linux_amd64.bz2,解压后放到 /usr/local/bin/restic,以后用 restic self-update 升级。

初始化仓库

先在对象存储的控制台里建一个桶(bucket),再建一对只对这个桶有读写权限的访问密钥。然后把连接信息写进一个只有 root 能读的文件:

# /root/.config/restic/env (chmod 600)
export RESTIC_REPOSITORY="s3:https://s3.example.com/homelab-backup"
export RESTIC_PASSWORD_FILE="/root/.config/restic/password"
export AWS_ACCESS_KEY_ID="替换成你的 Access Key"
export AWS_SECRET_ACCESS_KEY="替换成你的 Secret Key"
# 部分服务商要求指定区域,按它的文档填
# export AWS_DEFAULT_REGION="..."

环境变量名虽然带着 AWS,但对所有 S3 兼容的服务都是这么用的。password 文件里放仓库的加密密码,用 openssl rand -base64 32 生成一个就行。

source /root/.config/restic/env
restic init                       # 初始化仓库,只需执行一次

restic backup /srv/compose --exclude-file=/root/.config/restic/excludes
restic snapshots                  # 查看已有的快照

excludes 文件里每行一个排除规则。我排除了各种缓存目录,以及数据库的数据目录——数据库单独处理,见下文。

备份脚本

直接备份正在运行的数据库目录,恢复出来很可能是不一致的。我的做法是先用 pg_dump 导出成 SQL 文件,再连同其他文件一起备份:

#!/usr/bin/env bash
# /usr/local/bin/restic-backup.sh
set -euo pipefail
source /root/.config/restic/env

# 1. 导出数据库
mkdir -p /srv/dumps
cd /srv/compose/miniflux
docker compose exec -T db pg_dump -U miniflux miniflux > /srv/dumps/miniflux.sql

# 2. 备份
restic backup /srv/compose /srv/dumps \
  --exclude-file=/root/.config/restic/excludes \
  --tag daily

# 3. 按策略删除旧快照:保留 7 天、4 周、6 个月
restic forget --tag daily --keep-daily 7 --keep-weekly 4 --keep-monthly 6

# 4. 每周日真正清理数据,并抽查 5% 的数据能否正常读出
if [ "$(date +%u)" = "7" ]; then
  restic prune
  restic check --read-data-subset=5%
fi

forget 只是删掉快照记录,很快;prune 才会真正清理不再被引用的数据块,要下载和重写一部分数据,比较慢,所以我放到每周做一次。check --read-data-subset 会实际下载一部分数据来校验,比只检查仓库结构更让人放心。

定时执行用的是 cron,在 /etc/cron.d/restic 里加一行,每天凌晨 3 点半跑,输出追加到一个日志文件里。

一定要做恢复演练

没恢复过的备份不算备份。配好之后我挑了个周末,在一台临时虚拟机上只装了 restic,试着把 Miniflux 整个恢复出来:

restic snapshots --tag daily
restic ls latest /srv/compose/miniflux          # 先看看快照里有什么
restic restore latest --target /tmp/restore-test --include /srv/compose/miniflux
restic restore latest --target /tmp/restore-test --include /srv/dumps

然后用恢复出来的 compose.yaml 起一个空数据库,把 SQL 导进去,打开网页能看到原来的订阅,才算真正放心。

踩过的坑

  1. 仓库密码丢了就全完了。 restic 的加密是真加密,忘了密码谁也救不回来。我把密码存进了密码管理器,又打印了一份放在抽屉里。注意密码管理器自己的数据也在备份里,别形成“要恢复密码库得先有密码”的死循环。
  2. 第一次上传占满了上行带宽。 家里上行只有几十兆,第一次全量备份跑了好几个小时,期间视频通话一卡一卡的。加上 --limit-upload 2048(单位是 KiB/s)限速之后就好了。
  3. 残留的锁。 有一次备份到一半虚拟机重启了,之后的备份都报仓库被锁。确认没有其他 restic 进程在跑之后,执行 restic unlock 清掉过期的锁即可。
  4. 改了备份路径,旧快照不会被清理。 forget 默认按“主机名 + 备份路径”分组应用保留策略。我中途给 backup 命令加了 /srv/dumps,新旧快照就分成了两组,旧的那组永远保留着最后几个快照。可以用 --group-by host,tags 改变分组方式,或者确认无误后手动 forget 掉旧组。
  5. 留意服务商的计费规则。 有些对象存储对删除的数据有最短存储期限,或者按请求次数收费,频繁 prune 可能反而多花钱。这也是我把 prune 放到每周一次的原因之一。

小结

restic 用起来很省心:配置就是几个环境变量,备份、清理、校验、恢复都是一条命令。现在每天凌晨自动跑,异地的那一份总算补上了。还差的一块是失败告警:目前只能靠自己偶尔去翻日志,这点还不太放心,以后再想办法。