Encrypted Off-Site VPS Backups with restic or Borg (and Restores That Actually Work)
Set up encrypted, deduplicated off-site VPS backups with restic or Borg: repo layout, database dumps, retention, key handling and real restore drills.
Most VPS "backup strategies" are one of two things: a provider snapshot toggle nobody has ever restored from, or a cron job running tar to a second directory on the same disk. Neither survives the failure modes that actually destroy data — account loss, a compromised root shell, or a filesystem that silently corrupts weeks before you notice.
This is a practical guide to building encrypted, deduplicated, off-site backups from a Linux VPS using restic or Borg, including the parts people skip: database consistency, repository key handling, append-only protection against ransomware, retention math, and restore drills you can actually finish.
The short answer
Run a client-side encrypted, deduplicated backup tool (restic or BorgBackup) from the VPS to storage that is outside your hosting account, on a schedule, with:
- a repository password stored somewhere other than the server being backed up
- explicit include/exclude lists instead of "back up everything"
- proper database dumps rather than copies of live data files
- retention via
forget/pruneso the repository doesn't grow forever - an append-only or object-locked target so a compromised server can't erase history
- a documented restore that you have performed at least once end to end
Everything below is the detail behind those six lines.
Build the threat model before the script
A backup is only "good" relative to what it must survive. Write down which of these you're covering:
Hardware and node failure. The classic case. Any off-server copy handles it.
Human error. DROP TABLE, a bad migration, rm -rf in the wrong directory. Needs point-in-time history, not just a mirror. A mirrored copy that syncs deletions is not a backup.
Silent corruption. Bit rot or a bad application write that propagates before you notice. Needs multiple retained snapshots plus integrity verification (restic check, borg check).
Compromise. An attacker with root will look for backup credentials on the box and delete the repository. This is the failure mode that provider snapshots and "same-account object storage with full write keys" do not survive. Needs append-only or immutable storage.
Account loss. Payment failure, dispute, suspension, or a provider disappearing. If your backups live inside the same account as the server, they vanish with it. This is the single strongest argument for off-site.
Provider snapshots are useful — they're fast, they capture the whole disk, and they're excellent for "I'm about to upgrade the kernel." They are not a substitute, because they typically live on the same infrastructure and inside the same account as the instance.
restic vs Borg: how to choose
Both encrypt client-side, both deduplicate with content-defined chunking, both are mature and widely used. The practical differences:
restic speaks S3-compatible object storage, Backblaze B2, SFTP, REST, and anything rclone supports. Multiple hosts can write to one repository safely. Concurrency is good, and the single static binary is trivial to deploy. Downsides: pruning is heavier, and memory/cache usage grows with repository size.
Borg is generally faster and lighter on CPU and RAM for a single host and has excellent compression options (zstd). It requires a filesystem or SSH target with borg serve on the remote side — you cannot point it directly at object storage. One repository should be written by one client.
Rule of thumb: if your off-site target is object storage, use restic. If you control a remote box or a storage server with SSH access, Borg is a great fit. Do not run both against the same data; pick one and learn it properly.
restic to object storage: a working setup
Create a credentials file readable only by root, at /etc/restic/env:
RESTIC_REPOSITORY=s3:https://s3.example-storage.net/mybucket/web01
RESTIC_PASSWORD_FILE=/etc/restic/repo-pass
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxx
chmod 600 /etc/restic/env /etc/restic/repo-pass
set -a; . /etc/restic/env; set +a
restic init
Now define what to back up. Explicit lists beat backing up / and excluding the noise:
/etc/restic/includes.txt
/etc
/home
/srv
/root
/var/backups
/var/lib/postgresql/dumps
/var/www
/etc/restic/excludes.txt
**/.cache
**/node_modules
**/*.sock
/var/lib/docker
/var/log/journal
*.tmp
Skipping /var/lib/docker is deliberate: container layers are rebuildable from your Dockerfiles and compose files. Back up the definitions and the volumes, not the overlay filesystem. Also capture your package selection so a rebuild is fast:
dpkg --get-selections > /var/backups/dpkg-selections.txt
The backup command:
restic backup \
--files-from /etc/restic/includes.txt \
--exclude-file /etc/restic/excludes.txt \
--tag daily --one-file-system
Retention and integrity, run after the backup:
restic forget --keep-daily 7 --keep-weekly 5 --keep-monthly 6 --keep-yearly 2 --prune
restic check --read-data-subset=5%
forget removes snapshots; --prune reclaims the unreferenced data. check --read-data-subset actually downloads and verifies a slice of the packs — a metadata-only check will happily pass on a repository with corrupt data blobs. Rotate the subset percentage so you cover the whole repository over a month.
Schedule it with systemd, not bare cron
A systemd service plus timer gives you logging, failure status, and I/O priority control:
/etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
Nice=10
IOSchedulingClass=idle
ExecStart=/usr/local/bin/restic-backup.sh
/etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup
[Timer]
OnCalendar=*-*-* 03:20:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
IOSchedulingClass=idle and Nice=10 matter on shared virtualization: the first backup of a large dataset can saturate disk I/O and make a busy application unresponsive. If backups feel disproportionately slow or the whole box stalls while they run, it's worth checking whether the underlying node is the problem — the techniques in measuring CPU steal time and disk latency will tell you whether you're throttled or just doing a lot of I/O.
Then monitor for silence. A backup job that stops running is the most common real-world failure. Push a heartbeat to a monitoring endpoint on success, and alert when the heartbeat is missing — not just when a command exits non-zero.
Databases need dumps, not file copies
Copying /var/lib/mysql or /var/lib/postgresql from a running server gives you a torn, likely unusable snapshot. Dump first, then back up the dump — or stream it straight into the repository.
PostgreSQL:
sudo -u postgres pg_dump -Fc mydb -f /var/lib/postgresql/dumps/mydb.dump
# or stream directly:
sudo -u postgres pg_dump -Fc mydb | restic backup --stdin --stdin-filename mydb.dump --tag db
MySQL/MariaDB:
mariadb-dump --single-transaction --quick --routines --events --all-databases \
| gzip | restic backup --stdin --stdin-filename all-databases.sql.gz --tag db
--single-transaction gives a consistent view on InnoDB without locking writes. For MyISAM tables it does not, and you'll need --lock-tables and a short write pause.
Two things people forget: dumps are far more compressible and deduplicate less well than you'd hope (compressed dumps change entirely between runs, so store them uncompressed if you want restic's dedup to help), and a dump proves nothing until you've restored it into a scratch database.
Key handling: the mistake that voids the whole plan
restic and Borg encrypt on the client. That's exactly what you want — the storage provider holds ciphertext and cannot read your files. It also means the repository password is the data. Lose it, and the backups are cryptographic noise.
Practical rules:
- Store the repository password in an offline password manager and, ideally, on paper somewhere physical. Not only on the server it protects.
- Keep the storage access keys separate from the repository password. Someone who steals the bucket keys should get unreadable blobs.
- For Borg,
borg key exportyour repokey and store it separately;--encryption=repokey-blake2keeps the key in the repo, which is convenient but means the passphrase is the only barrier. - Rotate storage keys when a server is decommissioned or suspected compromised.
Note that client-side backup encryption and full-disk encryption solve different problems. LUKS on the VPS protects data at rest against certain offline-access scenarios but does nothing for a backup sitting in a bucket — and it does not protect a running server at all. If you're weighing both, the honest breakdown of what LUKS actually protects on a remote server is worth reading before you assume disk encryption covers your backups.
Protect against a compromised server deleting the backups
If the credentials on your VPS can delete objects, an attacker with root can wipe your history. Options, roughly in order of strength:
- Object Lock / immutability on the storage side, with a retention window longer than your detection time. Even valid credentials can't delete locked objects.
- Append-only repository access. With
rest-server --append-onlyfor restic, orborg serve --append-onlyrestricted via an SSHcommand=forced-command inauthorized_keys, the client can write new data but not remove it. - Pull-based backups, where a separate trusted host connects in and reads the data. The production server holds no backup credentials at all.
- Two targets with different credentials — e.g. a primary object store written daily, plus a second copy that only a separate machine can touch.
If you use append-only, remember that pruning must then happen from a privileged context, on a schedule, from somewhere else. That's a feature, not a bug.
Test the restore, and time it
An untested backup is a hypothesis. Do a real drill at least quarterly:
restic snapshots
restic restore latest --target /tmp/restore --include /etc/nginx
That's the cheap version. The valuable version is a full rebuild: provision a clean VPS, install packages from your saved selections, restore configuration and data, import the database dump, point a hosts-file entry at the new IP, and confirm the application works. Write down every step and how long it took — that number is your real RTO, and it is almost always longer than people guess.
Spinning up a throwaway instance for a restore drill and destroying it afterwards is the ideal use of a short-lived VPS; with crypto-billed, instantly deployed servers like IronBalkans VPS instances, a rehearsal costs very little. Just make sure the drill target is not the same box or account you're protecting against losing.
What backups leak, and what they don't
Client-side encryption hides file contents, filenames, and directory structure from the storage provider. It does not hide:
- Timing and size patterns. The provider sees when you upload and how much. Repository growth reveals activity.
- Your source IP. Every backup run connects from the server, tying it to the storage account.
- The account relationship. The email, payment method, and API keys used for the storage bucket link your identity to the data location just as much as the hosting account does.
If backup metadata is part of your threat model, treat the storage account with the same care as the server account: separate identity, separate payment path. The same considerations that apply to paying for a VPS with Monero without deanonymizing yourself apply to whoever holds your encrypted archives.
Also decide the jurisdiction question deliberately. Off-site should mean a different provider and ideally a different legal jurisdiction than your primary host, so that one legal or administrative event cannot take out both.
Common mistakes
- Backups on the same disk or same account. Convenient, and useless for the failure modes that matter.
- No retention policy. The repository grows until the storage bill or the prune operation becomes unmanageable.
- Only metadata verification.
checkwithout--read-data-subsetwon't catch corrupt packs. - Repository password stored only on the backed-up server. The most common way people permanently lose recoverable data.
- Backing up container overlay filesystems instead of volumes and compose files — huge, churny, and rebuildable anyway.
- No alert on missing runs. Failures are loud; silence is not.
- Never restoring. If you have not restored, you do not have backups.
FAQ
How often should backups run?
Match your tolerance for lost work. Daily is fine for a config-heavy server with low write volume. Databases with real transactions want hourly dumps or WAL archiving with pg_basebackup plus continuous archiving.
Do I still need provider snapshots? They're a useful convenience layer for quick rollbacks before risky changes. Keep them if they're cheap, but count them as zero in your disaster planning.
Is rsync to a second server enough?
It's a mirror, not history. Add --link-dest rotation or a snapshotting filesystem if you insist, but restic and Borg give you deduplication, encryption, and integrity checking for less effort.
How much storage will I need? Roughly the compressed, deduplicated size of your current data plus the delta across your retention window. Text-heavy configuration and code dedupe extremely well; media and already-compressed archives do not.
Takeaway
A working backup system is four decisions: a tool that encrypts client-side and keeps history, a target outside your hosting account that a compromised server can't erase, dumps for anything transactional, and a restore you've actually completed and timed. Get those right and everything else — schedules, compression settings, retention numbers — is tuning. Skip the restore drill and you have a very tidy archive of unverified guesses.
