Restic: The Only Backup Tool You’ll Ever Need (And How to Actually Test Your Restores)

Most people have a backup system. Almost nobody has a working backup system. There’s a difference, and you only find out which one you have at the worst possible moment — when you’re staring at a corrupted drive at 2 AM, trying to remember the last time you actually verified anything.

Restic fixes this. Not by being magic, but by being designed by someone who clearly got burned by bad backup tools before. It’s fast, it deduplicates, it encrypts everything client-side, and it works with a dozen different storage backends without requiring you to rewire your brain for each one.

This article covers everything from zero to a production-grade setup: repositories on multiple backends, automated scheduling, retention policies, and — most importantly — a restore testing workflow you’ll actually run.

Official repo: https://github.com/restic/restic


Why Most Backup Setups Are a Lie

The classic backup failure isn’t "I had no backups." It’s "I had backups, but when I needed them, they were broken." Bit rot, incomplete writes, a repo that was initialized but never actually backed up to, a cron job that silently died six months ago — these are the real killers.

Traditional tools like rsync + tar don’t help here. They’ll happily create a backup of a filesystem that’s mid-write and give you a corrupted archive. No deduplication, no encryption, no built-in integrity verification. You end up duct-taping monitoring, encryption, and scheduling on top, and it’s always the duct tape that fails.

Restic’s architecture avoids all of this. Every snapshot is stored as a tree of content-addressed, deduplicated, AES-256-CTR encrypted chunks. The repository is self-describing — you don’t need external metadata to restore from it. And restic check actually verifies the data, not just the file listing.


Installation

Restic ships as a single static binary. No runtime dependencies, no Python 3.8 vs 3.11 fights, no apt source lists to wrestle with.

Ubuntu/Debian (repo version is usually stale — grab the binary directly):

# Get the latest release from GitHub
RESTIC_VERSION=$(curl -s https://api.github.com/repos/restic/restic/releases/latest \
  | grep '"tag_name"' | cut -d'"' -f4 | tr -d 'v')

curl -fsSL "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2" \
  | bunzip2 > /usr/local/bin/restic

chmod +x /usr/local/bin/restic
restic version

Or via your package manager if you don’t mind a version or two behind:

# Debian/Ubuntu
apt install restic

# Arch
pacman -S restic

# macOS
brew install restic

Repository Backends

Restic isn’t tied to one storage backend. It abstracts the storage layer, so switching from local disk to Backblaze B2 to a rented VPS running SFTP is a matter of changing a connection string. The same init, backup, restore commands work everywhere.

Local Disk

Good for quick tests and on-server caches. Not a real offsite backup — don’t fool yourself.

restic init --repo /mnt/backup-drive/myserver

SFTP (SSH Remote)

The most common self-hosted option. Point it at any server where you have SSH access.

restic init --repo sftp:[email protected]:/var/restic/myserver

Your SSH key needs to be authorized on the remote. If you want to restrict the backup user from doing anything other than SFTP, add this to their authorized_keys:

command="internal-sftp",no-pty,no-port-forwarding,no-x11-forwarding,no-agent-forwarding ssh-ed25519 AAAA...

S3-Compatible (AWS S3, Backblaze B2, MinIO)

Backblaze B2 is the go-to here — it’s cheap, reliable, and S3-compatible enough that restic works with it natively.

export AWS_ACCESS_KEY_ID="your-key-id"
export AWS_SECRET_ACCESS_KEY="your-application-key"

# For Backblaze B2:
restic -r s3:s3.us-west-004.backblazeb2.com/your-bucket-name init

# For actual AWS S3:
restic -r s3:s3.amazonaws.com/your-bucket-name init

Gotcha: B2 bucket names must be globally unique. Also, create a restricted application key for restic — one with read/write access to a specific bucket only. Never use your master key.

REST Server (restic’s own HTTP backend)

If you’re running your own server, rest-server is a lightweight Go daemon that speaks restic’s native protocol. Better performance than SFTP for frequent backups.


Initializing a Repository

Every backend uses the same init command. What changes is only the --repo path.

restic init --repo /mnt/backup-drive/myserver

You’ll be prompted for a password. This password encrypts the repository key. Lose it, lose everything. Store it in a password manager, not in a README on the same server you’re backing up.

Gotcha: Restic uses a single password to decrypt a master key stored in the repo. You can add multiple passwords later (restic key add), which is useful for team access or key rotation. Don’t confuse "the password" with "the actual encryption key" — the repo key itself can be exported and stored separately.


Running Your First Backup

restic backup \
  --repo /mnt/backup-drive/myserver \
  /home \
  /etc \
  /var/www \
  --exclude /home/*/.cache \
  --exclude /home/*/node_modules \
  --exclude '*.log'

Restic will walk the paths, chunk the data, deduplicate against what’s already in the repository, and store only new chunks. First run is slow. Subsequent runs are fast — often completing in seconds if not much has changed.

List your snapshots:

restic -r /mnt/backup-drive/myserver snapshots

You’ll get a table of snapshot IDs, timestamps, hostnames, and paths. Those short IDs (like 3f1a2b4c) are what you use for restore operations.


Environment Files — Stop Typing Passwords in Terminal History

Every restic command needs the repo path and the password. Don’t paste them as flags. Use an environment file, keep it 0600, and source it.

# /etc/restic/backup.env
# Mode: 0600, owner: root

RESTIC_REPOSITORY="sftp:[email protected]:/var/restic/myserver"
RESTIC_PASSWORD="your-strong-password-here"

# For S3/B2 backends, also include:
# AWS_ACCESS_KEY_ID="..."
# AWS_SECRET_ACCESS_KEY="..."

Load it before running restic:

source /etc/restic/backup.env && restic snapshots

Or reference it directly in scripts with set -a; source /etc/restic/backup.env; set +a.


Automating Backups with systemd

Cron works, but systemd timers give you better logging, failure handling, and the ability to run missed jobs after a reboot (via Persistent=true). Use them.

Create the backup script:

# /usr/local/bin/restic-backup.sh
#!/usr/bin/env bash
set -euo pipefail

# Load credentials
set -a
source /etc/restic/backup.env
set +a

LOGFILE="/var/log/restic-backup.log"
exec >> "$LOGFILE" 2>&1

echo "=== Backup started: $(date -Iseconds) ==="

# Run backup
restic backup \
  /home \
  /etc \
  /var/www \
  --exclude '/home/*/.cache' \
  --exclude '/home/*/node_modules' \
  --exclude '*.log' \
  --one-file-system \
  --tag "$(hostname)" \
  --tag "daily"

# Apply retention policy — keep:
# 7 daily, 4 weekly, 6 monthly, 2 yearly snapshots
restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --keep-yearly 2 \
  --prune \
  --tag "$(hostname)"

echo "=== Backup completed: $(date -Iseconds) ==="
chmod +x /usr/local/bin/restic-backup.sh

systemd service unit:

# /etc/systemd/system/restic-backup.service
[Unit]
Description=Restic Backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/restic-backup.sh
# Fail the unit if restic exits non-zero
SuccessExitStatus=0
# Don't mask the exit code
StandardOutput=journal
StandardError=journal

systemd timer unit:

# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily Restic Backup
Requires=restic-backup.service

[Timer]
# Run daily at 02:30
OnCalendar=*-*-* 02:30:00
# Run immediately on boot if last run was missed
Persistent=true
# Randomize start within 30 minutes to avoid thundering herd if you run multiple servers
RandomizedDelaySec=1800

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

# Check it's scheduled:
systemctl list-timers restic-backup.timer

Gotcha: Persistent=true in the timer means "if the server was off during the scheduled window, run immediately on next boot." This is almost always what you want. Without it, a server that was off at 02:30 will simply skip that day’s backup silently.


Retention Policies — Think Before You forget

The restic forget command removes snapshot references. --prune removes the unreferenced data chunks. Without --prune, your repo grows forever even after forget.

The retention policy above (7 daily, 4 weekly, 6 monthly, 2 yearly) is a reasonable default for a personal server. Production databases or compliance environments need different rules — know your recovery point objective before picking numbers.

Gotcha: forget --prune on a large repository can be slow and lock the repo for the duration. On repos with years of data, consider running restic prune separately on a less frequent schedule (weekly), and forget (without --prune) on the daily run. Then prune weekly to actually reclaim space.


Restore Testing — The Part Everyone Skips

Here’s the uncomfortable truth: a backup you haven’t restored from is a hypothesis, not a backup. The restore test is the only thing that converts it to fact.

Restic makes this easier than most tools because you can restore to any path, not just the original.

Spot-Check Restore (File-Level)

source /etc/restic/backup.env

# List snapshots
restic snapshots

# Restore a specific file from the latest snapshot to /tmp
restic restore latest \
  --target /tmp/restore-test \
  --include /etc/nginx/nginx.conf

# Verify it looks right
cat /tmp/restore-test/etc/nginx/nginx.conf
diff /tmp/restore-test/etc/nginx/nginx.conf /etc/nginx/nginx.conf

Full Directory Restore (Staging)

# Restore /home from 3 snapshots ago to a staging path
restic restore latest~3 \
  --target /mnt/staging-restore \
  --path /home/nikita

# Check file counts and sizes match expectations
find /mnt/staging-restore -type f | wc -l
du -sh /mnt/staging-restore

Integrity Check

restic check verifies the repository structure. restic check --read-data actually reads and decrypts all the pack files to verify no silent corruption.

# Fast check — structure only (seconds to minutes)
restic check

# Full data verification — reads everything from the backend (slow, but thorough)
restic check --read-data

Run restic check after every backup in your script. Run restic check --read-data monthly on a schedule. Add it to your timer:

# Add to /usr/local/bin/restic-backup.sh, after the backup section:
echo "=== Running integrity check ==="
restic check

Automated Restore Test Script

This runs a restore of a known file, checks it matches the live version, and emails you if it doesn’t. Schedule this weekly.

# /usr/local/bin/restic-restore-test.sh
#!/usr/bin/env bash
set -euo pipefail

set -a
source /etc/restic/backup.env
set +a

TEST_FILE="/etc/os-release"
RESTORE_DIR="/tmp/restic-restore-test-$$"
mkdir -p "$RESTORE_DIR"

# Restore the test file from latest snapshot
restic restore latest \
  --target "$RESTORE_DIR" \
  --include "$TEST_FILE" \
  --quiet

RESTORED="$RESTORE_DIR$TEST_FILE"

if [ ! -f "$RESTORED" ]; then
  echo "FAIL: Restored file not found at $RESTORED" >&2
  rm -rf "$RESTORE_DIR"
  exit 1
fi

if diff -q "$TEST_FILE" "$RESTORED" > /dev/null 2>&1; then
  echo "OK: Restore test passed at $(date -Iseconds)"
else
  echo "FAIL: Restored file differs from live file" >&2
  diff "$TEST_FILE" "$RESTORED" >&2
  rm -rf "$RESTORE_DIR"
  exit 1
fi

rm -rf "$RESTORE_DIR"

Multi-Repository Strategy (The 3-2-1 Approach, Done Right)

One repository is not enough. The practical minimum for a server you care about:

  • Repo 1: Local fast SSD/disk — for quick restores of recent files
  • Repo 2: Remote SFTP on a different physical location — for disaster recovery
  • Repo 3: Object storage (B2 or S3) — for long-term retention, offsite

