A reliable VPS backup is a copy of your data that lives off the server, is encrypted, runs on a schedule and has been restored at least once. Restic gives you the first three in one binary and makes the fourth a short drill: it encrypts and deduplicates each snapshot before upload, stores it in any S3-compatible bucket, and restores one file or the whole data set.
This guide sets that up on Ubuntu 24.04 LTS with restic 0.19.1, released on July 5, 2026. You install and verify the binary, create the repository, choose what to back up and run it nightly with a systemd timer. Then you set retention, get an alert when a run fails, and rehearse a restore on a throwaway server. Commands come from the restic, systemd, Backblaze, Cloudflare and PostgreSQL documentation, checked on October 3, 2026.
Key takeaways
- A VPS backup only counts if it lives off the server: at HourlyVPS a deleted server's disk cannot be recovered, and snapshots may be deleted with the server.
- Install restic 0.19.1 from the checksum- and signature-verified official binary; Ubuntu 24.04's 0.16.4 package lacks --stdin-from-command, and only 0.19 and later fail a run when a listed path is missing.
- Keep the repository password and bucket key in a password manager as well as in root-only files, because restic's docs warn that a lost password means irrecoverably lost data.
- A systemd timer runs backup, forget --prune (7 daily, 4 weekly, 6 monthly) and a 5% data check every night, and a heartbeat ping turns any failure into an alert.
- Rehearse a full restore on a throwaway hourly server using only your password manager, time it, then delete the server; billing stops when it is deleted.
Snapshots are not backups. At HourlyVPS, deleting a server deletes its disk at once, and neither you nor support can recover it. Our Terms call snapshots a convenience tool that may be deleted with the server. Keep at least one copy with another company.
Why a VPS backup must live off the server
Most data loss on a VPS comes from ordinary events: a disk or file system fault, a bad upgrade, a command run in the wrong directory, a stolen SSH key, an unpaid invoice, or deleting the wrong server. A copy on the same disk shares every one of those risks. A snapshot on the same platform shares most of them.
| Where the copy lives | Deleted file or bad upgrade | Server deleted or suspended | Problem with the hosting account | Attacker with root on the server |
|---|---|---|---|---|
| Archive or dump on the same disk | Yes, if not overwritten | No | No | No |
| Provider snapshot | Yes (rolls back the whole server) | Not guaranteed: may be deleted with the server | No | Usually yes, unless they also hold your portal login |
| restic repository with another company | Yes (any snapshot, any file) | Yes | Yes | Only with versioning, Object Lock or append-only mode (see below) |
The 3-2-1 rule that CISA recommends puts the same idea in numbers: 3 copies of important files, on 2 different media, with 1 copy offsite. For a VPS that means the live data, a restic repository at an object-storage company, and ideally a second repository or a copy on a machine you own.
Why restic for a VPS?
- Encrypted before it leaves the server. All repository data is encrypted with AES-256 in counter mode and authenticated with Poly1305-AES, according to the restic design document. The storage company only sees encrypted pack files.
- Incremental, but every snapshot is complete. Restic splits files into chunks and uploads only chunks the repository does not already hold. Any snapshot still restores in full.
- Compressed. Repository format version 2, the current default, adds compression and needs restic 0.14.0 or newer (restic docs).
- Speaks S3 natively. No mount, no FUSE and no extra daemon: one static binary talks to the bucket over HTTPS.
BorgBackup is the closest alternative, and the Nextcloud AIO backup is built on it. Borg 1.4 reaches remote repositories over SSH, ideally with Borg installed on the other end, and its quickstart lists no S3 backend. If your offsite target is a bucket, restic is the shorter path. Plain rsync copies files, but on its own it keeps no encrypted history of earlier versions.
Step 1: Install restic 0.19 on Ubuntu 24.04
Ubuntu 24.04 ships restic 0.16.4 in its universe archive, Debian 12 ships 0.14.0 and Debian 13 ships 0.18.0. All of them can back up, but this guide relies on two newer features:
--stdin-from-command, which streams a database dump straight into the repository. It was added in restic 0.17.0.- Since 0.19.0,
backupexits with code 3 when any source path does not exist, so a typo in your path list raises an alert. Older versions exited with 0 for a missing top-level path.
The restic installation docs point to the official binaries when a distribution package is outdated. On the server, as your sudo user (connect over SSH first), install the tools and download the release and its checksum file. On an ARM server, replace amd64 with arm64 in every file name.
sudo apt update && sudo apt install -y bzip2 curl gpg
cd /tmp
curl -fLO https://github.com/restic/restic/releases/download/v0.19.1/restic_0.19.1_linux_amd64.bz2
curl -fLO https://github.com/restic/restic/releases/download/v0.19.1/SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
The last line must print restic_0.19.1_linux_amd64.bz2: OK. To check that the checksum file itself is genuine, verify its signature against the key the restic docs publish:
curl -fLO https://github.com/restic/restic/releases/download/v0.19.1/SHA256SUMS.asc
curl -fsSL https://restic.net/gpg-key-alex.asc | gpg --import
gpg --verify SHA256SUMS.asc SHA256SUMS
Look for Good signature and the primary key fingerprint CF8F 18F2 8445 7597 3F79 D4E1 91A6 868B D3F7 A907, the one printed in the installation docs. The warning that the key “is not certified with a trusted signature” only means you have not signed the key yourself. Then install the binary:
bunzip2 restic_0.19.1_linux_amd64.bz2
sudo install -m 0755 restic_0.19.1_linux_amd64 /usr/local/bin/restic
restic version
Later releases install with sudo restic self-update, which checks the GPG signature on its own. This works for the official binary only. If you would rather use the apt package, sudo apt install restic works too; just leave out the --stdin-from-command lines below.
Step 2: Choose S3-compatible storage and create a key
Any S3-compatible service works. Pick a company other than your VPS host, so that one account problem cannot take the server and its backups together. Then compare four things: the storage price, download (egress) fees for restores and checks, any minimum storage duration (restic deletes data when it prunes), and the region.
| Provider | Storage price | Free tier | Downloads | Minimums | 50 GB repository per month | 200 GB repository per month |
|---|---|---|---|---|---|---|
| Backblaze B2 | $6.95 per TB-month | First 10 GB | Free up to 3× your average stored data, then $0.01/GB | None | about $0.28 | about $1.32 |
| Cloudflare R2 | $0.015 per GB-month (Standard) | 10 GB-month, 1 million Class A and 10 million Class B requests a month | Free | None on Standard storage | about $0.60 | about $2.85 |
| Wasabi | $7.99 per TB-month | None | No egress fee, as long as monthly downloads stay at or below your stored volume | 1 TB minimum charge; 90-day minimum storage duration | $7.99 | $7.99 |
The repository size is measured after deduplication and compression, so it is often smaller than the raw data; restic stats --mode raw-data prints it after the first backup. Wasabi’s 90-day rule matters with restic. Every prune deletes pack files, and Wasabi bills each deleted object as if it were stored for its full 90 days.
Create the bucket and a key for this server only
- Create a private bucket for this server, for example
vps1-restic. B2 bucket names must be unique across all of B2. - Create a key that can reach only this bucket. At Backblaze, add an application key, choose this bucket under “Allow Access to Bucket(s)” and pick Read and Write (Backblaze docs). At Cloudflare, create an R2 API token with “Object Read & Write” scoped to this bucket (R2 docs). Both show the secret only once, so copy it into your password manager straight away.
- Note the endpoint and region. B2 endpoints have the form
https://s3.<region>.backblazeb2.com, for examples3.us-west-004.backblazeb2.comwith the regionus-west-004; the B2 web console lists your bucket’s endpoint. R2 useshttps://<ACCOUNT_ID>.r2.cloudflarestorage.com, and its S3 region is auto. - Decide how long deleted data stays. By default B2 keeps every version of every file. The restic docs recommend the lifecycle setting “Keep only the last version of the file”, so that space freed by prune is released. The compromised-server section explains when to keep prior versions for some days instead.
Step 3: Store the secrets and initialize the repository
Restic needs three secrets: the repository password, the access key ID and the secret key. Keep them in a root-only folder on the server and in your password manager. The copy on the server is gone with the server, and without the password the backup is useless. The restic docs put it plainly:
Losing your password means that your data is irrecoverably lost.
restic documentation, “Preparing a new repository”
Create the folder and a random 32-byte password, then print it once so you can copy it into your password manager:
sudo install -d -m 0700 /etc/restic
openssl rand -base64 32 | sudo tee /etc/restic/password > /dev/null
sudo chmod 0600 /etc/restic/password
sudo cat /etc/restic/password
Next, write the settings file. Open it with sudo nano /etc/restic/env and paste this Backblaze example with your own bucket, region and key:
RESTIC_REPOSITORY=s3:https://s3.us-west-004.backblazeb2.com/vps1-restic
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=your-key-id
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_DEFAULT_REGION=us-west-004
sudo chmod 0600 /etc/restic/env
For Cloudflare R2, use RESTIC_REPOSITORY=s3:https://<ACCOUNT_ID>.r2.cloudflarestorage.com/vps1-restic and AWS_DEFAULT_REGION=auto. Two details from the restic docs explain the format. First, restic expects path-style URLs (s3:https://endpoint/bucket), not the bucket name in the host name. Second, restic uses us-east-1 when no region is set, which is why the file names the bucket’s own region. Write each value without quotes, so that systemd and the shell read the file the same way.
The credentials are readable only by root, so open a root shell, load the file and create the repository:
sudo -i
set -a; . /etc/restic/env; set +a
restic init
The output ends with created restic repository … at s3:https://… and the same warning about the password. Keep this root shell open for the next two steps, and leave it with exit when you are done. To give a colleague, or a sealed envelope, a second way in, restic key add adds another password to the same repository.
Step 4: Decide what to back up on a VPS
Back up what you cannot rebuild: configuration, application data, home folders and database dumps. Skip what packages recreate, and never rely on copies of live database files.
| Path | Back it up? | Why |
|---|---|---|
/etc | Yes | System and app configuration: web server, systemd units, cron.d, SSH settings |
/home, /root | Yes, minus caches | Scripts, app checkouts, keys you rely on |
/srv, /opt, /var/ | Yes, where they exist | Websites, Compose projects and their bind-mounted data |
/usr/, /usr/ | Yes | Tools you installed by hand, including the backup script below |
/var/ | Yes | Per-user crontabs |
/var/ | Yes | Your database dumps and the package list the script writes |
/var/, /var/ | No: dump instead | Files copied while the database writes may not restore cleanly |
/var/ | No | Images are pulled again; back up bind mounts and database dumps instead |
/proc, /sys, /dev, /run, /tmp | No | Virtual or temporary file systems |
/usr, /var/ | No | Reinstalled from packages; keep the package list instead |
Put the paths in /etc/restic/include.txt (nano /etc/restic/include.txt in the root shell), one per line, and add /var/www only if it exists:
/etc
/home
/root
/srv
/opt
/usr/local/bin
/usr/local/sbin
/var/spool/cron/crontabs
/var/backups
The script reads this file with --files-from-verbatim, which takes each line as a literal path and does not skip comment lines, so keep the file to bare paths. Together with the 0.19 exit-code change, a path that is missing or misspelled fails the run instead of quietly shrinking your backups. This command prints an error for every listed path that does not exist:
ls -d $(cat /etc/restic/include.txt)
Exclusions go in /etc/restic/exclude.txt. A pattern without a leading slash, such as node_modules, matches that name at any depth (pattern rules):
/home/*/.cache
/root/.cache
node_modules
__pycache__
*.tmp
The /etc line also backs up /etc/restic, keys included. That copy is encrypted with the repository password, so it adds no risk, and it is no substitute for the copy in your password manager.
Databases: dump them, or stream the dump into restic
Option A: back up a dump folder. Keep your existing dump job and let restic pick up its output. Examples are the nightly PostgreSQL dump into /var/backups/postgresql and the WordPress database export. Schedule the dump to finish before the backup starts, and add its folder to include.txt if it is not under a listed path.
Option B: no dump on disk. With --stdin-from-command, restic runs the dump command and stores its output as one file. If the command exits non-zero, restic cancels the backup, creates no snapshot and exits with code 1. For every PostgreSQL database, including roles:
restic backup --tag db --stdin-filename postgres-all.sql --stdin-from-command -- runuser -u postgres -- pg_dumpall
Do not pipe a dump into restic backup --stdin instead. The restic docs warn that a pipe hides the dump’s failure and can store an empty backup. For containers, dump the database with the client inside the container, and back up the Compose folder and bind mounts. Our notes on dumping databases cover MySQL, MariaDB and Docker volumes, and the Docker guide covers the host setup.
Step 5: Run the first backup and set retention
Still in the root shell with the settings loaded, save the list of manually installed packages (your rebuild recipe), then run the first backup:
apt-mark showmanual > /var/backups/apt-manual.txt
restic backup --files-from-verbatim /etc/restic/include.txt --exclude-file /etc/restic/exclude.txt --exclude-caches --tag files
The first run uploads everything. Later runs skip files whose size, timestamps and inode have not changed, and upload only new chunks of the rest, so a quiet server adds little per night. The first upload is also the largest traffic you will generate, so check your plan’s traffic allowance on the pricing page before you push hundreds of gigabytes. Then look at the result:
restic snapshots
restic stats --mode raw-data
Retention is a two-part job. forget removes snapshots according to a policy, and prune deletes the data that only those snapshots used; forget --prune does both (restic docs). Preview the policy first:
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-run
With one backup a night, this keeps the last 7 daily snapshots, the newest snapshot of each of the last 4 weeks and of each of the last 6 months. That is at most 17 snapshots, reaching back about half a year. Weeks run Monday to Sunday, because the calendar options use natural boundaries. Restic groups snapshots by host and paths before it applies the policy, so file snapshots and database snapshots each keep their own 7, 4 and 6.
Finally, check the repository. Plain restic check verifies the structure. --read-data-subset=5% also downloads a random 5% of the pack files and verifies their contents (restic docs). Nightly, that adds up to about 1.5 times the repository size in downloads per month: inside B2’s free 3× allowance, and R2 charges no egress. A random sample does not promise to reach every pack file; run a full restic check --read-data after each restore drill if you want that.
restic check --read-data-subset=5%
Step 6: Automate nightly backups with a systemd timer
Restic has no scheduler of its own, and its docs ask you to make sure two runs never overlap. A systemd timer handles both. According to the systemd.timer manual, a unit that is still active when the timer elapses is not started again.
The backup script
Create /usr/local/sbin/restic-backup with nano in the root shell:
#!/usr/bin/env bash
# Nightly restic backup, started by restic-backup.service
set -euo pipefail
apt-mark showmanual > /var/backups/apt-manual.txt
restic backup \
--files-from-verbatim /etc/restic/include.txt \
--exclude-file /etc/restic/exclude.txt \
--exclude-caches \
--tag files
# PostgreSQL: uncomment to stream a full dump into the repository
# restic backup --tag db --stdin-filename postgres-all.sql \
# --stdin-from-command -- runuser -u postgres -- pg_dumpall
restic forget --prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6
restic check --read-data-subset=5%
# Heartbeat: only reached when every step above succeeded
PUSH_URL=""
if [ -n "$PUSH_URL" ]; then
curl -fsS -m 10 -o /dev/null "$PUSH_URL"
fi
chmod 0700 /usr/local/sbin/restic-backup
set -e stops the script at the first command that fails. Exit code 3 counts as a failure, whether a file could not be read or a listed path is missing, and so does 11 for a locked repository. The service then fails and the heartbeat is never sent. The settings come from the unit’s environment, so the script holds no secrets apart from the heartbeat URL.
The service and the timer
Create /etc/systemd/system/restic-backup.service:
[Unit]
Description=Back up this server with restic
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
CacheDirectory=restic
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
ExecStart=/usr/local/sbin/restic-backup
And /etc/systemd/system/restic-backup.timer:
[Unit]
Description=Run restic-backup.service every night
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=15min
Persistent=true
[Install]
WantedBy=timers.target
EnvironmentFile=loads the bucket settings and secrets without writing them into the unit.CacheDirectory=resticcreates/var/cache/restic, which matchesRESTIC_CACHE_DIR. A system service running as root gets no$HOMEby default, according to the systemd.exec manual, so restic would otherwise have no obvious place for its cache.Nice=10and I/O priority 7 (the lowest in the best-effort class) let your application win when both want the CPU or disk.Persistent=truestarts a missed run as soon as the server is back, for example after it was powered off at 03:15.RandomizedDelaySec=spreads the start over 15 minutes.OnCalendaruses the server’s time zone;timedatectlshows it.
Leave the root shell, then run the service once by hand. A oneshot service blocks until it finishes. Read the log, then enable the timer:
exit
sudo systemctl daemon-reload
sudo systemctl start restic-backup.service
sudo journalctl -u restic-backup.service -n 40 --no-pager
sudo systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer
Our systemd timer guide explains OnCalendar expressions and how to debug a unit that will not start.
Get an alert when a backup does not run
The failure that hurts is a backup that stopped weeks ago without anyone noticing. Point the script’s PUSH_URL at a heartbeat monitor, such as an Uptime Kuma push monitor. Set the monitor’s interval a little longer than a day; 25 hours leaves room for the random delay. A failed run, a full disk or a dead server all end the same way: no ping, so you get an alert. Run the monitor somewhere other than the server it watches.
How to restore from a restic backup
Open a root shell and load /etc/restic/env as in Step 3. To find a file and bring back one copy without touching the live one:
restic snapshots --tag files
restic ls latest --tag files /etc/caddy
restic restore latest --tag files --target /tmp/restore --include /etc/caddy/Caddyfile
Restic recreates the full path under the target, so the file lands at /tmp/restore/etc/caddy/Caddyfile. Compare it, then copy it into place. Restoring straight over a live system is possible, but the restic docs advise taking a fresh backup first, because an interrupted restore can leave files half-written.
A dump stored with --stdin-from-command comes back with restic dump. Load it into a fresh PostgreSQL cluster as the PostgreSQL docs show for pg_dumpall output:
restic dump latest --tag db /postgres-all.sql | runuser -u postgres -- psql -X -d postgres
Rehearse a full restore on a throwaway server
A backup you have never restored is a hope, not a backup. Run one full drill after setup and another after any big change. The point is to prove that you can rebuild from your password manager alone, while the original server stays untouched.
- Deploy a fresh server with Ubuntu 24.04 and a disk larger than your repository’s restored data plus the operating system.
- Install restic as in Step 1.
- Recreate
/etc/restic/passwordand/etc/restic/envby typing or pasting from your password manager, not by copying them from the old server. - Run
restic snapshots, thenrestic restore latest --tag files --target /restore. - Check what matters: configs read correctly, the app starts from the restored folders, and the database dump loads (the PostgreSQL restore drill walks through that part).
- Note how long it all took. That is your real recovery time. Then delete the server.
| Plan | NVMe disk | Price per hour | A 2-hour drill |
|---|---|---|---|
| Quartz Q1 (1 GB RAM) | 25 GB | $0.01/hour | $0.02 |
| Quartz Q2 (2 GB RAM) | 50 GB | $0.02/hour | $0.04 |
| Quartz Q4 (4 GB RAM) | 80 GB | $0.03/hour | $0.06 |
| Quartz Q8 (8 GB RAM) | 160 GB | $0.05/hour | $0.10 |
What the drill costs. HourlyVPS bills every server by the hour: Billing is by the hour: every hour a server exists is charged at the plan’s hourly rate. The price tapes and cost tables on this site count every started hour as a full hour, so they show the most a duration can cost; the cost calculator charges a partial hour to the nearest cent, as the bill does. A stopped server is still billed, because its vCPU, memory, disk and IP addresses stay reserved for you; only deleting the server stops billing. So delete the drill server when you finish instead of stopping it. Each server you order is funded by an initial credit, which is prepaid and used for that server’s hours; the billing guide shows worked examples. The server you protect usually runs all month. It is billed the same way, by the hour, and never costs more than the monthly price in a billing period (one month from your order date).
If the drill turns into the real thing, because the old server is gone and the new one takes over, keep one history in the repository. Set RESTIC_HOST in /etc/restic/env to the old server’s host name, or pass --host. Restic picks the parent snapshot, and forget groups snapshots, by host and paths, so this lets the new server continue the old server’s history instead of starting a new one. Run through our checklist before you delete a VPS when the old server retires.
Keep backups safe if the VPS is compromised
Anyone who gets root on the server can read /etc/restic and use the same key to delete what is in the bucket. The key cannot simply lack delete rights: restic removes its own lock files when a command ends, and forget and prune need full read, write and delete access (restic docs). Encryption keeps the storage company from reading your data. It does not stop someone who holds your key from deleting it. Add these layers, in order of effort:
- One key per server and per bucket. A leaked key then exposes one repository, not your whole storage account.
- Keep deleted versions for a while. In B2, choose “Keep prior versions for this number of days” (say 30) instead of “Keep only the last version” (B2 lifecycle rules). Files that prune or an intruder deletes then stay recoverable for 30 days, and you pay to store them for that time. A key that may delete files can also delete old versions through the API, so this protects against mistakes and simple attacks, not a careful attacker.
- Object Lock, where your provider offers it, blocks deletion of locked object versions until their retention date, even for a key with delete rights. Plan it with your provider’s documentation before you turn it on.
- Append-only access. Restic’s rest-server has an append-only mode, and
rclone serve restic --append-onlycan sit in front of a bucket. The server can then add snapshots but not delete them. Runforgetandprunefrom a separate, well-secured machine, and use--keep-withinpolicies there, as the restic docs advise for append-only repositories. - A second repository.
restic copycopies snapshots to another repository, for example at a second provider or on a machine at home. Restic warns that copying downloads and uploads the full snapshots.
None of this replaces the basics on the server itself: SSH keys only, a firewall and automatic security updates. Our VPS security checklist covers those in 15 minutes.
Restic troubleshooting: exit codes and fixes
Restic’s exit codes tell you what kind of failure you have. The scripting docs list them and warn that any unknown code must be treated as a failure. Read the details with journalctl -u restic-backup.service.
| Exit code or symptom | What it means | Fix |
|---|---|---|
| 1, with an access, signature or endpoint error | The key, endpoint or region does not match the bucket | Check the path-style URL s3:, set AWS_ to the bucket’s region, and confirm the key covers this bucket with write access |
| 10 | Repository does not exist | Wrong RESTIC_, or restic init never ran for this bucket |
| 11 | Failed to lock the repository: another restic process holds the lock, or a crashed run left a stale one | Wait for the running job. restic unlock removes stale locks only. --retry-lock 10m makes a command wait instead of failing |
| 12 | Wrong password | Compare /etc/ with the copy in your password manager |
| 3 | backup could not read some files (an incomplete snapshot was still saved), a listed path does not exist (0.19 and later), or forget could not remove a snapshot | The journal names the paths. Fix permissions or include., then run the service again |
| 130 | Cancelled by SIGINT or SIGTERM, for example a reboot during the run | The restic docs say an interrupted backup or prune does not damage the repository, though you may need restic unlock. Start the service by hand or wait for the next run |
check reports errors | The repository is damaged | Disable the timer so that forget and prune cannot run, then follow the restic troubleshooting steps, including the repair command that check prints |
| The timer never fires | The timer is not enabled, or its calendar is wrong | systemctl enable --now restic-backup.; systemctl list-timers shows the next run |
| The app slows down during backups | Disk or CPU contention | Keep Nice= and the I/O priority, and move OnCalendar to your quietest hour |
| The storage bill grows after pruning | The provider keeps old versions, or bills a minimum storage duration | Check the bucket lifecycle setting; at Wasabi, deleted data is billed until it is 90 days old |
Put the next restore drill in your calendar now. Six months from now, the drill will tell you whether your backups still work.
Deploy this setup
Rehearse a full restic restore on a throwaway server
Quartz Q2 · 1 shared vCPU · 2 GB RAM · 50 GB NVMe · Istanbul
- Per hour$0.02/hourFor this job
- Per day (24 h)$0.48/day
- Monthly cap$10.00/month
Starts with a $5 initial credit, which goes into the server’s balance and pays for its hours.
Billed by the hour, never more than $10.00 per billing period. Delete the server and billing stops.



