rsync to a directory is fine for "I have one server and a USB disk." The moment you have more than one box, want offsite, or need to restore a specific file from three weeks ago — Restic is the upgrade. Encrypted, deduplicated, content-addressable, with proper retention pruning.

I run this on every server I care about. Two restic commands a day, plus a monthly restore drill that proves the backups actually work.

TL;DR:

# Install
sudo apt install restic

# Initialize repository (one-time)
export RESTIC_REPOSITORY="s3:https://s3.eu-central-1.amazonaws.com/my-backup-bucket"
export RESTIC_PASSWORD_FILE="/root/.restic-password"
restic init

# Backup (runs nightly via systemd timer)
restic backup /etc /home /var/lib /opt --tag nightly

# Prune old backups (runs weekly)
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

Each backup after the first is fast — only changed file blocks are uploaded. A 200 GB box with 1 GB of daily churn does its nightly in 3-5 minutes.

Why Restic over alternatives

  • vs rsync: rsync's a sync, not a backup. No retention semantics, no encryption, no deduplication across snapshots. Identical files in 10 daily snapshots = 10× storage.
  • vs Borg: Borg is excellent but only writes to filesystems (or via SSH). Restic writes natively to S3, B2, Azure, GCS, MinIO, REST server. Easier offsite without running an SSH-accessible host.
  • vs Veeam / commercial: they work, they're expensive, and the on-premises ones have their own attack surface. Restic is a single static binary, no agent, no service.
  • vs tar | gzip: see "no retention semantics, no dedup."

The one downside: restoring a single file from a 1 TB repo takes a few seconds longer than cp from a directory backup. That's the tradeoff for cryptographic integrity and dedup.

Pick a backend

MinIO (self-hosted S3-compatible): if you already run a homelab box with disk to spare, run MinIO there. €0/month after the hardware. Don't put MinIO on the same box you're backing up — defeats the purpose.

Hetzner Object Storage / Wasabi / Backblaze B2: cheap commercial S3-compatible. ~€5-10/month for a few hundred GB. No egress fees on B2 — important for restores.

AWS S3: more expensive than B2/Wasabi, but the ecosystem is broadest. Use Glacier Deep Archive for long-term, S3 Standard for active.

I use a mix: hot copy to MinIO (fast restore), cold copy to B2 (offsite, survives the homelab burning down).

Set up the repository

Pick the repo URL based on backend. For S3:

export RESTIC_REPOSITORY="s3:https://s3.eu-central-1.amazonaws.com/my-bucket/server01"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."

For self-hosted MinIO:

export RESTIC_REPOSITORY="s3:http://minio.lab.example.com:9000/backup/server01"
export AWS_ACCESS_KEY_ID="restic-server01"
export AWS_SECRET_ACCESS_KEY="..."

Each server gets its own subpath in the bucket (server01, server02...). Don't share a single repo across multiple sources — restoring one corrupts the other on the rare bad day.

Set the repo password (this encrypts everything):

echo 'a-long-randomly-generated-password' > /root/.restic-password
chmod 600 /root/.restic-password
export RESTIC_PASSWORD_FILE="/root/.restic-password"

Save this password somewhere outside the server. Lose it, the encrypted backup is permanently unreadable. Same as a BitLocker recovery key — copy it to your password manager and a printout.

Initialize:

restic init
# created restic repository abc123 at s3:...

First backup

restic backup /etc /home /var/lib /opt \
  --exclude-file=/root/.restic-excludes \
  --tag nightly

Excludes file (/root/.restic-excludes):

/var/lib/docker/overlay2
/var/lib/lxcfs
/var/cache
/tmp
/proc
/sys
*.pyc
node_modules

Don't back up /var/lib/docker/overlay2 (huge, regenerable), /var/cache (regenerable), or anywhere that's a virtual FS. Add app-specific excludes as needed.

First backup uploads everything. A 50 GB box takes 30-60 minutes depending on bandwidth. Subsequent backups are minutes.

Verify

After the first backup:

restic snapshots
# ID        Time                 Host        Tags    Paths
# abcdef12  2026-04-27 02:00:00  server01    nightly /etc, /home, /var/lib, /opt

restic stats
# Total File Count:   24532
# Total Size:        18.234 GiB

Check repository integrity:

restic check
# load indexes
# check all packs
# check snapshots, trees and blobs
# no errors were found

Run check --read-data monthly. Slower (downloads every blob) but catches silent corruption.

Automate with systemd timer

/etc/systemd/system/restic-backup.service:

[Unit]
Description=Restic backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
EnvironmentFile=/root/.restic-env
ExecStart=/usr/bin/restic backup /etc /home /var/lib /opt \
  --exclude-file=/root/.restic-excludes \
  --tag nightly
ExecStartPost=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

/root/.restic-env:

RESTIC_REPOSITORY=s3:https://...
RESTIC_PASSWORD_FILE=/root/.restic-password
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...

Permissions: chmod 600 /root/.restic-env.

/etc/systemd/system/restic-backup.timer:

[Unit]
Description=Run Restic backup daily

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer

RandomizedDelaySec smears load across hosts so 100 servers don't all hit S3 at exactly 02:00. Persistent=true runs missed schedules after reboot.

Same systemd unit pattern as in systemd service unit files, just oneshot + timer instead of simple.

Retention

The forget --prune line:

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

Keeps:

  • The last 7 daily snapshots
  • The last 4 weekly snapshots
  • The last 12 monthly snapshots

23 total snapshots, covering 1 year. Adjust per appetite — --keep-yearly 5 for compliance retention, --keep-tag annual --keep-last 3 for labeled long-term.

--prune actually removes the data from the backend. Without it, snapshots are forgotten but bytes remain. Always run with --prune unless you're auditing what would be deleted.

The restore drill — actually do this

Backups you've never restored from aren't backups. They're hopes.

Once a month, pick a random file you'd care about losing, restore it to a scratch path, diff against the original:

restic restore latest --target /tmp/restore-test --include /etc/nginx/nginx.conf
diff /etc/nginx/nginx.conf /tmp/restore-test/etc/nginx/nginx.conf

Quarterly: full restore to a separate VM. Boot it, see if applications come up.

Annually: a "what if the source server is gone" drill. Initialize a new VM, install Restic, point at the repo with the same password, restore everything.

I mark these in the calendar. They've caught:

  • A wrong-permissions issue where Restic was running as a user that couldn't read /var/lib/postgresql — backup looked successful, was empty inside.
  • A rotation bug where retention had quietly evicted everything older than 7 days because someone fat-fingered the cron line.
  • An MinIO bucket that had quietly hit a quota.

If you don't drill, you don't actually have backups. Drill.

Cross-server bandwidth

A backup of a 200 GB server pushed to S3 over a 100 Mbit link takes 4-5 hours the first time. Subsequent nightly runs upload only changed blocks — usually 100 MB - 1 GB, finishes in minutes.

If you have many similar servers, the dedup is across hosts when they share a repo path prefix and the same encryption key. I keep separate repos per host (simpler, isolation), so I miss some cross-host dedup. For a homelab with 5 boxes, the storage savings of shared dedup aren't worth the operational risk.

Gotchas

  • Don't lose the password. Said it once already. Saying it again. There is no recovery.
  • Don't back up secrets in plaintext. Restic encrypts the backup, but if someone steals the repo URL + password, they have everything. Treat the password like the data itself.
  • --prune is slow on large repos. A 1 TB repo with months of history takes 30-60 min to prune. Schedule weekly, not nightly, on big sets.
  • MinIO needs versioning OFF for the bucket. Restic manages its own versions; MinIO/S3 versioning on top doubles or triples storage.
  • Don't back up /var/lib/mysql directly while MySQL is running. Use mysqldump (or PITR via binlogs) into a directory, then back up the directory. Same for Postgres / MongoDB. File-level backups of running databases give you Frankenstein corruption.
  • Restic processes are CPU-bound. Encryption + dedup hashing on a 4-core box will pin one core. Schedule for off-hours; use nice -n 19 ionice -c3 if you must run during work hours.

How this combines with other things

  • rsync setup — fine for fast local snapshots; Restic is the offsite layer above it.
  • linux-disk-full forensics — when prune misbehaves and your backup target fills, that post is the recovery.
  • SSH hardening — apply before storing repo credentials on a server.

The setup above is what I'd recommend to anyone who has more than "a couple of family photos" to back up. It's not the simplest tool, but it's the one I trust at 3am when everything else has gone wrong.


Related posts