Off-Site VPS Backups Over SSH With restic

Published on

Back up your VPS to another machine with restic over SFTP: SSH key, repository password, include list, systemd timer, retention, checks and restores.

A VPS panel with a disk usage bar at 0.96 of 120 GB and a bandwidth meter with IN and OUT bars

Your database dumps and app archives sit in /var/backups on the same VPS they are meant to protect. If that disk fails, the server is deleted or someone with root wipes it, the backups go too. restic copies them to another machine over SSH, encrypted and deduplicated, keeps as much history as you ask for and restores one file or everything. This guide sets it up with an SFTP repository, a stored password, an include list and a systemd timer, on Ubuntu 24.04 and Debian 12.

Quick answer: Install restic on the VPS, give root an SSH key, and on the other machine create a user whose key can only use SFTP. Create the repository with restic -r sftp:backupbox:/srv/restic/vps1 init --repository-version 2 and keep its password in a root-only file and in your password manager, because without it the backups can't be opened. Back up a list of folders (settings, home folders, apps and database dumps) with restic backup --files-from, run it nightly from a systemd timer followed by restic forget --group-by host --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune and restic check, and test restic restore every month.

Why a Copy on the Same Server Isn't Enough

A backup on the VPS's own disk shares every risk with the data: a failed disk, a cancelled or deleted server, an rm -rf in the wrong folder, an attacker who deletes both. A provider snapshot often lives on the same storage system too, as our snapshot guide explains. What survives is a copy on another machine, ideally in another building: a server at home, a second VPS elsewhere or a storage service that offers SFTP.

restic suits this job. The other machine only needs an SSH server with SFTP, because restic starts ssh HOST -s sftp itself (sftp.go). It encrypts everything with AES-256 and authenticates it with Poly1305-AES before upload, so that machine never sees your files (restic design). After the first run, it only stores the parts of files that changed.

Step 1: Install restic on the VPS

sudo apt update
sudo apt install restic
restic version

Ubuntu 24.04 ships restic 0.16.4 and Debian 12 ships 0.14.0, and every command in this guide works on both. The newest release, 0.19.1 (5 July 2026), is available as an official binary through the installation docs. A Space-Node VPS is KVM with full root access, so you can install restic and add systemd units as shown.

Step 2: Create a Key and a Backup User

Create a key for root on the VPS without a passphrase. The restic docs put it plainly: automatic backups are not possible if the server prompts for credentials.

sudo install -d -m 700 /root/.ssh
sudo ssh-keygen -t ed25519 -N '' -C restic@vps1 -f /root/.ssh/restic_ed25519
sudo cat /root/.ssh/restic_ed25519.pub

On the backup machine (Ubuntu or Debian here), add a user for this VPS. Its home folder and .ssh belong to root, so the user can't change its own keys, and it owns only the repository folder. Paste the .pub line from above in place of the example key:

sudo useradd -M -d /srv/restic -s /usr/sbin/nologin vpsbackup
sudo install -d -m 755 /srv/restic /srv/restic/.ssh
sudo install -d -m 700 -o vpsbackup /srv/restic/vps1
echo 'restrict,command="internal-sftp" ssh-ed25519 AAAA...your-key... restic@vps1' | sudo tee /srv/restic/.ssh/authorized_keys

restrict turns off port, agent and X11 forwarding and terminal allocation for this key (sshd(8)). The forced internal-sftp command makes sshd run its built-in SFTP server and answer any other request with This service allows sftp connections only. (session.c). useradd without -p locks the password, so the key is the only way to log in. sshd accepts key folders owned by root or by the user, as long as group and others can't write to them (misc.c).

Step 3: Connect Once and Create the Repository

Back on the VPS, add the backup machine to root's SSH config. Create the file as root, with your backup machine's address:

# /root/.ssh/config
Host backupbox
    HostName 203.0.113.20
    User vpsbackup
    IdentityFile /root/.ssh/restic_ed25519
    IdentitiesOnly yes
    ServerAliveInterval 60
    ServerAliveCountMax 240

The last two lines come from the restic docs: SFTP servers close connections that receive no data, which can happen while restic works through a lot of unchanged files. Now connect once, so root's known_hosts learns the backup machine's host key:

sudo sftp backupbox

ssh asks Are you sure you want to continue connecting (yes/no/[fingerprint])?. Compare the fingerprint with the one ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub prints on the backup machine (for an ED25519 key), type yes, then bye. Don't skip this: the timer runs without a terminal, so ssh can't ask then and stops with Host key verification failed.

