Tutorial

Security & Monitoring

Automate Your VPS Backups with Restic

A backup that is neither tested nor encrypted is worthless on the day of an incident. Restic combines incremental snapshots, deduplication and end-to-end encryption in a single binary. On your VPS, it becomes the automated safety net for your data, pushed to remote object storage at low cost.

11 min read8 steps
Contents· Why automate your backups with Restic1/10
  1. 01Why automate your backups with Restic
  2. 02What Restic brings you
  3. 03The 3-2-1 rule: why a provider snapshot is not enough
  4. 04The three copies of a 3-2-1 strategy on a VPS
  5. 05Prerequisites for this deployment
  6. 06Backblaze B2 or Cloudflare R2: choosing the S3 backend
  7. 07Setting up automated Restic backups
  8. 08Restic secrets: an isolated file, never hard-coded in the unit
  9. 09Troubleshooting: common errors
  10. 10Recovery plan: how long to restore everything?

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

CriterionBackblaze B2Cloudflare R2
Free storageThe first 10 GB10 GB-month per month (Standard class)
Data egressFree up to 3 times the average monthly stored volume, and with no cap to partner CDNs (Cloudflare, Fastly)Free, no cap
S3 endpointSpecific to the account's region, for example `s3.us-west-004.backblazeb2.com``<account-id>.r2.cloudflarestorage.com`, region `auto`
Access keysA dedicated Application Key: the master key is rejected by the S3 APIAn R2 API token with object read and write permissions
Watch out forFrequent full restores can exceed the free egress allowanceRegular restore tests add no egress cost

Setting up automated Restic backups

  1. 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 plain KEY=value lines, without export: that is the format read by systemd's EnvironmentFile= 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-bucket

    Restrict the file's permissions, then load it into your session:

    chmod 600 /root/.restic-env
    set -a; . /root/.restic-env; set +a
  2. Initialize the encrypted repository

    restic init

    This 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.

  3. Run a first backup

    Back up your critical directories, then your databases. For a dump, prefer --stdin-from-command to a pg_dump | restic backup --stdin pipe: 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 snapshots
  4. Define a retention policy

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

    Seven 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 --prune pass is essential: forget alone removes snapshots from the list without freeing space, and the storage bill keeps growing.

  5. 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-data

    restic check validates the repository structure (snapshots, trees, indexes, packs) without reading the content of the data files: it is fast and runs after every forget --prune. --read-data downloads and verifies every pack, which takes time and consumes egress bandwidth. In between, --read-data-subset checks a random share (10%, or a size such as 5G) or a fixed slice (1/5 to 5/5, covering everything in five runs). A partial check every week and a full one once a month is a sound compromise.

  6. 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 check

    The 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.sh
  7. Schedule it with a systemd timer

    The timer triggers the service every night. Persistent=true catches up on a missed run if the VPS was off at the scheduled time, and RandomizedDelaySec=900 spreads 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.target

    Enable 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 today

    If you prefer cron, a 0 3 * * * line that loads the environment file and calls the same script does the job, without the catch-up provided by Persistent=.

  8. 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, cmp fails and the service enters the failed state. The test targets the most recent snapshot: if you add --stdin-from-command dumps 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.txt

    Schedule 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.target

    Enable the timer with systemctl enable --now restic-restore-test.timer, and wire the failed state 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.

Secure your data from day one

On a ServOrbit Cloud VPS with a preconfigured template, you install Restic in a few commands and schedule encrypted backups to the object storage of your choice.