Featured image of post Restic for real 3-2-1 backups: encrypted, deduplicated, off-site

Restic for real 3-2-1 backups: encrypted, deduplicated, off-site

Proxmox backups protect your VMs. They do not protect the directory on the NAS that holds ten years of photos, and they certainly do not survive losing the building. Restic fills that gap: client-side encryption, content-defined chunking for deduplication, and a repository that can live on any S3-compatible bucket.

This is the layout I use, and the parts that took me two attempts to get right.

1. Repository layout

One repository per data class, not one per machine. Mixing a 40 GB media library with nightly config snapshots means every forget run has to walk both, and pruning gets slow:

export RESTIC_REPOSITORY=s3:https://s3.eu-central-003.backblazeb2.com/my-backups

restic init --repo $RESTIC_REPOSITORY/config   # /etc, dotfiles, dashboards
restic init --repo $RESTIC_REPOSITORY/media    # photos, documents
restic init --repo $RESTIC_REPOSITORY/db       # nightly SQL dumps

Write the password to a file you mount into the job, never into the command line — it lands in ps and in your shell history.

install -m 600 /dev/null /root/.restic-pass
echo '<long passphrase>' > /root/.restic-pass
export RESTIC_PASSWORD_FILE=/root/.restic-pass

2. Dump databases first, always

Copying a live MySQL data directory gives you a torn backup that restores into a corrupt cluster. Dump, then back up the dump:

#!/usr/bin/env bash
set -euo pipefail
export RESTIC_PASSWORD_FILE=/root/.restic-pass
export RESTIC_REPOSITORY=s3:https://s3.eu-central-003.backblazeb2.com/my-backups/db

DUMPDIR=/var/backups/sql
mkdir -p "$DUMPDIR"
mysqldump --single-transaction --quick --routines \
  --databases app_db analytics_db | gzip > "$DUMPDIR/all-$(date +%F).sql.gz"

restic backup "$DUMPDIR" --tag sql --host db01
find "$DUMPDIR" -name '*.sql.gz' -mtime +7 -delete

--single-transaction matters: it gives InnoDB a consistent snapshot without locking writes. On PostgreSQL, pg_dump -Fc is the equivalent.

3. Retention that matches reality

restic forget \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 2 \
  --prune

forget without --prune only deletes snapshot references; the data stays until a prune runs. Run prune weekly rather than nightly — it rewrites pack files and is the expensive part.

Two subtleties worth knowing:

  • Deduplication is per repository. A file copied into two repositories is stored twice. That is exactly why separate repositories per data class work: media does not need to be deduplicated against config files.
  • --group-by host (the default) keeps retention independent per host. If you back up the same host from two paths, check restic snapshots to make sure you are not double-counting.

4. Verify the repository, not just the job exit code

# structural check, reads all metadata
restic check --read-data-subset=5%

# full read of every pack, monthly
restic check --read-data

A 5% subset weekly plus a full --read-data monthly is a reasonable compromise. Bit rot in object storage is rare, and detecting it is exactly why you pay for checksums.

5. The restore that proves it

Monthly, restore a random file to a scratch directory and diff it:

restic snapshots --latest 3
restic restore latest --target /tmp/restore-test --include /var/backups/sql/all-2026-10-01.sql.gz
gunzip -t /tmp/restore-test/var/backups/sql/all-2026-10-01.sql.gz

If gunzip -t passes, the data survived the round trip. If it does not, you have weeks of silent corruption to deal with.

Pitfalls

  • Cache growth. restic keeps a local cache under ~/.cache/restic that can reach several GB on a large repo. Set --no-cache for one-off scripts, keep it for scheduled jobs — it makes incremental runs much faster.
  • S3 lifecycle rules deleting packs. If the bucket has an expiry rule, it will happily delete repository data. Disable lifecycle management on backup buckets.
  • Forgotten --host. Two machines backing up to the same repo with the same hostname tag confuse retention. Give each host a unique --host.
  • Only one copy. Restic on the same NAS as the source is not 3-2-1. The whole point is a copy somewhere else.

Restic is not glamorous, but it is the difference between an incident and a bad afternoon.

Built with Hugo
Theme Stack designed by Jimmy