Next, the repository password:

sudo install -d -m 700 /etc/restic
sudo sh -c 'head -c 32 /dev/urandom | base64 > /etc/restic/password'
sudo cat /etc/restic/password

Put that password in your password manager now. restic init warns that if you lose it, "your data is irrecoverably lost", and the copy in /etc/restic dies with the VPS. Then create /etc/restic/env:

# /etc/restic/env
RESTIC_REPOSITORY=sftp:backupbox:/srv/restic/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic

restic keeps a local cache so it doesn't have to load metadata from the repository every time (manual). It looks for that cache in $XDG_CACHE_HOME or under $HOME, and systemd sets neither for a service without User= (systemd.exec), so the timer would run without one. RESTIC_CACHE_DIR fixes that. Load the file in a root shell and create the repository:

sudo -i
set -a; . /etc/restic/env; set +a
restic init --repository-version 2

set -a exports every variable the file sets, so each restic command in this shell finds the repository and password; the rest of this guide runs in it. Repository version 2 adds compression. It's the default from restic 0.15.0 on, but Debian 12's 0.14.0 still creates version 1 without it.

Step 4: Choose What to Back Up

Create the two lists:

# /etc/restic/include.txt
/etc
/home
/root
/srv
/opt
/var/www
/usr/local
/var/backups
/var/spool/cron/crontabs
# /etc/restic/exclude.txt
/root/.cache
/home/*/.cache
node_modules

That covers the server's settings, the home folders, your apps and sites (Debian's nginx serves /var/www/html by default), your own scripts in /usr/local, database dumps and crontabs. restic skips a path that doesn't exist with a warning such as pattern "/srv" does not match any files, skipping, and ignores lines that start with #. In exclude patterns, * doesn't match across a /, and a name without a slash, like node_modules, matches that folder anywhere (backup docs).

Leave out the data folders of running databases, such as /var/lib/postgresql and /var/lib/mysql, and Docker's /var/lib/docker, which holds its volumes. PostgreSQL's manual says the server "must be shut down in order to get a usable backup" of its files (File System Level Backup). Back up dumps instead: our PostgreSQL, MariaDB and MySQL and Docker volumes guides write them to /var/backups every night.

Now run the first backup and list the result:

restic backup --files-from /etc/restic/include.txt --exclude-file /etc/restic/exclude.txt --exclude-caches
restic snapshots

--exclude-caches also skips every folder that contains a CACHEDIR.TAG file.

Step 5: Decide How Long to Keep Snapshots

restic forget removes snapshots by a policy, and --prune then deletes the data that only those snapshots used (forget docs). Preview the policy with --dry-run:

restic forget --group-by host --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --dry-run

That keeps the newest snapshot of each of the last 7 days, 4 weeks and 12 months that have snapshots. By default, forget groups snapshots by host and paths. After you edit include.txt, the older snapshots would form a group of their own, and the policy would keep that group's newest daily, weekly and monthly snapshots indefinitely. --group-by host keeps one history per server. Prune locks the repository while it runs, and the docs advise running restic check afterwards, so the timer does both after the backup.

Step 6: Run It Every Night With a systemd Timer

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

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
Nice=19
ExecStart=restic backup --files-from /etc/restic/include.txt --exclude-file /etc/restic/exclude.txt --exclude-caches
ExecStart=restic forget --group-by host --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
ExecStart=restic check
# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Nightly restic backup

[Timer]
OnCalendar=*-*-* 04:30:00
Persistent=true

[Install]
WantedBy=timers.target

Still in the root shell, test the service once, read its log, then enable the timer:

systemctl daemon-reload
systemctl start restic-backup.service
journalctl -u restic-backup.service -n 30 --no-pager
systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer

With Type=oneshot, systemd runs the ExecStart= lines in order and stops at the first one that fails, so a failed backup never leads to a prune. A oneshot service also has no start timeout, so a big upload isn't cut off (systemd.service). systemd finds a bare restic in /usr/local/bin or /usr/bin, so an official binary works too. Nice=19 gives the backup the lowest CPU priority. Persistent=true catches up on a run missed while the server was off (systemd.timer), and network-online.target makes that catch-up wait for the network (systemd.special). Schedule it after your dump jobs: the ones in our PostgreSQL, MariaDB and Docker guides run at 03:30.

Step 7: Check and Restore

Once a month, read part of the stored data back and restore a folder next to the original:

restic check --read-data-subset=10%
restic ls latest /etc/apt
restic restore latest --target /tmp/restore-test --include /etc/apt
diff -r /etc/apt /tmp/restore-test/etc/apt && echo SAME
rm -rf /tmp/restore-test

The nightly restic check verifies the repository's structure, and --read-data-subset=10% also downloads and verifies a random tenth of the stored data (check docs). Snapshots keep full paths, so --include /etc/apt lands in /tmp/restore-test/etc/apt (restore docs), and diff prints nothing when the copies match. For a single file, restic dump latest /etc/hostname prints it.

After losing the VPS. On the new server, install restic, create a new key as in Step 2 and swap it into the backup machine's authorized_keys, then recreate /root/.ssh/config and /etc/restic with the password from your password manager. Run sudo sftp backupbox once, load the env file in a root shell as in Step 3, and restore into a folder:

restic snapshots
restic restore latest --target /srv/restore

Copy back what you need from /srv/restore rather than restoring over /, so the new server keeps its own /etc/fstab and network settings, then load the dumps as the PostgreSQL, MariaDB and Docker guides show. If several servers share the repository, add --host with the old server's name.

restic, borg or rsync

BorgBackup also deduplicates, compresses and encrypts on the client, but it works best with borg installed on the backup machine too, because the VPS starts borg serve there. In return you can lock the key to one path with command="borg serve --restrict-to-path /path/to/repo",restrict. Borg 1.4.5 (18 July 2026) is the stable release, and 2.0 is still in beta. Plain rsync copies files as they are, so they are readable on the other machine, and it keeps no history unless you build snapshots with --link-dest. It is the simplest way to pull dumps from another machine, as our PostgreSQL guide does, so the VPS holds no login to the copies.

If It Still Fails

  • subprocess ssh: Host key verification failed. Root has never connected to that host, or the host key changed. Run sudo sftp backupbox and check the fingerprint as in Step 3.
  • Permission denied from ssh. The backup machine rejected the key. Check that authorized_keys holds the whole .pub line and that IdentityFile points at the private key; sudo ssh -v backupbox shows which key ssh offers. On the backup machine, sudo journalctl -u ssh shows lines such as Authentication refused: bad ownership or modes for directory when a folder is writable by others.
  • Fatal: wrong password or no key found. The password file doesn't match the repository. Compare it with the copy in your password manager.
  • Is there a repository at the following location? The path in RESTIC_REPOSITORY differs from the folder you created, or restic init never ran.
  • repository is already locked. A run that was stopped, for example by a reboot, left its lock behind: systemd stops services with SIGTERM (systemd.kill), and restic before 0.17.0 then exits without cleaning up (changelog). Check that systemctl status restic-backup.service shows no run in progress, then run restic unlock. It removes only stale locks: older than 30 minutes, or left by a process that no longer runs on this host.
  • The service fails with exit status 3. The snapshot was saved, but some files could not be read, and the journal lists them. Fix their permissions or exclude them. Until then, the forget and check lines don't run.

FAQ

Can someone who gets root on my VPS delete these backups? Yes. The VPS holds a key with write access to the repository, and restic's threat model says it "is not designed to protect against attackers deleting files at the storage location". To close that gap, run restic's rest-server with --append-only on the backup machine, which "allows creation of new backups but prevents deletion and modification of existing backups". Then move forget and prune off the VPS to a separate, trusted machine and use --keep-within, as the forget docs advise for append-only repositories.

Can the backup machine read my files? No. Everything except some informational metadata in the key files is encrypted and authenticated before it leaves the VPS, and only the repository password opens it. That's why the password belongs in your password manager, not only on the server.

Can I back up a running database with restic? Not its data folder: back up a dump, as in Step 4. Run the restic timer after the dump jobs, so each snapshot holds that night's dump.

How much space does the repository need? The first snapshot stores your data once, compressed in a version 2 repository. Every later snapshot only adds new and changed chunks. restic stats --mode raw-data shows how much the repository holds, so check it after a few weeks and leave room for a year of monthly snapshots.

Are backups included with a Space-Node VPS? Not in the plan price; you can arrange them through a support ticket. A restic repository you run yourself stays useful either way: you choose where it lives, and only your password opens it.

Sources

All opened on 30 October 2026: