Use a VPS as your off-site backup target
A snapshot at your provider is not off-site. Put backups on a VPS you control with Proxmox Backup Server or restic, and price the retention first.
What an off-site backup target actually is
An off-site backup target is a second machine that holds a copy of your data and fails independently of the original, and a VPS at another provider is the cheapest one most readers can get. Three shapes are realistic: Proxmox Backup Server running on the VPS, a restic repository reached over SSH or S3, or an rsync mirror that the backup host pulls. Which one fits depends on what you restore and how fast you need it back. Who is allowed to delete the copy decides the rest.
Off-site means a different failure domain. That means a different provider, and an account that shares no login with the one running your server. A second server in another region of the same provider survives a fire in one building. It does not survive a compromised control panel login, because one account controls both copies.
A snapshot at your provider is not that second copy. It sits behind the same panel password, so whoever gets that password deletes the server and its snapshots in one session. Snapshot services also bill per gigabyte per month at rates well above plain disk, which makes ninety days of them expensive. The difference between VPS snapshots and backups is worth reading before you rely on either.
Which of the three shapes fits you
- Proxmox Backup Server (PBS): the source is Proxmox VE (virtual environment) and the thing you restore is a whole virtual machine. It backs up at the disk-image level, and its verify jobs re-read the data sitting on the target.
- A restic repository: the source is one or several Linux hosts and the thing you restore is a directory or a database dump. It encrypts on the client, and it speaks SSH and S3, plus its own REST protocol.
- rsync over SSH, pulled by the backup host: you want the files on the target as ordinary files, readable with
lsandcat, with no client software needed to get them back.
If you cannot decide, run restic. It encrypts before anything leaves the machine, and it needs nothing on the target except an SSH account and disk. Setting up restic backups on a VPS covers the client side in more depth, and restic and BorgBackup side by side covers the choice if you already run Borg.
Sizing the target: what a month of retention costs
Deduplication is why the numbers are smaller than people expect. restic and PBS both split files into variable-size chunks and hash each chunk. Each unique chunk is stored once. The second backup of a 500 GB dataset does not add another 500 GB. It adds the chunks that changed.
So repository size tracks the age of your oldest snapshot, not the number of snapshots. Take 500 GB of data and 5 GB of new unique data per day. The repository then holds the 500 GB base, plus roughly 5 GB for every day back to the oldest snapshot the policy keeps.
The data behind this chart
[
{
"label": "7 daily",
"repo_size_gb": 535,
"usd_at_10_per_tb": 5.35
},
{
"label": "7 daily, 4 weekly",
"repo_size_gb": 640,
"usd_at_10_per_tb": 6.4
},
{
"label": "7 daily, 4 weekly, 6 monthly",
"repo_size_gb": "1,400",
"usd_at_10_per_tb": 14.0
},
{
"label": "7 daily, 4 weekly, 12 monthly",
"repo_size_gb": "2,325",
"usd_at_10_per_tb": 23.25
}
]The dollar column prices that repository at 10 US dollars per TB per month. It is a placeholder for the arithmetic, not a quote from any provider, so substitute the real per-TB price of the plan you are considering. A week of dailies holds about 535 GB. A full year of history holds 2,325 GB, which is $23.25 a month against $5.35 for the week. History is cheap. The base copy is what you are paying for.
Deduplication does nothing for data that arrives already compressed or encrypted. A gzipped database dump changes completely on every run, so each dump lands as new chunks and the repository grows by a full dump every night. Write the dump uncompressed and let the backup tool compress it, since restic has supported compressed repositories since 0.14 and version 0.19 added the fastest and better zstd modes. Photo and video libraries dedupe poorly for the same reason, so size those from their real growth rate instead of from the rows above.
You are buying idle disk rather than CPU here, which is exactly the case where a storage VPS beats a regular VPS.
Why bandwidth and restore time decide the plan
Disk is the cheap part. The first upload and the eventual restore are the expensive parts. 500 GB is 4 trillion bits, so dividing by the link speed gives the floor on how long a full restore can take.
The data behind this chart
[
{
"label": "40 Mbit/s home upload",
"elapsed_h": 27.8
},
{
"label": "100 Mbit/s",
"elapsed_h": 11.1
},
{
"label": "500 Mbit/s",
"elapsed_h": 2.2
},
{
"label": "1 Gbit/s VPS port",
"elapsed_h": 1.1
}
]Those are line-rate figures with no protocol overhead, so read them as a best case. At 100 Mbit/s a full restore needs 11.1 hours before anyone touches the data. From a home upload of 40 Mbit/s it needs 27.8 hours. On a 1 Gbit/s port the same restore takes 1.1 hours. Many small files run slower than the arithmetic, because per-file overhead dominates once files are under a few hundred kilobytes.
Two things follow. If your recovery time objective (RTO), the outage you can tolerate, is four hours, a 500 GB restore over a 100 Mbit/s link has already missed it, and cheaper disk does not help. And most VPS plans meter outbound transfer, so one full restore spends 0.5 TB of the backup host's monthly allowance. Check that allowance, and check what the provider does when you cross it, before you need the data.
The first backup is the whole dataset and it is the slowest run you will ever do. Start it on a Friday, and rate-limit it so it does not saturate the source's uplink: restic takes --limit-upload in KiB per second, rsync takes --bwlimit.
Shape 1: Proxmox Backup Server as a remote datastore
PBS fits when the source is Proxmox VE and the unit of restore is a virtual machine. A VPS cannot boot the Proxmox ISO, so install PBS on top of Debian. Version 4.2 is current as of August 2026 and is built on Debian 13 (trixie).
wget https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg \
-O /usr/share/keyrings/proxmox-archive-keyring.gpg
sha256sum /usr/share/keyrings/proxmox-archive-keyring.gpgCompare that checksum against the value published on the Proxmox package repositories page. An apt repository is only as trustworthy as the key you verified. Then write /etc/apt/sources.list.d/proxmox.sources:
Types: deb
URIs: http://download.proxmox.com/debian/pbs
Suites: trixie
Components: pbs-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpgsudo apt update && sudo apt install -y proxmox-backup-server
sudo proxmox-backup-manager datastore create offsite /mnt/datastore/offsiteGive the datastore its own filesystem or its own volume. A full datastore stops backups, and a datastore that shares the root filesystem takes the whole server down when it fills.
Next, create the account the source will use, and hand it a token instead of a password.
sudo proxmox-backup-manager user create backup@pbs
sudo proxmox-backup-manager user generate-token backup@pbs pve1
sudo proxmox-backup-manager acl update /datastore/offsite DatastoreBackup \
--auth-id 'backup@pbs!pve1'The token secret prints once and cannot be read back, so save it when it appears. The role matters as much as the token. DatastoreBackup can create and restore its own backups, and it does not carry the Datastore.Prune privilege, so that token cannot delete a snapshot it already wrote.
Retention on PBS has two halves, and the second one is the half people skip. Prune removes snapshots. Garbage collection removes the chunks that no surviving snapshot references. Free space appears after garbage collection, not after prune.
proxmox-backup-client prune host/web1 \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-run
sudo proxmox-backup-manager garbage-collection start offsite
sudo proxmox-backup-manager verify offsiteDrop --dry-run once the list of snapshots it plans to remove looks right. Garbage collection runs in two phases: it updates the access time of every chunk that is still referenced, then deletes chunks whose access time is older than the cutoff, which is 24 hours and 5 minutes before the run started. That grace period exists so a chunk being written by a backup in flight is not deleted underneath it. Schedule prune daily and garbage collection weekly on the datastore, and add a verify job so the target re-reads its own chunks and reports corruption on disk before a restore does.
If the source is itself a PBS instance, the off-site box can pull instead of being pushed to.
sudo proxmox-backup-manager remote create home1 \
--host pbs.home.example --userid sync@pam --password 'SECRET' \
--fingerprint '64:d3:ff:3a:50:38:53:5a:9b:f7:50:ab:fe'
sudo proxmox-backup-manager sync-job create home1-offsite \
--remote home1 --remote-store main --store offsite --schedule 'Wed 02:30'Run that sync job on the VPS, in the default pull direction. The VPS reaches into the home datastore, which means the home box holds no credential that can touch the off-site copy.
Shape 2: a restic repository over SSH or S3
Debian and Ubuntu both package restic, and both lag behind upstream. Version 0.19.1 is current as of August 2026. Install the official binary on the source host.
curl -LO https://github.com/restic/restic/releases/download/v0.19.1/restic_0.19.1_linux_amd64.bz2
bunzip2 restic_0.19.1_linux_amd64.bz2
sudo install -m 755 restic_0.19.1_linux_amd64 /usr/local/bin/restic
restic versionrestic version prints the version and the Go compiler it was built with. Later upgrades are sudo restic self-update, which works on official binaries and not on a copy installed from apt.
On the backup VPS, make an account that owns nothing else, then copy the source host's public key into /home/resticsrv/.ssh/authorized_keys.
sudo adduser --disabled-password --gecos '' resticsrv
sudo install -d -m 700 -o resticsrv -g resticsrv /srv/resticInitialise the repository from the source, over SFTP.
sudo sh -c 'umask 077; head -c 32 /dev/urandom | base64 > /root/.restic-password'
export RESTIC_REPOSITORY='sftp:resticsrv@backup.example.net:/srv/restic/web1'
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /etc /srv /var/backups --exclude-cachesStore that password somewhere that is neither this server nor the backup target. Lose it and the repository is unreadable, with no recovery path at all. That is the deal client-side encryption makes with you.
Retention is one command, and its second half is the part that frees disk.
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic check --read-data-subset=10%forget deletes snapshots. prune deletes the pack files that only those snapshots referenced, and --prune runs it automatically when something was actually removed. Without it the repository never shrinks. restic check verifies repository structure, and --read-data-subset=10% re-reads and re-hashes a tenth of the pack files, which catches corruption on the target without the cost of reading everything. The other form, --read-data-subset=1/10, checks one fixed tenth, so incrementing that first number each week covers the whole repository over ten weeks.
If a run is killed, the next one stops with repository is already locked exclusively by PID. Confirm no backup is running, then clear it with restic unlock.
For object storage the repository string becomes s3:https://s3.example.net/web1, with credentials in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Everything else is identical, which is how restic talks to a self-hosted MinIO object store running on the same VPS.
Shape 3: rsync over SSH with a pull-only key
The security property of this shape is direction. The backup VPS connects to the source and reads. The source holds no key and no route to the backup host, so a compromise of the source cannot reach the backups at all.
Generate a key pair on the backup VPS, then install the public half on the source with a forced command.
command="rrsync -ro /srv",restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA offsite-pullrrsync ships inside the rsync package at /usr/bin/rrsync on Debian 13 and Ubuntu 24.04. -ro allows reading only and implies -no-del, so this key cannot write to the source or delete anything on it. restrict switches off the SSH features that are not needed here, including port forwarding and the pty, so the key cannot be used for an interactive login. Paths are then relative to the directory you named, so the remote path / means /srv on the source.
The pull keeps history with hardlinks. Unchanged files in the new tree are hardlinks to the previous tree, so they cost a directory entry instead of a second copy.
DEST=/srv/mirror/web1
TODAY=$(date +%F)
LAST=$(ls -1d "$DEST"/2* 2>/dev/null | tail -1)
LINK=""
if [ -n "$LAST" ]; then LINK="--link-dest=$LAST"; fi
rsync -aH --numeric-ids $LINK -e 'ssh -i /root/.ssh/pull_ed25519' \
pull@web1.example.net:/ "$DEST/.$TODAY.partial/"
mv "$DEST/.$TODAY.partial" "$DEST/$TODAY"The rename at the end is what makes a dated directory trustworthy: the name appears only after rsync exited 0, so an interrupted transfer never looks like a finished snapshot. Expire old trees with one line, keeping thirty.
ls -1d /srv/mirror/web1/2* | sort | head -n -30 | xargs -r rm -rfBe honest about what this shape costs. Hardlinks deduplicate whole files only, so changing one byte inside a 4 GB disk image copies all 4 GB, where restic and PBS would store a few changed chunks. The target also holds your files in plaintext, so anyone with root on the backup VPS can read them.
Client-side encryption, so the target never sees plaintext
Treat the backup VPS as a machine you do not fully control. It has a provider, and that provider has staff and failed disks that leave the building.
restic encrypts every chunk on the source before sending it, so the repository is ciphertext plus metadata about sizes and timing. PBS makes encryption opt-in: create a key, then pass it on every backup.
proxmox-backup-client key create /root/pbs-encryption.key
proxmox-backup-client backup root.pxar:/ --keyfile /root/pbs-encryption.key
proxmox-backup-client key paperkey --output-format text > qrkey.txtPrint the paper key and store it somewhere physical. The Proxmox documentation is blunt about the stakes: without their key, backed up files are inaccessible. Keep the key off the backup target, because a key stored next to the ciphertext protects nobody.
rsync mirrors have no equivalent. The files land as files. If the data is sensitive, either accept that the target can read it, or use one of the other two shapes.
Stop a compromised source from wiping its own backups
An attacker who takes the source looks for the backups next, and the credential that uploads them is sitting right there on the machine. If that credential can also delete, they use it.
PBS answers this with roles. A token holding only DatastoreBackup can write new snapshots and restore its own, and it cannot prune, because deleting a snapshot needs the separate Datastore.Prune privilege. Run retention from the PBS side and the source never holds a credential that removes anything.
restic over SFTP has no such split, since the SSH key that writes to the repository can also delete from it. The fix is the REST backend. Run rest-server on the backup VPS with --append-only, which allows creation of new backups but prevents deletion and modification of existing ones, and point the client at rest:https://backup.example.net:8000/web1 using RESTIC_REST_USERNAME and RESTIC_REST_PASSWORD. A restic forget --prune from the source then fails, which is the intended result, so retention runs from a second machine with its own credential. The restic manual also recommends --keep-within instead of count-based policies on append-only repositories, because an attacker who floods the repository with junk snapshots would otherwise push your real ones out of a --keep-last window.
rsync solves the same problem structurally by pulling, since the source holds no credential for the target.
One rule covers all three shapes: the credential that can delete backups lives on a machine that is not the one being backed up.
Put the restore drill on the calendar
A backup you have never restored is a hypothesis. Book an hour every quarter and test it.
restic snapshots
restic restore latest --target /var/tmp/restore-test --include /etc/nginx
diff -r /etc/nginx /var/tmp/restore-test/etc/nginxdiff -r printing nothing means the restored tree matches the live one. On PBS the same drill is proxmox-backup-client restore host/web1/2026-08-13T02:30:00Z root.pxar /var/tmp/restore-test/, plus a scheduled verify job that re-reads chunks on the target and reports checksum failures.
The drill has to prove more than the bytes being intact.
- Restore from a third machine, not from the source, because the source is exactly what you are assuming is gone. That means the repository password or the PBS key has to be reachable without it.
- Time the restore and write the number down, then compare it with the RTO you claimed. The chart above gives the transfer floor. The real number also includes decrypting and writing to disk, plus the time spent working out which snapshot you wanted.
- Restore something with state, such as a database dump you then load into a scratch instance. A tar file that extracts is not proof that the application starts.
The cheapest disk in the world is worth nothing until you have restored from it once.
FAQ
Is a snapshot at my VPS provider an off-site backup?
No. A provider snapshot lives in the same account, behind the same panel login, and on the same invoice as the server it copies. Whoever obtains that login can delete the server and every snapshot of it in one session. Snapshots are useful for a fast rollback before a risky upgrade, and they are not a second location. An off-site copy lives under a different account, ideally at a different provider, with credentials the source machine does not hold.
How much disk do I need for a month of retained backups?
Size it from the age of your oldest snapshot rather than the number of snapshots. A deduplicating tool stores each unique chunk once, so the repository is roughly the size of the source plus the new unique data per day multiplied by the days you keep. For 500 GB of data changing by 5 GB a day, a week of dailies is about 535 GB and a full year of history is 2,325 GB. Buy headroom on top, because a full disk fails the next backup, and restic's prune needs free space to repack pack files before it can give space back.
Can a compromised server delete its own off-site backups?
Yes, unless you designed against it. With a plain SSH or SFTP repository, the key that writes can also delete. Give the source a credential that cannot remove data: a PBS API token holding only the DatastoreBackup role, which lacks the Datastore.Prune privilege, or restic against rest-server started with --append-only, which refuses deletion and modification of existing backups. A pull design goes further, because the source then holds no credential for the backup host at all. Run retention from the side that is not the source.
Should I run Proxmox Backup Server or restic on the backup VPS?
Match the tool to the unit you restore. If the source is Proxmox VE and you want a whole virtual machine back, run PBS, because it backs up at the disk-image level and restores a VM in one step. If the source is a Linux host and you want files and database dumps back, run restic, which needs only an SSH account on the target and encrypts before sending. Running both is normal: PBS for the hypervisor, restic for the servers that are not on it.
How long does a restore from a VPS backup take?
Divide the data size by the link speed to get the floor, then add time for decryption and writing. 500 GB over a 100 Mbit/s link is 11.1 hours at line rate, and the same restore over a 1 Gbit/s port is 1.1 hours. Many small files run slower than that arithmetic because of per-file overhead. Time one real restore and use that measured number, because it is the only one your recovery plan can rely on.