SSD Nodes Learn
Guides Matt ConnorBy Matt Connor

Restic backups: get your VPS data off the box

Restic sends encrypted, deduplicated backups from your VPS to another server or object storage, on a nightly timer, with a restore drill that proves it.

Why a backup on the same server is not a backup

Restic is a free, open source backup tool that sends encrypted, deduplicated snapshots of your files to a repository somewhere else: a second VPS, a machine at home, or S3-compatible object storage. This guide sets it up on Ubuntu 24.04, from install to a repository over SFTP, a first backup, a nightly systemd timer, a retention policy, and the restore drill that proves the whole thing works. The destination has to be another machine, because a copy that lives on the same server dies with the server.

A backup/ directory on the box it backs up protects you from one thing only: deleting a file by accident. It does not survive a failed disk, because it was on that disk. It does not survive an attacker with root, because they delete the copies first. It does not survive the account mistake that removes the VPS itself. The world's least efficient datacenter jokes about a tarball named backup_final_v2_REAL sitting on the same array as the data, and the joke lands because so many of us have run exactly that. Off the box is the rule, and restic is the least painful way to follow it.

Restic in four ideas

Repository. The place restic writes to. It is a directory in restic's own format, full of encrypted blobs, and only restic can read it. You never edit it by hand; you talk to it through restic commands and the -r address.

Snapshot. One point-in-time picture of the files you backed up. Every backup run creates a snapshot, every snapshot can be restored on its own, and each behaves like a complete copy of your data at that moment.

Deduplication. Restic splits files into content-defined chunks and uploads only the chunks the repository has not seen. The first backup uploads everything; every run after that uploads roughly what changed. A nightly snapshot of 20 GB where 50 MB changed costs about 50 MB, which is why keeping dozens of snapshots is cheap.

Encryption by default. A restic repository is always encrypted (AES-256), and every command needs the repository password. The backup host or the storage provider only ever sees encrypted blobs. The hard consequence: lose the password and the data is gone, permanently and by design. Keep a copy of the password somewhere that is not this server. This matters enough that it comes up twice more below.

Install restic on Ubuntu 24.04

sudo apt update && sudo apt install -y restic
restic version

On Ubuntu 24.04 this installs restic 0.16.4, while the current upstream release is 0.19.1. The gap exists because an LTS (long term support) release freezes its package versions, and it does not matter here: 0.16.4 does everything in this guide. If you want the newest release for its speed improvements, download the official single-binary build from the restic project's GitHub releases page, unpack it with bunzip2, and install it to /usr/local/bin/restic; there is nothing else to a restic install.

Create the repository on another server over SFTP

You need a destination machine: a second small VPS is the usual answer, and any box with an SSH server and spare disk works. Restic speaks SFTP (file transfer over SSH), so the backup host needs nothing installed at all. In this guide the backup host is 10.0.0.12 with a user named restic. Do not name that user backup: Ubuntu and Debian ship a reserved system account called backup (uid 34, no login shell) on every install, so adduser backup fails and ssh backup@... lands in nologin.

The nightly job will run as root on the server being backed up, so root needs key login to the backup host. Create a dedicated key with no passphrase, because no human is present at 3am to type one, and copy it over:

sudo ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "" -C "web1-restic"
sudo ssh-copy-id -i /root/.ssh/id_ed25519.pub restic@10.0.0.12
sudo ssh restic@10.0.0.12 true && echo key login works

If keys are new to you, SSH key management basics explains the model, the permissions, and how to revoke a key later.

Next, the repository password. Generate a strong one into a root-only file:

openssl rand -base64 32 | sudo tee /root/.restic-password
sudo chmod 600 /root/.restic-password

Now copy that password into your password manager, before you go any further. If this VPS dies, the repository plus this password brings everything back; the repository without the password brings back nothing.

Initialise the repository:

sudo restic -r sftp:restic@10.0.0.12:/srv/restic/web1 --password-file /root/.restic-password init
created restic repository 9f3c2a1b0d at sftp:restic@10.0.0.12:/srv/restic/web1

The alternative destination is S3-compatible object storage, which is the right choice when you do not want to run a second machine. Any S3-compatible bucket works the same way; only the address and two credential variables change:

export AWS_ACCESS_KEY_ID=your-key-id
export AWS_SECRET_ACCESS_KEY=your-secret-key
sudo -E restic -r s3:https://s3.example.com/web1-backups --password-file /root/.restic-password init

Everything after init is identical for both destinations. The rest of this guide shows the SFTP address; substitute yours.

The first backup, with excludes

Back up the data you cannot reinstall, not the whole filesystem. The operating system comes back with a reinstall; your configuration and your data do not. For a typical VPS that means /etc, /home, and wherever your applications keep state, such as /srv or /var/www. Exclude caches, because they are large, they churn every day, and they rebuild themselves:

sudo restic -r sftp:restic@10.0.0.12:/srv/restic/web1 --password-file /root/.restic-password backup /etc /home /srv --exclude '/home/*/.cache'
Files:        4181 new,     0 changed,     0 unmodified
Added to the repository: 731.204 MiB (312.418 MiB stored)
snapshot 5b8a3f2c saved

The first run uploads everything, so it takes a while. Run the same command again and it finishes in seconds, reporting a few files changed and a few MiB added, because deduplication only uploads new chunks. List what you have:

sudo restic -r sftp:restic@10.0.0.12:/srv/restic/web1 --password-file /root/.restic-password snapshots

Each snapshot shows an ID, a time, and the paths it contains. Those IDs are what you restore from.

Nightly runs with a systemd timer

Typing the repository address on every command gets old, and a backup you run by hand stops happening within a month. Both problems end with one script and one timer. The script sets the two environment variables restic reads, RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE, so every command inside it stays short:

