Why automate your backups with Restic
A VPS often hosts irreplaceable data: databases, Docker volumes, configuration files. Restic meets three key requirements of a sound strategy: encryption (AES-256; the repository is unusable without the password), deduplication (identical blocks are stored only once, which drastically reduces volume and cost), and support for varied backends (S3, Backblaze B2, SFTP, local storage). Self-hosting Restic on the VPS rather than relying on a managed backup service gives you full control of encryption: the key stays with you, and the storage provider sees only opaque blobs. This is the foundation of a serious 3-2-1 strategy.
What Restic brings you
- Client-side AES-256 encryption: the storage backend sees only unreadable data.
- Block-level deduplication that greatly reduces the size and cost of successive backups.
- Fast incremental snapshots: only new blocks are transferred on each run.
- Multiple backends (S3, Backblaze B2, SFTP, rest-server, local) without changing your workflow.
- Granular restore: a single file, a folder or an entire snapshot via
restic restore. - Automated retention policy with
forget --prune(keep N daily, M weekly...).
The 3-2-1 rule: why a provider snapshot is not enough
Turning on your VPS provider's snapshots is useful, but it is not a backup strategy: it is a single extra copy, at the same provider, sharing the fate of your VM. Three scenarios show why. If the VM is deleted (a slip, an unpaid invoice, a termination), its snapshots often vanish with it. If an attacker takes over your hosting account, they can wipe VM and snapshots in a few clicks or through the API. And a snapshot is never restored as a test: a database frozen mid-write may turn out to be unusable on the day you need it.
The 3-2-1 rule answers these three risks: three copies of the data, on two distinct media, one of them outside the provider and encrypted before it leaves the server. Restic builds exactly that last link, the only one that survives losing the hosting account entirely.
The three copies of a 3-2-1 strategy on a VPS
- Copy 1, the live data on the VPS disk: the original, and the source of the fastest restores.
- Copy 2, the provider's snapshots: they cover a failed upgrade or a configuration mistake by rolling back the whole machine.
- Copy 3, the Restic repository at another object storage provider: encrypted client-side, it stays out of reach of your host's dashboard.
- Two truly separate media: the VPS disk on one side, a third party's object storage on the other, with distinct credentials.
Prerequisites for this deployment
Restic is frugal: it runs on any VPS, including 1 vCPU and 1 GB of RAM, with deduplication using a little memory in proportion to repository size. You need the restic binary, a remote destination backend (an S3-compatible bucket at a provider, or another server over SFTP) and its credentials. No Docker required here: Restic is a command-line tool.
Check the version your distribution ships: Debian 13 and Ubuntu 26.04 provide 0.18, but Ubuntu 24.04 is at 0.16.4 and Debian 12 at 0.14.0, too old for the --stdin-from-command option used below (introduced in 0.17.0). In that case, install the official binary from the releases page (0.19.1, published on 5 July 2026): only these official binaries include the restic self-update command, while a distribution package is updated through apt. Finally, plan a systemd timer (or cron) for automation, and a safe place outside the VPS to keep the repository password.
Backblaze B2 or Cloudflare R2: choosing the S3 backend
Scroll the table
| Criterion | Backblaze B2 | Cloudflare R2 |
|---|---|---|
| Free storage | The first 10 GB | 10 GB-month per month (Standard class) |
| Data egress | Free up to 3 times the average monthly stored volume, and with no cap to partner CDNs (Cloudflare, Fastly) | Free, no cap |
| S3 endpoint | Specific to the account's region, for example `s3.us-west-004.backblazeb2.com` | `<account-id>.r2.cloudflarestorage.com`, region `auto` |
| Access keys | A dedicated Application Key: the master key is rejected by the S3 API | An R2 API token with object read and write permissions |
| Watch out for | Frequent full restores can exceed the free egress allowance | Regular restore tests add no egress cost |
Setting up automated Restic backups
Install Restic and prepare the environment file
Install
restic(a recent package or the official binary, see the prerequisites), then create/root/.restic-env. Write it as plainKEY=valuelines, withoutexport: that is the format read by systemd'sEnvironmentFile=directive, and it also loads in a shell. With Restic, B2 is best used through its S3-compatible API:# /root/.restic-env (Backblaze B2, S3 API) RESTIC_REPOSITORY=s3:https://s3.us-west-004.backblazeb2.com/my-bucket RESTIC_PASSWORD=change-me AWS_ACCESS_KEY_ID=b2-key-id AWS_SECRET_ACCESS_KEY=b2-application-key # Cloudflare R2 # RESTIC_REPOSITORY=s3:https://<account-id>.r2.cloudflarestorage.com/my-bucketRestrict the file's permissions, then load it into your session:
chmod 600 /root/.restic-env set -a; . /root/.restic-env; set +aInitialize the encrypted repository
restic initThis command creates the repository structure and seals the encryption with your password; recent repositories (format 2, since Restic 0.14.0) also compress the data. Keep this password off the VPS, in a password manager or a vault on another machine: without it, no restore is possible, even with the complete bucket.
Run a first backup
Back up your critical directories, then your databases. For a dump, prefer
--stdin-from-commandto apg_dump | restic backup --stdinpipe: Restic cannot tell whether a stream read from standard input is complete, and a dump that fails midway would produce a truncated snapshot without any error. With--stdin-from-command, a non-zero exit code from the command cancels the backup and no snapshot is created.restic backup /etc /var/www /opt/docker-data restic backup --stdin-filename mydb.sql --stdin-from-command -- pg_dump -U postgres mydb restic snapshotsDefine a retention policy
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --pruneSeven dailies cover a full week, enough to catch corruption that only shows up after a few days. Four weeklies open a one-month window for a slow application problem, such as data disappearing bit by bit. Six monthlies let you go back to a state from before a failed migration. The
--prunepass is essential:forgetalone removes snapshots from the list without freeing space, and the storage bill keeps growing.Check integrity: check, then read-data
These commands do not verify the same thing:
restic check restic check --read-data-subset=10% restic check --read-datarestic checkvalidates the repository structure (snapshots, trees, indexes, packs) without reading the content of the data files: it is fast and runs after everyforget --prune.--read-datadownloads and verifies every pack, which takes time and consumes egress bandwidth. In between,--read-data-subsetchecks a random share (10%, or a size such as5G) or a fixed slice (1/5to5/5, covering everything in five runs). A partial check every week and a full one once a month is a sound compromise.Write the backup script and its systemd service
Group the steps into a script. It first writes a timestamped sentinel file, used later by the restore test, and stops at the first error thanks to
set -eu:#!/bin/sh # /usr/local/bin/restic-backup.sh set -eu date +%s > /opt/restic-sentinel.txt restic backup /etc /var/www /opt/docker-data /opt/restic-sentinel.txt restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune restic checkThe service runs it with the variables from the environment file, without any secret appearing in the unit definition:
# /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/local/bin/restic-backup.shSchedule it with a systemd timer
The timer triggers the service every night.
Persistent=truecatches up on a missed run if the VPS was off at the scheduled time, andRandomizedDelaySec=900spreads the start over fifteen minutes, useful when several servers target the same bucket:# /etc/systemd/system/restic-backup.timer [Unit] Description=Daily restic backup [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true RandomizedDelaySec=900 [Install] WantedBy=timers.targetEnable it, then check the next run and the logs of the last one:
chmod 700 /usr/local/bin/restic-backup.sh systemctl daemon-reload systemctl enable --now restic-backup.timer systemctl list-timers restic-backup.timer journalctl -u restic-backup.service --since todayIf you prefer
cron, a0 3 * * *line that loads the environment file and calls the same script does the job, without the catch-up provided byPersistent=.Automate the restore test
A backup that has never been restored remains a hypothesis. This second script restores only the sentinel file from the latest snapshot and compares it with the original: if last night's backup failed or the repository no longer returns anything,
cmpfails and the service enters thefailedstate. The test targets the most recent snapshot: if you add--stdin-from-commanddumps to the backup script, place them before the file backup, which must remain the last one.#!/bin/sh # /usr/local/bin/restic-restore-test.sh set -eu rm -rf /tmp/restic-test-restore restic restore latest --target /tmp/restic-test-restore --include /opt/restic-sentinel.txt cmp /tmp/restic-test-restore/opt/restic-sentinel.txt /opt/restic-sentinel.txtSchedule it weekly, after the nightly backup:
# /etc/systemd/system/restic-restore-test.service [Service] Type=oneshot EnvironmentFile=/root/.restic-env ExecStart=/usr/local/bin/restic-restore-test.sh # /etc/systemd/system/restic-restore-test.timer [Timer] OnCalendar=Sun 05:00:00 Persistent=true [Install] WantedBy=timers.targetEnable the timer with
systemctl enable --now restic-restore-test.timer, and wire thefailedstate of both services into your monitoring. Once a quarter, also restore a real directory, database included, on a test machine.
Restic secrets: an isolated file, never hard-coded in the unit
Never put RESTIC_PASSWORD or the S3 keys in an Environment= line of a systemd unit, in a versioned script or in a Dockerfile: variables declared in the unit can be read with systemctl show. The /root/.restic-env file with chmod 600, loaded by EnvironmentFile=, keeps secrets out of the unit definition. To go further, systemd's LoadCredential= directive exposes a secret in a directory reserved for the service ($CREDENTIALS_DIRECTORY): Restic then reads its password through RESTIC_PASSWORD_FILE.
Troubleshooting: common errors
restic init or the first backup fails on the bucket. First check that the variables are loaded (echo $RESTIC_REPOSITORY), then the endpoint: on B2 it must match your account's region, and the AWS endpoint (s3.amazonaws.com) is a frequent mistake. A B2 Application Key restricted to a single bucket also needs the listAllBucketNames permission to work with S3 tools, and the account's master key does not work with the S3 API. On R2, the token needs read and write permissions on the bucket's objects.
The timer does not fire. Three commands are enough: systemctl status restic-backup.timer (active or not), systemctl list-timers --all | grep restic (next run), journalctl -u restic-backup.service --since today (log of the last run). To validate an OnCalendar expression, systemd-analyze calendar '*-*-* 03:00:00' must print the next trigger date.
The repository stays locked. An interrupted backup (reboot, network drop) can leave a lock behind: restic list locks shows it, and restic unlock removes stale locks. Make sure beforehand that no other Restic operation is running.
restic check --read-data takes too long. On a repository of several tens of GB, a full read takes hours and weighs on the egress allowance. Switch to --read-data-subset=10% or a 1/4 slice every week, and keep the full read for a monthly maintenance window.
Recovery plan: how long to restore everything?
The duration of a full restore depends on repository size, bandwidth between the VPS and the bucket, and the provider's egress policy. On R2, egress is free: nothing forces you to spread a restore out. On B2, it is free up to three times the average volume stored over the month; beyond that, a full restore of a large repository is billed.
Do not guess this time, measure it. restic stats gives the repository size; then restore a meaningful directory (restic restore latest --target /tmp/test --include /etc) and time the operation to extrapolate. Two practices shorten recovery: back up only what matters (no caches, logs, /tmp or abandoned Docker volumes), and write down in your procedure the order in which services come back, databases first, then the applications that depend on them.
Protect your backups against ransomware by enabling "append-only" mode on the backend: deploy a remote rest-server with the --append-only option, or use an S3 bucket with an IAM policy that forbids deletions. That way, even if your VPS is compromised and the attacker obtains the credentials, they will be unable to erase or encrypt your existing snapshots. Combine this with running restic forget --prune from a separate machine that does hold deletion rights.