Restic doesn’t natively support writing to multiple repos in one command. You run the backup command twice, pointing at different repos. Yes, that means two uploads. Yes, it’s worth it.

# Back up to local first (fast), then to remote
restic -r /mnt/local-backup backup /home /etc /var/www ...
restic -r sftp:[email protected]:/restic/myserver backup /home /etc /var/www ...

Use separate .env files per repo. Wrap both in the same systemd service with sequential ExecStart lines, or a script that runs both.

Gotcha: If you use tags for filtering in forget, make sure you apply the same forget/prune to all repositories. It’s easy to accidentally keep a lean local repo and a bloated remote one because you only ran forget against one of them.


Production Gotchas, Collected

Mount points and --one-file-system: Add this flag to avoid accidentally backing up remote NFS mounts, bind mounts, or /proc. Without it, restic will happily try to back up everything in sight.

Large file deduplication: Restic chunks files with a content-defined chunker (CDC). VM disk images, Docker volumes, and SQLite databases will deduplicate reasonably well between runs if the changes are localized. PostgreSQL base backups won’t deduplicate well — back up WAL files separately or use pg_dump and back up the dump.

Repository locking: If a backup process crashes, it may leave a stale lock. restic unlock clears it. Don’t just unlock blindly — verify no other restic process is actually running first.

Password management: Store the password in Bitwarden, Vault, or at minimum an encrypted local file. Test that you can retrieve it from your password manager without accessing the server you’re recovering. Many people discover their password manager data lived on the same disk they’re trying to restore.

Cold storage and key rotation: Restic supports multiple keys per repository (restic key list, restic key add, restic key remove). If you rotate a compromised password, add the new key, then remove the old one. Old snapshots are automatically accessible with the new key — the master key doesn’t change, just the envelope that wraps it.

Monitoring backup success: Your systemd timer failing silently is a real risk. Point systemd’s OnFailure= at a notification unit, or at minimum check your backup log in your weekly review. Better: use a dead man’s switch service (Healthchecks.io has a self-hosted version) — ping it at the end of a successful backup, get alerted if the ping stops arriving.

# Add to end of restic-backup.sh, after all checks pass:
# Replace with your actual Healthchecks.io URL or self-hosted equivalent
curl -fsS --retry 3 "https://hc-ping.com/your-uuid-here" > /dev/null

Migrating from rsync/tar

If you’re coming from an rsync + tar setup, you don’t need to migrate anything. Restic is a fresh start. Initialize a new repository, run your first backup, verify the restore, then decommission the old setup once you’re confident in the new one. There’s no import tool and you don’t need one.


Quick Reference Card

# Initialize a new repository
restic init -r /path/to/repo

# Back up paths
restic backup -r /path/to/repo /home /etc

# List snapshots
restic snapshots -r /path/to/repo

# Restore latest snapshot to /tmp/restore
restic restore latest -r /path/to/repo --target /tmp/restore

# Restore specific path from specific snapshot
restic restore 3f1a2b4c -r /path/to/repo --target /tmp/restore --include /etc/nginx

# Mount repository as FUSE filesystem (browse like a directory)
restic mount -r /path/to/repo /mnt/restic-browse

# Check repository integrity
restic check -r /path/to/repo

# Remove old snapshots + prune data
restic forget -r /path/to/repo --keep-daily 7 --keep-weekly 4 --prune

# Unlock stale lock
restic unlock -r /path/to/repo

The mount command deserves a mention: restic mount exposes all snapshots as a FUSE filesystem where you can browse history like directories. Incredibly useful for figuring out exactly which snapshot has the version of a file you want before committing to a full restore.


Restic won’t protect you from not running it, not verifying it, or storing the password in the same place as the data. The tooling is solid — the operational discipline is on you. Set up the timer, run the restore test script, add a dead man’s switch, and actually check the logs once a week. That’s the difference between "I have backups" and "I have a working recovery system."

👁 Views: 225,652 · Unique visitors: 59,963