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.
--pruneis 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/mysqldirectly while MySQL is running. Usemysqldump(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 -c3if 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.