The passphrase was only on the dead machine
The repository is intact, encrypted, and permanently unreadable. Store the passphrase — and for Borg the exported key — in a password manager and on paper, before the first snapshot.

Six steps from an unprotected server to a verified encrypted repository on a second machine in a second jurisdiction — restic and Borg compared, database dumps that restore clean, a systemd timer, and an append-only repository that a compromised server cannot erase. The second box costs $3.90 a month. Tested on Debian 12.
Inventory
What cannot be rebuilt
Install
restic or Borg
Destination
A second bastion
Initialise
Repository + first run
Automate
Dumps, timer, retention
Drill
Restore, and time it
Almost every argument about backups goes wrong in the first minute, because the two people talking have different failures in mind. One is picturing a dead drive. The other is picturing a Tuesday morning where the account is gone. They need different answers, and a setup that only survives the first one feels safe right up until it is tested by the second.
There are four failure modes worth designing against, and they are not variations on a theme — each one defeats a different defence.
Hardware. The NVMe fails, the host node dies, the array loses two members at once. This is the failure everybody plans for, and it is the rarest one on modern virtualised infrastructure. Redundant storage handles it. Redundant storage handles nothing else, which is why RAID is not a backup and never was: it copies every write faithfully, including the write that says "delete everything".
Human. A migration script run against production. A DROP TABLE in the wrong terminal. An rsync with the arguments reversed. This is by a wide margin the most common cause of real data loss, and the only defence against it is history — a copy from before the mistake, kept somewhere the mistake did not reach. Time is the ingredient: if the only copy is a mirror that is thirty seconds behind, it already contains the mistake.
Malicious. Someone gets root, or ransomware gets in through an unpatched application. The first thing modern ransomware does is look for backup configuration — credentials, mounted shares, cloud keys — and destroy what it finds, precisely because a working restore turns a catastrophe into an afternoon. A backup credential that can delete is a backup credential the attacker inherits. This is the failure the append-only chapter of this guide exists for.
Custodial. Nothing broke and nothing was attacked; you simply lost access. An account suspended over an automated fraud signal, a card that failed while you were travelling, a dispute, a compliance review, an order served on the provider. The machine is intact and unreachable, and every snapshot inside that account is unreachable with it. This is the failure that no amount of redundancy inside one provider can answer, and the reason a serious backup lives under a different roof.
| Defence | Hardware | Human error | Ransomware | Lost account |
|---|---|---|---|---|
| RAID / redundant storage | Yes | No | No | No |
| Provider snapshot, same account | Yes | Partly | No | No |
| Offsite repository, writable key | Yes | Yes | No | Yes |
| Offsite repository, append-only, other jurisdiction | Yes | Yes | Yes | Yes |
The last row is what the rest of this guide builds. It is not more expensive than the row above it — the append-only setting is free and the second jurisdiction costs the same $3.90 as the first.
The 3-2-1 rule, restated for a single VPS. Three copies of the data, on two different kinds of storage, one of them offsite. The classic formulation comes from an era of tape and office servers, and the spirit survives the translation better than the letter. For one self-hosted box it reads: the live data on the VPS, a repository on a second machine somewhere else, and — for anything genuinely irreplaceable — a third copy you can hold, on a disk at home or in a drawer. The number that actually matters is not three. It is how many independent things have to go wrong before the data is gone. Two is the minimum worth having.
The category you want is called a deduplicating, encrypting snapshot tool. It splits every file into content-defined chunks, encrypts each chunk on the machine that owns the data, and stores it once no matter how many snapshots reference it. That gives you three properties at the same time: many restore points for barely more space than one, plaintext that never leaves the source, and a transfer that only moves what changed.
Two programs dominate this space and both are excellent. restic is a single static Go binary with a wide range of storage backends. BorgBackup is a Python program that speaks SSH to a copy of itself on the far end. Everything below is the honest difference between them.
| Property | restic | BorgBackup | rsync / rclone |
|---|---|---|---|
| Client-side encryption | Always on, not optional | Always on, not optional | Only via an rclone crypt remote |
| Deduplication | Content-defined chunks | Content-defined chunks | None, or hardlink trees |
| Snapshot history | Yes, with retention policies | Yes, with retention policies | A mirror of right now |
| Destination requirements | Nothing — SFTP, S3, REST, rclone | Borg installed on both ends | An SSH account or an API |
| Append-only enforcement | Via rest-server or object lock | Built in, over plain SSH | None |
| Good for | Almost everyone, most backends | An SSH box you own, append-only | Mirroring, not backup |
rsync is in the table because it is what most people reach for first, and because it is genuinely the wrong tool: a mirror faithfully reproduces a deletion, and a mirror of an encrypted-at-rest disk is not itself encrypted anywhere. It earns its place inside a backup as the thing that moves a finished archive, not as the archive.
This guide shows both restic and Borg for every command that differs, so you can follow it with either. Where a single choice has to be made — the worked example, the timer, the script — it uses restic, because the destination then needs nothing but an SSH account and that keeps the second machine boring.
Backing up the whole filesystem is a defensible choice and a wasteful one. Most of a server is a package manager away from being recreated, and every gigabyte of it you carry makes the repository slower to prune, more expensive to hold and slower to search when you are in a hurry. The useful exercise is the opposite: list what a fresh install would not give you back.
For a typical self-hosted VPS that list is short, and it always contains these five things:
Turn the list into two files on the source server. An include list keeps the intent visible; an exclude list keeps the noise out. Both are read by the backup command, and both belong in the backup themselves.
/etc/backup/include.txt
/etc
/opt
/root
/var/backups
/var/lib/docker/volumes
/home
/etc/backup/exclude.txt
# Caches and rebuildable artefacts
**/node_modules
**/.cache
**/*.tmp
/var/lib/docker/volumes/*/_data/cache
# Log noise — keep the config, drop the volume
/var/log/journal
# Never back up the mount point you restore into
/mnt/restore
Two notes on that Docker line, because it catches people. Named volumes live under /var/lib/docker/volumes and are what you want; bind mounts live wherever you put them, usually next to the Compose file, and are covered by /opt. Container images are not data — they come back with a pull, and a backup that carries them is carrying gigabytes it does not need.
Finally, measure the result before you build anything around it. The number tells you which repository tier to order and whether the first run will take four minutes or forty:
du -sh --exclude=/var/lib/docker/overlay2 /etc /opt /root /home /var/lib/docker/volumes
Install the tool on the machine that owns the data. On Debian and Ubuntu both are packaged, and for restic the distribution package is often a release or two behind — which matters, because repository features and performance work land in the point releases. Install the package, then let restic update itself in place:
apt update && apt install -y restic
restic self-update
restic version
For Borg, the distribution package is the right choice, because the version on the source and the version on the destination have to understand each other and a matched pair from the same distribution release is the least painful way to get that:
apt install -y borgbackup
borg --version
A word on major versions before you commit years of history to a repository: Borg's repository format changed between its major lines, and moving a repository across that boundary is a conversion, not an upgrade. Read the release notes of the version you are installing, and keep the destination on the same major line as the source. restic has been forward-compatible across its releases and adds features behind an explicit repository version, so the equivalent worry there is smaller.
Neither tool needs a daemon, an agent or an open port on the source. That is worth saying out loud: the backup system you are installing adds no listening service and no new attack surface to the machine it protects. Everything it does is outbound, on a schedule, over SSH.
The repository host has one job and almost no requirements. It does not need cores, because it never reads your data — it holds encrypted blobs it cannot open. It does not need memory, because the deduplication work happens on the source. It needs disk, a stable address, and to be somewhere the source machine's troubles do not follow it.
In the panel: Order → VPS → Sentinel, image Debian 12, and — the whole point — a different bastion from the one the source runs in. Helsinki working, Reykjavík holding. No email address is required to open the account and the invoice settles in Monero, Bitcoin, Lightning or any of the other supported assets, which means the second machine does not re-introduce the identity the first one was careful about.
| Repository target | Monthly | Storage | Working set it holds | Cost to restore |
|---|---|---|---|---|
| Sentinel | $3.90 | 120 GB NVMe | ~30 GB | Nothing — unmetered |
| Garrison | $7.90 | 240 GB NVMe | ~70 GB | Nothing — unmetered |
| Ravelin | $16.90 | 480 GB NVMe | ~150 GB | Nothing — unmetered |
| Bulwark | $32.90 | 960 GB NVMe | ~300 GB | Nothing — unmetered |
| S3-class object storage | ~$0.023/GB | Elastic | Any | Metered egress |
The working-set column assumes daily snapshots kept for a year with normal churn; a repository that mostly holds slowly-changing files goes much further, one full of large re-written binaries goes less far. The last row is there for scale, and for the detail people forget until the bad day: object storage charges for the download too, and the download is what you do in an emergency.
On the destination — an unprivileged user and a directory.
adduser --disabled-password --gecos "" backup
install -d -m 0700 -o backup -g backup /srv/restic/web01
The backup user has no password, no sudo and nothing to log into. It exists to own a directory. Give it its own directory per source machine if you back up several — one repository per source keeps a compromise on one box from touching the history of another.
On the source — a key used for nothing else.
ssh-keygen -t ed25519 -f /root/.ssh/id_backup -N "" -C "backup:web01"
cat /root/.ssh/id_backup.pub
Do not reuse your login key. This one lives unencrypted on disk because an unattended timer has to use it at three in the morning, which is exactly the kind of key you want scoped to a single destination and a single purpose.
Back on the destination — restrict what that key may do. Paste the public key into /home/backup/.ssh/authorized_keys with a prefix in front of it. For restic over SFTP:
restrict,from="198.51.100.7" ssh-ed25519 AAAAC3NzaC1lZDI1... backup:web01
The restrict option turns off port forwarding, agent forwarding, X11 and PTY allocation in one word, leaving the file transfer and nothing else. The from= clause pins the key to the source address, so a copy of it stolen from the server is useless anywhere else. For Borg, go one step further and force the command, which is where its append-only mode lives:
command="borg serve --append-only --restrict-to-path /srv/borg/web01",restrict ssh-ed25519 AAAAC3NzaC1lZDI1... borg:web01
That line is the whole ransomware defence in one place: whatever the source sends, the destination will only ever run borg serve, only inside that path, and only in a mode that appends. A root shell on the source cannot use this key to erase last month.
Name the destination once, on the source. Put it in /root/.ssh/config so every later command is short and the address lives in exactly one file:
Host rkv-repo
HostName 198.51.100.42
User backup
IdentityFile /root/.ssh/id_backup
IdentitiesOnly yes
Then connect once by hand — ssh rkv-repo — to accept the host key. An unattended timer will not answer a fingerprint prompt, and a first backup that hangs silently for a week is a classic. While you are on the destination, give it the same treatment as any other machine you own: run the first-hour hardening checklist on it as well. It holds a copy of everything, and it deserves the full hour.
Generate a passphrase you will never type. It is read from a file by a script, so length costs nothing and there is no reason to make it memorable:
openssl rand -base64 32 > /root/.restic-pass
chmod 600 /root/.restic-pass
cat /root/.restic-pass
Now stop and copy that string somewhere that is not this server. A password manager, a paper note in a drawer, a second machine — anywhere that survives the loss of the source. This is the same failure that destroys self-hosted vaults and automation engines: the key that decrypts everything is stored next to everything, and both are lost together. A repository whose passphrase existed only on the machine it was protecting is not a backup; it is a very tidy pile of noise.
Write the environment file that every later command will read:
/etc/backup/restic.env
RESTIC_REPOSITORY=sftp:rkv-repo:/srv/restic/web01
RESTIC_PASSWORD_FILE=/root/.restic-pass
chmod 600 /etc/backup/restic.env
set -a; . /etc/backup/restic.env; set +a
restic init
The equivalent pair for Borg — the repokey mode stores the encryption key inside the repository, protected by the passphrase, which is convenient and is exactly why you also export a copy of it:
export BORG_REPO=ssh://borg@rkv-repo/srv/borg/web01
export BORG_PASSCOMMAND="cat /root/.restic-pass"
borg init --encryption=repokey-blake2
borg key export :: /root/borg-key.txt # copy this off the server too
The first snapshot. Run it by hand, watch it, and let it finish before you automate anything. This is the slow one — every chunk is new — and it is the run that tells you whether the include list was right:
restic backup \
--files-from /etc/backup/include.txt \
--exclude-file /etc/backup/exclude.txt \
--tag nightly --one-file-system --verbose
If the source is serving traffic and the upload is saturating the link, throttle it. The flag takes kibibytes per second, so 20000 is roughly 20 MB/s:
restic backup --limit-upload 20000 --files-from /etc/backup/include.txt
When it finishes, look at what you actually got. These three commands are the ones to run now, and again in six months when you have stopped thinking about any of this:
restic snapshots # the list, with dates and tags
restic stats latest # what the newest snapshot contains
restic ls latest /etc/backup # confirm a path you expected is in there
That third command is worth a moment. Half of all broken backups are not broken at all — they are complete, healthy repositories of the wrong directory. Listing a path you expected to find, on the very first snapshot, catches that while the include file is still fresh in your mind.
The database rule, stated once. A running database engine keeps state in memory and writes it out in its own time. Read its files while it is doing that and you get a set of pages from several different moments — a file that may restore, may restore corrupt, or may restore into something that opens cleanly and is quietly missing the last hour. There is no way to tell which from the outside. So the backup never touches the live files: a dump is written to disk first, and the backup picks up the dump.
Everything goes into one script. Put it at /usr/local/sbin/nb-backup.sh, make it executable, and keep it in the backup itself:
#!/bin/sh
set -eu
# ---- 1. Consistent dumps, written where the include list will find them.
install -d -m 0700 /var/backups/db
# PostgreSQL in a container:
docker exec -t app-db pg_dumpall -U postgres | gzip -9 > /var/backups/db/postgres.sql.gz
# MariaDB / MySQL, InnoDB tables:
# docker exec -t mail-db mariadb-dump --single-transaction --all-databases \
# | gzip -9 > /var/backups/db/mariadb.sql.gz
# SQLite — never a plain file copy:
# sqlite3 /opt/app/data/app.db ".backup '/var/backups/db/app.db'"
# ---- 2. The snapshot.
restic backup \
--files-from /etc/backup/include.txt \
--exclude-file /etc/backup/exclude.txt \
--tag nightly --one-file-system
# ---- 3. Retention, then reclaim the space it freed.
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
# ---- 4. Structural check every night; read 5% of the data on Sundays.
restic check
[ "$(date +%u)" = "7" ] && restic check --read-data-subset=5%
echo "backup ok: $(date -u +%FT%TZ)"
Three details in that script are doing more work than they look like. The set -eu at the top means a failed dump aborts the run instead of quietly snapshotting yesterday's dump for the next six months. The forget and the prune are one command, because a retention policy that never reclaims space is a disk that fills up. And the Sunday check reads a slice of the actual data rather than only the index — repository corruption is rare, and silent, and the only thing that finds it is reading.
The timer. Two small unit files. systemd is the right scheduler here rather than cron, because a timer gives you Persistent — a run missed while the machine was down happens at the next boot instead of being skipped in silence — and because journalctl then holds the history of every run.
/etc/systemd/system/nb-backup.service
[Unit]
Description=Nightly encrypted offsite backup
Wants=network-online.target
After=network-online.target docker.service
[Service]
Type=oneshot
EnvironmentFile=/etc/backup/restic.env
ExecStart=/usr/local/sbin/nb-backup.sh
Nice=10
IOSchedulingClass=idle
/etc/systemd/system/nb-backup.timer
[Unit]
Description=Nightly encrypted offsite backup
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=45m
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now nb-backup.timer
systemctl list-timers nb-backup.timer
systemctl start nb-backup.service && journalctl -u nb-backup.service -n 40 --no-pager
The randomised delay is not decoration. Every self-hosted server on the internet runs its maintenance at exactly 03:00, and a backup that starts at the same second as the log rotation, the certificate renewal and the container update spends its night fighting them for I/O. Spreading the start across forty-five minutes costs nothing and removes a whole class of mysterious slow nights.
Then make failure loud. This is the step everyone skips and it is the one that decides whether any of the above was worth doing. A backup that stops working announces nothing: the timer keeps firing, the service keeps failing, and the last good snapshot recedes into the past while the dashboard stays green. Add an OnFailure= unit to the service that sends you something you will actually see, and once a month glance at the top of the snapshot list. One line, one habit.
restic snapshots --latest 3 # is the newest one from last night?
systemctl list-timers nb-backup.timer # is the timer still armed?
There are two drills, and they answer different questions. The small one asks "is the data really in there and readable?" and takes ninety seconds. The big one asks "how long would it actually take to get the service back?" and takes an afternoon, once a year. Do the small one monthly. Do the big one at least once, because the answer is never what people guess.
The small drill. Pull one directory and one dump into a scratch path, then compare them with what is live:
install -d -m 0700 /mnt/restore
restic restore latest --target /mnt/restore --include /etc/backup
restic restore latest --target /mnt/restore --include /var/backups/db
diff -r /etc/backup /mnt/restore/etc/backup && echo "config identical"
gzip -t /mnt/restore/var/backups/db/postgres.sql.gz && echo "dump readable"
Even better, when the tool offers it: mount the repository read-only and walk it like a filesystem. Every snapshot appears as a dated directory, and you can browse history with ls and cat rather than restoring anything at all. It needs FUSE on the machine doing the mounting:
restic mount /mnt/snapshots # then: ls /mnt/snapshots/snapshots/latest/
borg mount :: /mnt/snapshots # the Borg equivalent
The big drill. Order a scratch VPS by the hour, and rebuild the service on it from the repository alone — no notes from memory, no files copied from the live box. Work from a written runbook and correct the runbook as you go, because the gaps you find are the whole point of the exercise. The usual discoveries, in order of frequency: the passphrase was only on the dead machine; the DNS records were never written down; a bind-mounted directory sat outside every include path; the restore needed a package version that is no longer the default; nobody knew which container had to start first.
Time it. Write the number at the top of the runbook with the date. That number — not the size of the repository, not the number of snapshots — is the only honest measure of how protected the service actually is.
And read the data occasionally. The nightly check verifies structure: that the index agrees with itself and no blob is missing. It does not read the blobs. Once a month, or on the Sunday run, read a slice of them — a rotating five per cent covers the whole repository over a couple of months without ever costing a long night:
restic check --read-data-subset=5%
borg check --verify-data # the Borg equivalent, slower and thorough
Everything up to here protects against accidents. This chapter protects against an adversary, and the design problem is uncomfortable once you see it: the source machine has to reach the repository every night, which means the source machine holds a working credential for the repository. Whoever owns the source owns that credential. If the credential can delete, the attacker deletes — and modern ransomware does exactly this, on purpose, before it encrypts anything, because a restorable victim does not pay.
The fix is not a better password. It is an asymmetric permission: the source may add data and may not remove any. Three ways to build it, in descending order of how easy they are to get right.
One — forced append-only over SSH. This is Borg's home ground and it is the cleanest answer available on a machine you own. The authorized_keys entry from step 03 forces borg serve --append-only, so the far end refuses deletions no matter what the near end asks for. Pruning still has to happen, but it happens from the destination side, on a schedule the source cannot influence, run by an account the source cannot reach. An attacker with root on the source can fill the repository with garbage; they cannot remove the archive from the night before they arrived.
Two — an append-only REST endpoint. restic's companion rest-server has an --append-only mode with the same property: new blobs are accepted, existing ones cannot be removed. Run it on the destination behind TLS, point RESTIC_REPOSITORY at the https:// URL instead of the sftp: one, and the shape of the defence is identical. It costs one more service on the destination and buys the same guarantee.
Three — object lock. On S3-compatible storage, versioning plus an object-lock retention period makes deletion impossible until the period expires, enforced by the storage layer rather than by a program. It is the strongest of the three and the most expensive, and it introduces a provider account — which brings back the custodial risk this whole guide is trying to spread out. Sensible as a third copy, awkward as the only one.
If none of those fit — plain SFTP, a key that can write and therefore delete — do not pretend otherwise; add a second, independent copy that the source cannot touch at all. Pull instead of push: let the destination log in to the source, read what it needs, and store it. The credentials then live on the machine that is not exposed, and an attacker on the source finds nothing pointing at the backup. It is a little more work to set up and it inverts the risk in exactly the right direction.
Whichever you pick, verify it the way you would verify any other security control — by trying to break it. From the source, with the backup key, attempt a delete. The correct outcome is a refusal.
borg delete ::name-of-an-old-archive
# → Remote: Repository is in append-only mode. Refusing to delete.
Every service has a small piece of state that is not data and is not configuration, and losing it does not damage the service — it replaces it with a different service that happens to have the same name. Old links break, federated peers refuse the new identity, clients re-key. These are the pieces, per stack, for the things this site has guides for.
The data directory, config/config.php, and a database dump. Turn maintenance mode on for the dump, off afterwards — Nextcloud writes to both at once otherwise.
The whole data directory: db.sqlite3 taken with .backup rather than copied, plus rsa_key files, attachments and sends.
The signing key, homeserver.yaml, the media store and a PostgreSQL dump. Lose the signing key and the federation identity is gone for good.
The mail store itself, the virtual user tables, and the DKIM private keys — a new DKIM key means every message is unsigned until DNS catches up.
N8N_ENCRYPTION_KEY first, then the PostgreSQL dump. The database without that key is a list of workflows whose every credential is unreadable.
The hidden service directory. Those few files are the .onion address — without them the service comes back under a different name and every link to it is dead.
/etc/wireguard in full. Small, boring, and the difference between a five-minute rebuild and re-keying every device you own.
The seed and the channel state, on the node's own terms. Restoring stale channel data can cost you funds — follow the implementation's backup procedure, not a generic file copy.
Ask most people why the backup should be somewhere else and the answer is fire, flood, or a datacentre losing power. All true, all increasingly rare, and all answered by a hundred kilometres of distance. The failures that actually take businesses down in 2026 are administrative, and distance alone does nothing about them.
One account is one point of failure. A suspension for an automated fraud signal, a chargeback while you were on a plane, a compliance review, a takedown addressed to the company rather than to a machine — every one of these reaches every server under that account at the same moment, including the one holding the snapshots. The technical redundancy was perfect and irrelevant. Two providers, or at minimum two accounts under two different legal roofs, is the only structure that answers this.
One jurisdiction is one legal frame. The four Nordic bastions are not interchangeable — Sweden, Finland, Norway and Iceland each protect a slightly different thing, and Norway and Iceland sit outside the European Union entirely. Splitting a working machine and its repository across two of them means the copy is not merely elsewhere; it is under a second, independent set of rules. The Nordic jurisdictions reference goes through the statutes one by one.
And the destination learns nothing. This is what makes the split cheap rather than a trade-off. Both restic and Borg encrypt before anything leaves the source, so the repository host stores blobs it has no key for. It does not know which files it holds, or how many, or what they are named. You are not extending trust to a second party; you are renting a disk that cannot read itself. That is also the honest answer to "should I use a provider I do not fully trust as a backup target?" — for an encrypted repository, trust barely enters into it.
The latency is not a factor. Helsinki to Reykjavík is 30 ms on the backbone, Helsinki to Stockholm 8 ms, Stockholm to Oslo 11 ms. A nightly backup does not care about any of those numbers, and neither does a restore that is bounded by disk throughput. Pick the pair for legal distance, not for network distance — the network distance across the Nordics is a rounding error.
One more thing worth saying plainly, because it is the reason this guide sits on this site rather than on a generic sysadmin blog: the second machine re-opens every question the first one answered. If the working VPS was ordered without an identity document and paid in Monero, and then the backup box for it is ordered with a company card and a passport scan, the pair is exactly as identifiable as the weaker half. The pillar guide on anonymous VPS hosting walks the three layers — signup, payment, network — and they apply to a backup target just as literally as to a web server.
The repository is intact, encrypted, and permanently unreadable. Store the passphrase — and for Borg the exported key — in a password manager and on paper, before the first snapshot.
Live data files were copied instead of dumped. Write a dump in a pre-backup step and back up the dump; never point the include list at a running engine's data directory.
A writable credential on a compromised box is a writable credential for the attacker. Force append-only on the destination, or take the second copy by pull instead of push.
forget without prune marks snapshots and reclaims nothing. Run them together, keep free space on the destination, and never back up container images or node_modules.
The timer failed after an upgrade and nothing said so. Add an OnFailure= unit, and check the top of the snapshot list once a month — it takes ten seconds.
A bind mount outside /opt, a volume moved during a migration, an exclude pattern that matched more than intended. List a known path on every new snapshot, and re-read the include file after any change to the stack.
Ten questions that come up while setting this up — and two that only come up afterwards, once.
No — it is a rollback, and a useful one, but it fails at exactly the moments a backup exists for. A snapshot lives inside the same provider account as the server it copies. If the account is suspended, if a payment is disputed, if a support agent fat-fingers a deletion, or if an attacker gets your panel credentials, the snapshot goes with the machine. It also gives you nothing granular: you cannot pull a single deleted file out of yesterday, only roll the entire disk back and lose everything since. Keep snapshots — they are the fastest way to undo a bad upgrade — but do not confuse them with a copy that exists somewhere the original problem cannot reach. The rule of thumb: if one compromised credential can destroy both copies, you have one copy.
Both encrypt on the client, both deduplicate at chunk level, both are mature, and both will do the job. Pick restic if you want one static binary, a repository that can live on SFTP, S3-compatible object storage, a REST server or anything rclone can reach, and nothing at all installed on the destination. Pick Borg if the destination is an SSH box you control, you want the strongest append-only enforcement available without object-lock storage, and you like its slightly tighter compression. The practical differences that decide it: Borg needs borg installed at both ends and speaks only SSH or a local path; restic needs a prune pass that briefly takes an exclusive lock on the repository. If you cannot decide, use restic — fewer moving parts on the destination is worth more than any benchmark.
Start from the size of what you are actually backing up — not the size of the disk it sits on. A typical self-hosted stack is a few gigabytes of database and configuration plus whatever the users uploaded. Deduplication and compression then earn their keep: a daily snapshot of a 20 GB working set with a twelve-month retention policy usually lands between 40 and 80 GB of repository, because only changed chunks are ever stored twice. A Sentinel ($3.90/mo, 120 GB NVMe) covers that comfortably. Move to a Garrison (240 GB, $7.90) when several servers back up into the same repository host, and to a Bulwark (960 GB, $32.90) when the working set itself runs to hundreds of gigabytes. Size for the repository, then double it — a repository with no free space cannot prune, and a repository that cannot prune only grows.
You can copy them. You may not be able to restore them. A database writing to disk while you read it hands you a file set from several different instants, which is the definition of a torn backup — it restores as a corrupted database, or worse, as a database that opens fine and is quietly missing rows. The fix is a dump: pg_dump or pg_dumpall for PostgreSQL, mariadb-dump --single-transaction for MariaDB and MySQL on InnoDB tables, and sqlite3 db .backup out.db or VACUUM INTO for SQLite. Write the dump to a file, then back the file up. If the database is too large to dump nightly, the alternatives are a filesystem snapshot taken while the engine is briefly quiesced, or the engine's own physical backup tool — but for anything a single VPS runs, a dump is right and it is simple.
The backups are gone. Both restic and Borg encrypt client-side, and neither has a recovery path, a master key, or a support ticket that will unlock a repository for you. That is the point: the destination host — including a host that is not yours — never sees your plaintext. It also means the passphrase is now a piece of data worth exactly as much as everything it protects, and it must not live only on the machine being backed up. Put it in your password manager, and put a copy somewhere physical. For Borg, also export the repository key with borg key export and store that alongside; in repokey mode the key lives inside the repository, so a repository you can no longer reach takes the key with it.
You give it a credential that can add but not remove. This is the single most important design decision in the whole exercise, because modern ransomware looks for backup configuration first and follows it home. Three ways to get it. Append-only over SSH: in the destination's authorized_keys, force the command borg serve --append-only --restrict-to-path /srv/borg — the source can write new archives and cannot delete old ones. Append-only over REST: run restic's rest-server with --append-only and point the repository at it over HTTPS. Object lock: an S3-compatible bucket with versioning and a retention lock. Plain SFTP gives you none of this — a key that can write a repository can erase it — so if SFTP is your transport, pair it with a pull-based second copy taken from the destination side, where the credentials an attacker captured on the source do not reach.
Ask it the other way around: how much work are you willing to redo? That number is your recovery point objective, and it sets the interval. Nightly is right for almost every self-hosted service — a mail server, a Nextcloud, a Matrix homeserver, a workflow engine. Hourly is worth it when the data is transactional and re-entry is impossible. For retention, the pattern that survives contact with reality is --keep-daily 7 --keep-weekly 4 --keep-monthly 12: a week of granular undo, a month of weekly checkpoints, a year of monthly ones, for roughly 23 snapshots. The long tail matters more than people expect, because the failure it catches is not a dead disk — it is a corruption or a deletion that nobody noticed for six weeks.
Different building is the technical minimum; different jurisdiction is the part most people skip and then regret. A fire, a flood or a rack-level failure is answered by distance alone. What distance does not answer is legal or commercial: an account suspended by one provider, a payment dispute, a takedown that reaches every machine sitting under the same corporate roof, an order served on one company. If both copies live with the same provider, one letter reaches both. Splitting the pair across two Nordic bastions — the working machine in Helsinki, the repository in Reykjavík, 30 ms apart on the backbone — costs nothing in latency and buys a second legal frame. The data is encrypted before it leaves the source either way, so the destination learns nothing from holding it.
Encryption is not the bottleneck — modern CPUs do AES faster than a gigabit link can carry the result, and both tools use hardware acceleration where it exists. The bottleneck is the first upload, because everything is new: on a 1 Gbps uplink a 20 GB working set takes a few minutes at line rate, and rather longer if the destination is throttled or the files are many and small. Every subsequent run transfers only the chunks that changed, which for a typical stack is tens of megabytes and finishes in under a minute. If the first run competes with a production workload, rate-limit it — restic takes --limit-upload in KiB/s, Borg takes --upload-ratelimit — and run it under nice and ionice.
Yes, and the reason is not paranoia — it is that the common failures are silent. The timer that stopped firing after a package upgrade. The exclude pattern that quietly swallowed the uploads directory. The database dump written to a path the backup never covered. The repository that has been failing its integrity check for weeks into a log nobody reads. None of these announce themselves; all of them are found in ninety seconds by restoring a file and looking at it. Do a small restore monthly — pull one database dump and one data directory to a scratch path and diff them — and a full rebuild once a year, on a fresh VPS, timed. The number that comes out of that rebuild is your real recovery time, and it is rarely the number you would have guessed.
Sentinel — 120 GB of NVMe for $3.90 a month, in Helsinki, Stockholm, Oslo or Reykjavík. Unmetered bandwidth, so restoring costs nothing. No email at signup, no identity document, and a destination that cannot read a byte of what it holds.
Last reviewed · 2026-08-24 · Sources · restic and BorgBackup documentation, PostgreSQL and MariaDB backup manuals, sshd authorized_keys(5), systemd.timer(5) · Cadence · yearly
This guide is one spoke of a larger series. The pillar walks the three privacy layers end to end — the sibling spokes below dive into the specifics.
Three independent layers — signup, payment, network — explained, legal context included, common mistakes flagged.
SSH keys, ufw, fail2ban, kernel knobs, unattended-upgrades.
Deploy your own MCP server on a no-KYC VPS — TLS, streamable HTTP, OAuth.
Host an MCP server with no ID — the privacy stack, crypto-paid.
Move the agent off your laptop — sizing, systemd, secrets, spend caps.