Tested on: Ubuntu 24.04 LTS and AlmaLinux 9.4, restic 0.16.x. The patterns translate to restic 0.15+ unchanged; older versions have a few CLI differences flagged inline.
Why this matters
The most common “we had backups, but…” incidents reduce to one of four failures:
- The backup was on the same disk as the data. When the disk failed, both went together.
- The backup tool never encrypted anything. A misconfigured S3 bucket leaked the entire database — including the personal data the business had promised to protect.
- The encryption passphrase died with the original server. The off-site backups still exist; nobody can read them.
- The backup ran nightly and the most recent twelve failed silently. No alert, no log review, no drill.
restic addresses every one of these as a default. It encrypts in
the client, deduplicates, supports a wide range of off-site targets,
exposes a clean check command for integrity, and reports non-zero
exit codes on failure that integrate cleanly with cron and CI. It is
not the only acceptable choice — borgbackup is the close runner-up,
and for some local-first workloads it is the better fit — but for the
EEA-region, S3-compatible, off-site target this site recommends,
restic is the path of least resistance.
This guide is the deeper dive referenced from
postgres-backups and
privacy-by-design-server-build.
Where those guides treat restic as “step 4: off-site”, this one
covers the operational details that turn a daily push into a
defensible recovery story.
1. Install restic
1# Debian / Ubuntu
2sudo apt update && sudo apt install -y restic
3
4# RHEL / AlmaLinux (via EPEL)
5sudo dnf install -y epel-release && sudo dnf install -y restic
6
7restic version # confirm 0.15+ ideally 0.16+
The packaged version is often a few releases behind. If you need a recent feature (e.g. compression, added in 0.14), install the upstream binary instead:
1sudo restic self-update
2. Decide where the repository lives
A restic repository is a directory of encrypted, content-addressed blobs. Pick the location before you initialise:
| Target | When to use | DPA |
|---|---|---|
| S3 / S3-compatible (EEA region) | Default for production servers — Hetzner Storage Box, Wasabi EU, AWS eu-central-1, OVH |
Provider DPA required (GDPR Art. 28) |
| SFTP to a separate host | Smaller shops without an S3 plan, or where S3 is overkill | The remote host operator is your processor |
| Local disk on the same host | Never as the only target. Useful as a fast first-stage that you then sync off-host | n/a |
rclone to a cloud drive |
Acceptable for personal / low-stakes use; complicated DPA story for business | Depends on provider |
For the rest of this guide we use an S3-compatible endpoint as the canonical example, named in the privacy notice’s processor table. Any EEA-region S3-compatible service works — the commands do not change.
3. Passphrase strategy — the load-bearing decision
This is the single most important choice in the whole setup. A restic repository is encrypted symmetrically; lose the passphrase, lose the backups. There is no recovery, by design. Three rules:
- Long and random. 30+ characters, generated by a password manager. A memorable phrase is a security regression here — the passphrase travels in a file, not in your head.
- Stored outside the original server. Password manager that survives the loss of the server. Treat it like the root TOTP seed for a domain registrar.
- Known to at least two people. A single-passphrase-holder is one bus-incident away from un-recoverable backups.
Concretely:
1# Generate a passphrase in your password manager (or with openssl).
2openssl rand -base64 33 | tr -d '/+=' | head -c 40
3# → store in 1Password / Bitwarden / Vault under
4# "stackharden / production backups / restic"
5
6# On the server, write it to a file the backup user can read.
7sudo mkdir -p /etc/restic
8sudo install -o root -g root -m 600 /dev/stdin /etc/restic/passphrase.txt <<< 'PASTE_HERE'
The local file at /etc/restic/passphrase.txt is the operational
copy. The password manager is the authoritative copy. If the file
is deleted, the password manager restores it. If the server dies,
the password manager is what you reach for.
4. Initialise the repository
1export RESTIC_REPOSITORY=s3:s3.eu-central-1.amazonaws.com/stackharden-backups
2export RESTIC_PASSWORD_FILE=/etc/restic/passphrase.txt
3export AWS_ACCESS_KEY_ID=...
4export AWS_SECRET_ACCESS_KEY=...
5
6restic init
Notes:
- The S3 bucket must already exist. restic does not create it.
- Use a dedicated S3 IAM user with
PutObject,GetObject,DeleteObject,ListBucketon this bucket only. Not the account-wide credentials. - Server-side encryption (SSE-S3 / SSE-KMS) is optional and additive. The data is already client-side encrypted; SSE is a defence in depth against a misconfigured bucket policy, not a substitute for restic’s own encryption.
5. The daily backup script
/usr/local/bin/restic-backup:
1#!/usr/bin/env bash
2set -euo pipefail
3
4# Configuration lives in /etc/restic/env — sourced from systemd or cron.
5: "${RESTIC_REPOSITORY:?}"
6: "${RESTIC_PASSWORD_FILE:?}"
7
8TAG="$(hostname)"
9LOCK_HOLD=600 # seconds — abort a stale lock from a failed run
10
11# 1. Drop a stale lock if one exists.
12restic unlock --no-cache || true
13
14# 2. Run the backup. --tag groups snapshots per host so retention
15# can be per-host even in a shared repository.
16restic backup \
17 --tag "$TAG" \
18 --exclude-file=/etc/restic/excludes \
19 --one-file-system \
20 /etc /home /srv /var/log /var/backups
21
22# 3. Apply retention. --prune removes blobs unreferenced by any
23# retained snapshot.
24restic forget \
25 --tag "$TAG" \
26 --keep-daily 7 \
27 --keep-weekly 4 \
28 --keep-monthly 12 \
29 --prune
30
31# 4. Verify the repository structure (cheap — metadata only).
32restic check --no-cache
/etc/restic/excludes excludes paths that are noise or dangerous:
# Caches and ephemeral data
**/node_modules
**/.cache
**/__pycache__
/var/log/journal/**
/var/cache/**
/tmp/**
# Other people's backups (avoid double-backing)
/var/lib/postgresql/16/backups/**
# Anything containing live secrets you have committed to never copying
Schedule via systemd timer (preferred — it logs to the journal and
supports OnFailure=) or cron. The script’s set -euo pipefail plus
restic’s own exit codes mean a failure is loud, not silent.
6. Retention — and matching the policy you publish
The numbers above (7 daily / 4 weekly / 12 monthly) are a defensible
starting point. They must reconcile with the user-data retention
period in your privacy notice — see
privacy-by-design-server-build
for the reasoning. Common adjustments:
- Cut the monthly retention if your privacy policy promises “we delete user data within 30 days of account closure” and you do not have a legal basis for keeping it longer.
- Extend the weekly retention for environments under legal hold.
- Add
--keep-yearlyonly if you have a specific compliance obligation that requires it. Yearly retention compounds — five yearly snapshots of a 100 GB system is more storage than people expect.
7. Verification — restic check is not enough
There are three levels of verification, each with a different cost:
| Level | Command | Cost | What it verifies |
|---|---|---|---|
| Metadata | restic check |
seconds — local index only | Structure of the repository; no actual blob reads |
| Sample blobs | restic check --read-data-subset 5% |
minutes — downloads 5% of data | Random blobs decrypt and match their hash |
| Full | restic check --read-data |
hours — downloads everything | Every blob in the repo decrypts and hashes correctly |
Recommended cadence:
- Daily: metadata check (already in the backup script).
- Weekly: 5% sample check from cron.
- Monthly: full read-data verification. Schedule for off-peak.
A read-data check is the only way to detect bit-rot at the storage provider — file integrity at the source plus the provider’s checksums do not, in practice, catch every case.
8. Restoration drills
A backup is not a backup until it has been restored. The drill, at minimum quarterly:
1# 1. Pick a snapshot.
2restic snapshots --tag "$(hostname)" | tail
3
4# 2. Restore to a temp directory.
5sudo mkdir -p /var/restore-test
6restic restore <snapshot-id> --target /var/restore-test
7
8# 3. Sanity-check key files.
9ls -la /var/restore-test/etc/passwd
10# ... and whatever else matters for your stack
11
12# 4. Time it. RTO is the metric, not RPO.
Record the date, the snapshot ID, the time-to-restore, and any anomalies. A spreadsheet line per drill is the level of documentation an auditor expects to see.
Once a year, do the drill on a different host, restoring to a fresh VPS — that is the only way to verify the passphrase survives the loss of the original server.
9. Monitoring backup health
A silent backup failure is the default failure mode. Wire alerting on at least:
- Non-zero exit from the backup script. systemd
OnFailure=to an email or webhook unit; cron piping tologger -p user.errand alerting on the syslog channel. restic checknon-zero. Same as above; the metadata check inside the backup script handles this.- Repository quiet for >36 hours. The most insidious failure is a
cron job that stops firing. A scheduled job elsewhere — your
monitoring host, a hosted cron — that queries
restic snapshotsand alerts on the most recent timestamp being older than the expected interval.
restic stats --mode raw-data produces machine-readable output
suitable for Prometheus scraping if you have a Prometheus textfile
collector available.
10. Restoring a single file or partial directory
The common case is not “everything is gone” — it is “I just deleted the wrong file”:
1# List versions of a file across snapshots
2restic find /etc/nginx/nginx.conf
3
4# Restore a single file from a specific snapshot
5restic restore <snapshot-id> --target /tmp/restore \
6 --include /etc/nginx/nginx.conf
7
8# Or mount the snapshot read-only for browsing
9mkdir /mnt/restic
10restic mount /mnt/restic
11# Browse via /mnt/restic/snapshots/<id>/...
12# Ctrl-C in the mount terminal to unmount.
restic mount requires FUSE; on hardened hosts that is sometimes not
available, in which case restore --include is the path.
Gotchas
Passphrase rotation is not a normal operation
restic supports adding additional passwords to a repository
(restic key add) and removing existing ones (restic key remove).
This is useful when staff turnover means a previous passphrase has
walked out of the door. It is not a routine rotation activity —
each key add creates a new master-key wrapper but does not re-encrypt
data. The cost of full passphrase rotation is a full re-backup.
restic prune and contention
restic prune rewrites data and cannot run concurrently with backup.
The retention pattern in the daily script (forget --prune) acquires
the repository lock during pruning, which on large repositories can
block the next scheduled backup. For repositories above ~100 GB,
split: daily forget (cheap), weekly prune (heavier, off-peak).
Skipped --one-file-system with bind mounts
--one-file-system is a sensible default — it stops restic descending
into network mounts, FUSE mounts, and /proc / /sys. But it
also stops descent into bind mounts that legitimately need
backing up. Audit your /etc/fstab for bind mounts before relying on
the flag.
Backup target credentials with too much scope
The dedicated S3 IAM user should have privileges scoped to the single bucket. A common mistake is reusing the application’s S3 credentials — which often have wider scope — for backup. A compromise of the production app then leaks the backup target’s credentials too.
Repository in the same blast radius as the source
A “backup” S3 bucket in the same cloud account, same region, same billing entity as the production VPS is one stolen credential away from being deleted alongside the production data. Cross-account or cross-provider backup is a meaningful upgrade — at minimum, enable versioning + MFA-delete on the backup bucket so a credential compromise cannot wipe history.
What this guide deliberately does not cover
- borgbackup as an alternative — equally credible, different ergonomics; a comparison guide is on the backlog.
- Database-specific backup orchestration — see
postgres-backupsforpg_dump/ WAL archiving feeding restic. - Bare-metal restore — restic restores files; full system recovery from bare metal also needs a known-good base image plus a documented order of operations. Out of scope here.
- Restic Server (the optional REST endpoint that fronts a filesystem repository) — useful in private networks; a separate guide.