跳过正文
  1. 博客文章/

Restic 备份实践

·1188 字·6 分钟·
DevOps SRE Restic Backup Rest-Server Docker-Compose DevOps
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录
生产变更复盘 - 这篇文章属于一个选集。
7: 本文
Restic 的日常使用不复杂,真正容易出错的是仓库地址、密码保存、rest-server --private-repos 路径规则,以及 forget --prune 在 append-only 场景里的行为。这篇把客户端备份、服务端参数、Docker 定时任务和恢复演练整理成一条可复用流程。
文中使用 backup.example.internal192.0.2.31backup-userserver-a 作为示例值。落地时替换为自己的域名、IP、仓库名和凭据;REST 认证密码、仓库加密密码、SMTP/Token 这类值只放在环境变量、.env 或密钥管理系统中。

0. 结论和边界
#

这套方案适合在局域网或私有云里集中存放多台机器的 Restic 备份。服务端用 rest-server 提供 HTTP REST 后端,客户端用 Restic CLI 或 mazzolino/restic 容器执行备份、保留策略、校验和恢复。

核心边界先收住:

建议
仓库地址使用 rest:http://backup-user:<REST_SERVER_PASSWORD>@backup.example.internal:8000/backup-user/<repo>
仓库加密密码使用 RESTIC_PASSWORDRESTIC_PASSWORD_FILE--password-command,不要写进命令历史
服务端路径隔离启用 --private-repos 后,仓库路径必须以用户名开头
删除能力启用 --append-only 后,客户端不能真正删除或 prune 远端数据
恢复策略先恢复到临时目录验证,再考虑覆盖原路径
Docker 密钥Compose 变量插值不等于密钥隔离,生产环境优先用密码文件或 Docker secrets

整体链路:

flowchart TB
    client["Client / Cron Container"]
    server["rest-server"]
    repo["/data/restic/backup-user/"]

    client -->|"REST over HTTP(S)"| server
    server -->|"repository namespace"| repo

最小验证目标:

restic snapshots
restic check
restic restore latest --target /tmp/restic-restore-test --verify

1. 先明确两个密码
#

Restic + rest-server 同时涉及两类密码,混在一起最容易出问题。

密码用途泄露影响
REST Server 认证密码HTTP Basic Auth,决定能不能访问服务端仓库路径可上传、读取或删除仓库对象,取决于服务端模式
Restic 仓库加密密码解密仓库内容泄露后可读取备份内容;丢失后备份不可恢复

推荐用环境变量注入:

export RESTIC_REPOSITORY="rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a"
export RESTIC_PASSWORD="${RESTIC_REPOSITORY_PASSWORD}"

如果 REST Server 认证密码包含 @:/# 等 URL 特殊字符,需要先做 URL encode,或者改用不把认证信息写进 URL 的反向代理认证方案。

更稳妥的做法是使用密码文件或命令:

export RESTIC_REPOSITORY="rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a"
export RESTIC_PASSWORD_FILE="/etc/restic/server-a.repository.pass"

restic snapshots

不要把包含密码的完整 RESTIC_REPOSITORY 写进 shell 历史、工单、README 或博客正文。仓库加密密码也不能只存在聊天记录里,丢了就没有可恢复路径。

2. 客户端安装和初始化
#

macOS 上可以直接用 Homebrew 安装:

brew install restic
restic version

示例版本输出:

restic 0.18.1 compiled with go1.25.1 on darwin/arm64

初始化仓库:

export REST_SERVER_PASSWORD="change-rest-server-password"
export RESTIC_REPOSITORY_PASSWORD="change-repository-password"
export RESTIC_REPOSITORY="rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/macmini"
export RESTIC_PASSWORD="${RESTIC_REPOSITORY_PASSWORD}"

restic init

unset REST_SERVER_PASSWORD RESTIC_REPOSITORY_PASSWORD RESTIC_PASSWORD

如果仓库创建成功,会看到类似输出:

created restic repository <repo-id> at rest:http://backup-user:***@backup.example.internal:8000/backup-user/macmini/

Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.

首次备份:

restic backup /Users/example/Downloads/compressed

查看快照:

restic snapshots

3. rest-server 服务端参数
#

rest-server 是 Restic 的 HTTP 后端实现。常用参数如下:

参数作用建议
--path备份仓库数据根目录指向持久化磁盘,例如 /data/restic
--listen监听地址内网可用 0.0.0.0:8000,公网前面放反向代理
--htpasswd-fileBasic Auth 用户文件生产环境启用
--append-only追加模式,禁止删除和覆盖已有数据防勒索有价值,但 prune 要另行设计
--private-repos用户只能访问自己用户名下的仓库路径多用户强烈建议开启
--no-auth禁用认证只适合临时本机测试
--no-verify-upload不校验上传完整性只在低性能设备上谨慎使用
--group-accessible-repos让 UNIX 组共享仓库文件权限本地文件权限共享场景
--prometheus开启 /metrics需要监控时启用
--prometheus-no-auth/metrics 不鉴权只在受控网络内使用
--tls直接启用 HTTPS也可以交给 Nginx/Traefik 终止 TLS
--tls-cert / --tls-keyTLS 证书和私钥配合 --tls 使用
--tls-min-ver最低 TLS 版本建议 1.21.3
--max-size限制仓库最大容量防止单仓库占满磁盘
--debug调试日志排障时临时打开
--logHTTP 请求日志建议写到文件或 stdout 后由日志系统收集

基础启动:

rest-server \
  --path /data/restic \
  --listen 0.0.0.0:8000 \
  --htpasswd-file /data/restic/.htpasswd \
  --append-only \
  --private-repos

如果直接使用 TLS:

rest-server \
  --path /data/restic \
  --listen 0.0.0.0:8443 \
  --tls \
  --tls-cert /data/certs/tls.crt \
  --tls-key /data/certs/tls.key \
  --htpasswd-file /data/restic/.htpasswd \
  --append-only \
  --private-repos

自签名证书场景下,客户端可以临时使用 --insecure-tls,但更建议把 CA 证书正确导入信任链。

4. private-repos 的路径规则
#

启用 --private-repos 后,rest-server 会按 Basic Auth 用户名限制路径。假设认证用户是 backup-user,合法路径必须在 /backup-user/ 下面:

rest:http://backup-user:<password>@backup.example.internal:8000/backup-user
rest:http://backup-user:<password>@backup.example.internal:8000/backup-user/macmini
rest:http://backup-user:<password>@backup.example.internal:8000/backup-user/server-a

下面这种路径会失败:

rest:http://backup-user:<password>@backup.example.internal:8000/macmini

服务端会认为这个仓库不属于 backup-user,通常返回 401 Unauthorized 或类似认证失败信息。这个坑在新建仓库时很常见,因为 Restic URL 看起来只是少了一层路径,但 rest-server 会把它当成权限边界。

排查顺序:

现象优先检查
401 Unauthorized用户名、HTTP 密码、仓库路径是否以用户名开头
config does not exist仓库是否尚未 init,或路径写错
repository does not exist目标 repo 不存在,容器镜像可能会尝试自动初始化
403 Forbiddenappend-only 模式下执行了删除、prune 或覆盖类操作

5. 手动备份和恢复
#

手动备份适合先验证服务端连通性,再上定时任务。

备份单个目录:

export RESTIC_REPOSITORY="rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/macmini"
export RESTIC_PASSWORD="${RESTIC_REPOSITORY_PASSWORD}"

restic backup /Users/example/Downloads/compressed

查看快照:

restic snapshots

查看快照内容:

restic ls latest
restic ls latest /Users/example/Downloads/compressed

恢复到临时目录:

RESTORE_DIR="$(mktemp -d /tmp/restic-restore-test.XXXXXX)"
trap 'rm -rf "${RESTORE_DIR}"' EXIT

restic restore latest --target "${RESTORE_DIR}" --verify

只恢复指定路径:

restic restore latest \
  --target /tmp/restic-restore-test \
  --include /Users/example/Downloads/compressed \
  --verify

确认临时目录内容正确后,再决定是否覆盖原路径。覆盖系统目录前要停止相关服务,并确认目标路径没有新的业务写入。

6. 快照保留策略
#

常用策略:

restic forget \
  --prune \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6

这组规则不是简单相加,也不是按日期连续保留。Restic 会分别按 daily、weekly、monthly 选择快照,再取并集。只要一个快照被任意规则选中,就会保留。

可以这样理解:

规则含义
--keep-daily 7最近 7 个有快照的日期里,每天保留最新快照
--keep-weekly 4最近 4 个有快照的周里,每周保留最新快照
--keep-monthly 6最近 6 个有快照的月里,每月保留最新快照
--keep-within 30d保留最近 30 天内的全部快照

边界行为:

  • 某天没有快照,不会为了凑数生成快照。
  • 同一个快照同时命中 daily、weekly、monthly 时,只保留一份。
  • 刚开始备份、快照数量不足时,Restic 会尽量避免过早删除最早的可用快照。
  • --append-only 模式下,forget --prune 可能无法删除远端数据,因为服务端会拒绝删除请求。

append-only 的常见做法是分开职责:

角色权限动作
日常备份客户端append-only只上传备份,不删除
维护窗口管理员可删除在确认无异常后执行 prune

这样可以降低客户端被入侵后删除备份的风险,也能保留定期清理能力。如果服务端入口始终开启 --append-only,日常备份容器里不要配置 RESTIC_FORGET_ARGS,否则删除快照引用时也可能被服务端拒绝。

维护窗口清理 append-only 仓库时,优先把 --keep-within 加进策略:

restic forget \
  --dry-run \
  --keep-within 30d \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6

原因是 append-only 客户端如果被入侵,攻击者仍可能追加带异常时间戳的快照。--keep-within 会保留一个时间窗口内的所有快照,管理员有机会先发现可疑快照或异常主机,再做真正删除。

7. Docker 定时备份
#

在 Docker/TrueNAS 环境里,用 mazzolino/restic 跑 cron 比宿主机手写 crontab 更容易迁移。下面是一个按天备份的模板,适用于允许客户端执行 forget --prune 的仓库入口。

这里用 registry.example.internal/mazzolino/restic:RESTIC_IMAGE_TAG 表示私有镜像仓库里的固定版本。实际部署时不要长期使用 latest,备份链路需要可复现。

services:
  restic-cron:
    image: registry.example.internal/mazzolino/restic:RESTIC_IMAGE_TAG
    container_name: restic-cron
    restart: always
    environment:
      RESTIC_REPOSITORY: "rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a"
      RESTIC_PASSWORD: "${RESTIC_REPOSITORY_PASSWORD}"
      BACKUP_CRON: "10 17 * * *"
      RUN_ON_STARTUP: "true"
      RESTIC_BACKUP_SOURCES: "/srv/app /srv/compose /etc /root/scripts"
      RESTIC_BACKUP_ARGS: >-
        --one-file-system
        --exclude-file /excludes.txt
      RESTIC_FORGET_ARGS: >-
        --prune
        --keep-daily 7
        --keep-weekly 4
        --keep-monthly 6
      TZ: Asia/Shanghai
    volumes:
      - /srv/app:/srv/app:ro
      - /srv/compose:/srv/compose:ro
      - /etc:/etc:ro
      - /root/scripts:/root/scripts:ro
      - ./excludes.txt:/excludes.txt:ro

如果环境允许,建议把仓库 URL 和仓库密码放入文件,再通过只读挂载交给容器:

services:
  restic-cron:
    image: registry.example.internal/mazzolino/restic:RESTIC_IMAGE_TAG
    environment:
      RESTIC_REPOSITORY_FILE: "/run/secrets/restic-repository"
      RESTIC_PASSWORD_FILE: "/run/secrets/restic-password"
    volumes:
      - ./secrets/restic-repository:/run/secrets/restic-repository:ro
      - ./secrets/restic-password:/run/secrets/restic-password:ro

这样能避免仓库密码直接出现在 Compose 文件里。但只要使用 Docker,本机 Docker 管理员仍然可以读取容器配置和挂载内容,密钥隔离边界不能理解成“对宿主机管理员保密”。

启动前先准备排除文件:

touch excludes.txt
docker compose up -d
docker compose logs -f

首次启动时,如果仓库不存在,镜像可能会先检查失败,再尝试初始化仓库。日志大致如下:

Checking configured repository 'rest:http://backup-user:***@backup.example.internal:8000/backup-user/server-a' ...
Fatal: repository does not exist: unable to open config file: <config/> does not exist
Trying to initialize (in case it has not been initialized yet) ...
created restic repository <repo-id> at rest:http://backup-user:***@backup.example.internal:8000/backup-user/server-a/
Repository successfully initialized.
Scheduling backup job according to cron expression.
new cron: 10 17 * * *

BACKUP_CRON 的字段数量要按镜像版本确认。当前环境日志已经验证 5 字段写法可用,例如 10 17 * * * 表示每天 17:10;Resticker 新文档也会看到 6 字段写法,第一位是秒,例如 0 10 17 * * * 表示每天 17:10:00。不要混用两种格式,上线前以容器启动日志里的 new cron: 输出为准。

如果不希望每次备份后立即 prune,可以把保留策略和数据清理拆开。下面的写法只适合具备删除权限的仓库入口;append-only 入口不要开启 RESTIC_FORGET_ARGS

备份服务只负责 forget,不带 --prune

services:
  restic-cron:
    image: registry.example.internal/mazzolino/restic:RESTIC_IMAGE_TAG
    container_name: restic-cron
    restart: always
    environment:
      BACKUP_CRON: "10 17 * * *"
      RUN_ON_STARTUP: "true"
      RESTIC_REPOSITORY: "rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a"
      RESTIC_PASSWORD: "${RESTIC_REPOSITORY_PASSWORD}"
      RESTIC_BACKUP_SOURCES: "/srv/app /srv/compose /etc /root/scripts"
      RESTIC_BACKUP_ARGS: >-
        --one-file-system
        --exclude-file /excludes.txt
      RESTIC_FORGET_ARGS: >-
        --keep-daily 7
        --keep-weekly 4
        --keep-monthly 6
      TZ: Asia/Shanghai
    volumes:
      - /srv/app:/srv/app:ro
      - /srv/compose:/srv/compose:ro
      - /etc:/etc:ro
      - /root/scripts:/root/scripts:ro
      - ./excludes.txt:/excludes.txt:ro

然后单独安排 prune 服务,只做数据重打包和空间回收:

services:
  restic-prune:
    image: registry.example.internal/mazzolino/restic:RESTIC_IMAGE_TAG
    container_name: restic-prune
    restart: always
    environment:
      PRUNE_CRON: "30 4 * * *"
      SKIP_INIT: "true"
      RESTIC_REPOSITORY: "rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a"
      RESTIC_PASSWORD: "${RESTIC_REPOSITORY_PASSWORD}"
      RESTIC_PRUNE_ARGS: >-
        --max-unused 5%
      TZ: Asia/Shanghai
    volumes:
      - ./excludes.txt:/excludes.txt:ro

定期校验可以再拆一个 check 服务:

services:
  restic-check:
    image: registry.example.internal/mazzolino/restic:RESTIC_IMAGE_TAG
    container_name: restic-check
    restart: always
    environment:
      CHECK_CRON: "15 5 * * *"
      SKIP_INIT: "true"
      RESTIC_REPOSITORY: "rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a"
      RESTIC_PASSWORD: "${RESTIC_REPOSITORY_PASSWORD}"
      RESTIC_CHECK_ARGS: >-
        --read-data-subset=10%
      TZ: Asia/Shanghai
    volumes:
      - ./excludes.txt:/excludes.txt:ro

这里最容易写错的是参数归属:RESTIC_FORGET_ARGS 传给 restic forget--keep-daily--keep-weekly--keep-monthly 应该放在这里;RESTIC_PRUNE_ARGS 传给 restic prune,不能放保留策略参数。上线前必须先在测试仓库里跑一次,确认日志里实际执行的命令符合预期。

8. 多机器仓库命名
#

多机器备份时,仓库命名要固定,便于定位恢复来源。

主机角色仓库路径示例备份路径示例
入口 Nginx/LB/backup-user/nginx-lb/etc/srv/compose/var/lib/docker/volumes/root
应用服务器/backup-user/server-a/srv/app/srv/compose/etc/root/scripts
macOS 客户端/backup-user/macmini/Users/example/Downloads/compressed

Nginx/LB 示例:

services:
  restic-cron:
    image: registry.example.internal/mazzolino/restic:RESTIC_IMAGE_TAG
    restart: always
    environment:
      RESTIC_REPOSITORY: "rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/nginx-lb"
      RESTIC_PASSWORD: "${RESTIC_REPOSITORY_PASSWORD}"
      BACKUP_CRON: "33 */8 * * *"
      RUN_ON_STARTUP: "true"
      RESTIC_BACKUP_SOURCES: "/etc /srv/compose /var/lib/docker/volumes /root"
      RESTIC_BACKUP_ARGS: >-
        --one-file-system
        --exclude-file /excludes.txt
      RESTIC_FORGET_ARGS: >-
        --prune
        --keep-daily 7
        --keep-weekly 4
        --keep-monthly 6
      TZ: Asia/Shanghai
    volumes:
      - /etc:/etc:ro
      - /srv/compose:/srv/compose:ro
      - /var/lib/docker/volumes:/var/lib/docker/volumes:ro
      - /root:/root:ro
      - ./excludes.txt:/excludes.txt:ro

备份 Docker volume 前要想清楚一致性问题。只读挂载能避免备份容器误写宿主机,但不能保证数据库这类在线写入数据天然一致。数据库优先用 dump、WAL、binlog 或服务自带备份机制,再把备份产物交给 Restic。

9. 恢复演练
#

没有恢复演练的备份不能算完成。建议每个仓库至少保留一套固定演练命令。

用容器恢复到宿主机目录:

mkdir -p /restore/server-a

docker run --rm -it \
  -e RESTIC_REPOSITORY="rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a" \
  -e RESTIC_PASSWORD="${RESTIC_REPOSITORY_PASSWORD}" \
  -v /restore/server-a:/restore \
  restic/restic restore latest --target /restore --verify

恢复指定快照:

restic snapshots
restic restore <snapshot-id> --target /restore/server-a --verify

仅查看内容,不恢复:

restic ls latest
restic ls <snapshot-id> /etc/nginx/nginx.conf

覆盖原路径前的停止条件:

信号动作
恢复目标目录和预期结构不一致停止,先确认快照路径
restic check 报错停止,先排查仓库完整性
目标服务仍在写入停止服务或切换维护窗口
需要恢复数据库目录优先使用数据库级恢复,不直接覆盖数据目录

10. 校验和排障命令
#

常用校验命令:

restic check
restic check --read-data-subset=10%
restic snapshots --last 5

check --read-data 会读取全部数据,远端 REST 仓库可能耗时很长。日常可以用 --read-data-subset=10% 做抽样,维护窗口再做全量校验。

异常退出留下锁时:

restic unlock

索引异常时:

restic rebuild-index

限速和缓存:

restic backup /srv/app \
  --limit-upload 8192 \
  --limit-download 8192 \
  --cache-dir /var/cache/restic

临时绕过缓存:

restic snapshots --no-cache

11. 验证矩阵
#

验证项方法通过标准
服务端认证restic snapshots能打开仓库,不返回 401
路径隔离使用错误路径访问返回认证或权限错误
初始化restic init仓库创建成功并生成 config
备份restic backup <path>生成新 snapshot
快照查询restic snapshots能看到对应 host/path
恢复restic restore latest --target <dir> --verify文件恢复且校验通过
保留策略restic forget --dry-run --keep-*待删除快照符合预期
仓库健康restic check无结构性错误

forget 上线前先 dry-run:

restic forget \
  --dry-run \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6

确认保留结果符合预期后,再移除 --dry-run,并按服务端是否 append-only 决定能否加 --prune

12. 常见问题
#

问题:401 Unauthorized

优先检查三件事:

  • REST 用户名和密码是否正确。
  • --private-repos 模式下,仓库路径是否以用户名开头。
  • URL 中是否多写或少写了仓库层级。

问题:容器启动时提示仓库不存在

可能是首次启动,也可能是仓库路径写错。先确认日志是否随后出现 Repository successfully initialized。如果没有初始化成功,手动执行:

restic -r "rest:http://backup-user:${REST_SERVER_PASSWORD}@backup.example.internal:8000/backup-user/server-a" init

问题:forget --prune 删除失败

如果服务端开启 --append-only,这是预期行为。日常客户端不应该有删除权限。需要清理空间时,在维护窗口切换到具备删除能力的管理入口,先 forget --dry-run,再 prune。

问题:备份 Docker volume 是否可靠

配置文件、静态文件、轻量状态通常可以直接备份。数据库、消息队列、对象存储元数据这类在线写入系统,应先生成一致性备份产物,再由 Restic 备份产物目录。

问题:仓库密码忘了怎么办

没有后门。Restic 仓库加密密码丢失后,备份数据不可恢复。密码必须纳入密钥管理和离线备份流程。

13. 可复用原则
#

这套 Restic 方案落地时,优先守住这几条:

  • 仓库路径和主机角色一一对应,不要多个主机混用同一个 repo。
  • REST 认证密码和仓库加密密码分开管理。
  • --private-repos 开启后,URL 路径必须带用户名命名空间。
  • 日常备份账号尽量 append-only,清理权限留给维护窗口。
  • 每个仓库都要有固定恢复演练命令。
  • forget --prune 上线前先 dry-run,再看 append-only 是否允许真正删除。
  • 数据库类服务优先备份一致性导出,而不是直接备份在线数据目录。

14. 参考
#

生产变更复盘 - 这篇文章属于一个选集。
7: 本文

相关文章

用 Docker Compose 部署 Draw.io 并接入 GitLab 存储
·1060 字·5 分钟
SRE Nginx DevOps Draw.io GitLab +4
用 Docker Compose 部署 ALLinSSL 并接入 Nginx 反向代理
·564 字·3 分钟
SRE SSL TLS Nginx DevOps +4
用 Docker Compose 部署 Scrutiny:NVMe 健康监控的最小可用方案
·772 字·4 分钟
SRE NVMe SMART SRE DevOps +3