rest-server --private-repos 路径规则,以及 forget --prune 在 append-only 场景里的行为。这篇把客户端备份、服务端参数、Docker 定时任务和恢复演练整理成一条可复用流程。backup.example.internal、192.0.2.31、backup-user、server-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_PASSWORD、RESTIC_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-file | Basic 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-key | TLS 证书和私钥 | 配合 --tls 使用 |
--tls-min-ver | 最低 TLS 版本 | 建议 1.2 或 1.3 |
--max-size | 限制仓库最大容量 | 防止单仓库占满磁盘 |
--debug | 调试日志 | 排障时临时打开 |
--log | HTTP 请求日志 | 建议写到文件或 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 Forbidden | append-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 是否允许真正删除。- 数据库类服务优先备份一致性导出,而不是直接备份在线数据目录。