sudo nano /usr/local/bin/restic-backup.sh
#!/usr/bin/env bash
set -euo pipefail
export RESTIC_REPOSITORY='sftp:restic@10.0.0.12:/srv/restic/web1'
export RESTIC_PASSWORD_FILE=/root/.restic-password

restic backup /etc /home /srv --exclude '/home/*/.cache'
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic check
sudo chmod 700 /usr/local/bin/restic-backup.sh

The forget and check lines are explained in the next two sections. Now the schedule: a oneshot service that runs the script, and a timer that fires it at 03:00 every night. A timer beats a cron line here because the run logs to the journal, and Persistent=true runs a missed backup as soon as the server is back up after downtime.

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

[Service]
Type=oneshot
ExecStart=/usr/local/bin/restic-backup.sh
# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Run the nightly restic backup

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target

Enable the timer, then run the service once by hand and watch it work:

sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
sudo systemctl start restic-backup.service
sudo journalctl -u restic-backup.service -f

systemctl list-timers shows when the next run fires. You can also generate the pair of unit files instead of typing them:

ToolGenerate the backup service and timer

The full pattern behind these two files, including calendar syntax and the hardening directives a service can carry, is in running a program as a systemd service on a VPS.

A backup is a rumor until you restore it

Treat that sentence as a commandment. A backup job that runs green every night proves only that a job ran; it does not prove your data comes back. Two checks close the gap.

First, restic check, which the script already runs nightly. It verifies the repository structure and the index, so silent corruption on the backup host gets caught the next night instead of on restore day. Once a month, run the deeper version, which downloads and cryptographically verifies a random tenth of the actual data:

sudo -i
export RESTIC_REPOSITORY='sftp:restic@10.0.0.12:/srv/restic/web1'
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic check --read-data-subset=10%

Because the subset is random each time, monthly runs work their way through the whole repository without ever paying for a full download.

Second, the restore drill. Still in the root shell from above, restore one real directory from the latest snapshot to a scratch location and compare it against the live files:

restic restore latest --target /srv/restore-drill --include /etc/ssh
diff -r /etc/ssh /srv/restore-drill/etc/ssh

diff printing nothing means every byte came back identical, which is the only evidence that counts. Delete /srv/restore-drill afterwards. Do this drill monthly, and once or twice a year do the full version: restore the entire latest snapshot onto a scratch VPS and check that your application actually starts from it. The day you need this to work under pressure, you want it to be a routine you have already done.

Retention: forget plus prune

Without a policy, snapshots accumulate forever and the repository only grows. The script's forget line applies a policy every night: --keep-daily 7 keeps one snapshot per day for the last seven days, --keep-weekly 4 one per week for four weeks, and --keep-monthly 6 one per month for six months. Everything not protected by a rule is forgotten.

forget on its own only removes the snapshot records; the data chunks stay in the repository until something deletes them. That is what --prune does: it finds chunks no remaining snapshot references and deletes them, which is when disk space actually returns. Prune does real repository work, so on a large repository some people run forget nightly and --prune weekly; at typical VPS sizes, nightly is fine.

Databases: dump first, then back up the dump

Restic copies files as it reads them, and a database writes to its files continuously. A live database file captured mid-write restores as a corrupt database, because the copy mixes pages from before and after a write. The fix is standard: have the database engine produce a consistent export to a file, then let restic back up that file.

For PostgreSQL, add a dump line to the top of restic-backup.sh, before the restic backup command, and include the dump directory in the backup paths:

mkdir -p /var/backups/db
sudo -u postgres pg_dump myapp | gzip > /var/backups/db/myapp.sql.gz

mysqldump fills the same role for MariaDB and MySQL. For a worked example of the whole pattern, the Nextcloud backup section turns on maintenance mode, dumps Postgres, and copies the files as one consistent set, exactly the set restic should carry off the box each night. SQLite is the same idea with a smaller hammer: the Vaultwarden guide stops the container for a few seconds to take a cold copy of db.sqlite3, and that archive is what restic ships off the server.

FAQ

Are restic backups encrypted?

Yes, always. Every restic repository is encrypted with AES-256, there is no unencrypted mode, and every command requires the repository password. The machine or provider storing the repository only ever holds encrypted blobs, so a compromised backup host does not expose your files. The trade is absolute: without the password the data cannot be recovered by anyone, so store a copy of it away from the server.

Does restic do incremental backups?

Every restic snapshot behaves like a full backup, while costing incremental storage. Restic splits files into chunks and uploads only chunks the repository has not already stored, so a nightly run transfers roughly what changed that day. Unlike traditional incremental schemes, there is no chain to replay: any snapshot restores directly and deleting an old snapshot never breaks a newer one.

How do I restore files from a restic backup?

Run restic snapshots to find the snapshot ID, then restic restore <id> --target /some/empty/dir to restore it, adding --include /path to restore only part of it. latest works in place of an ID. Restic recreates the original directory structure under the target, so restoring /etc/ssh lands in /some/empty/dir/etc/ssh. Practice this before you need it, because an untested backup is a rumor.

How often should I run restic backup?

Nightly is the sensible floor for a server, and deduplication makes it cheap: each run uploads only the chunks that changed since the last one. Data that changes fast, or that would hurt to lose even a day of, can run every few hours with the same timer pattern. Frequency is the easy half; also run restic check regularly and a restore drill monthly, because schedule without verification is false comfort.

What happens if I lose my restic repository password?

The backups are unrecoverable. Restic's encryption has no back door and no reset, so the password is as important as the backups themselves. Keep a copy in your password manager and anywhere else durable that is not the backed-up server. While you still have access, restic key add can register a second password for the same repository, which gives you a spare.