A lit matte-black server blade in a dark datacenter streaming a ribbon of emerald data particles into a glowing wireframe duplicate of itself, sealed by a luminous padlock
Backup guide

Back up a VPS you can actually restore

Nearly everyone running a server has something they call a backup. Far fewer have ever restored one, and the distance between those two sentences is where data actually disappears. This is how to build a copy that survives the three things that genuinely destroy servers — your own hands, a compromise, and an account you stop controlling — without quietly bolting a verified identity onto an anonymous machine.

We keep three copies of every byte that runs this network and we have restored from them, on purpose, on an ordinary Tuesday, more times than we have ever needed to in an emergency. That is not diligence for its own sake. It is the only way to know that the thing in the repository is a backup rather than a folder of encrypted noise with a reassuring name.

The advice below is deliberately unfashionable. It does not involve a backup product with a dashboard, a monthly subscription or a support queue, because every one of those things is an account, and an account is a name, a card and a jurisdiction — three things you may have spent real effort keeping away from this server. It involves a second machine, about forty lines of shell, and one habit that almost nobody keeps.

You should be able to work through this in an evening. The tooling is Debian 13 and Ubuntu 24.04 flavoured; everything transfers to RHEL derivatives with the obvious substitutions. If your server is new, do the first-hour hardening before this page rather than after — backing up a machine that is already someone else's is a way of preserving their work.

What actually destroys data on a rented server

Ask anyone why they back up and the answer is usually “in case the disk dies”. On a modern VPS that is close to the least likely thing that will happen to you. Your volume sits on NVMe in a redundant array with a hypervisor that will migrate you off a failing host before you notice; drive failure is the provider's problem and it is a solved one. Building your entire backup strategy around it is like buying a fire extinguisher for a house that floods.

Here is what actually takes people's data, roughly in order of frequency. First, and by a wide margin: you. A rm -rf with a variable that expanded to nothing. A DROP DATABASE on the wrong terminal because two tabs looked identical. A migration script run twice. A docker compose down -v where the -v was muscle memory. These are not exotic; they are Tuesday.

Second, applications that delete their own data. An upgrade that runs a destructive migration and fails halfway. A plugin that clears a cache directory that turned out not to be a cache. A log rotation configured against the wrong path. Third, compromise — and note that this is now the case where an attacker wants the backups gone, which changes the design in ways we come back to below. Fourth, and easy to forget on a no-KYC host: losing the account. A balance that hit zero while you were away, a password manager entry you never wrote down, an email address you abandoned. We suspend within hours of a zero balance and destroy seven days later, and that is deliberately unforgiving — it is the arithmetic that lets us not ask who you are. There is no support agent who can look up your identity and make an exception, because there is no identity to look up.

That last category is the one that separates privacy-first hosting from the mainstream kind, and it should shape your plan. Every control that makes this server hard to attribute to you also makes it hard for anyone — including us — to give it back to you. The backup is what turns that trade from a risk into a choice.

Why a snapshot is not a backup

Snapshots are wonderful and you should use them. Take one before every kernel upgrade, every database migration, every time you are about to do something you would rather be able to undo. They restore in seconds, they cost almost nothing, and they will save you an afternoon a dozen times a year.

They are still not backups, for three structural reasons that no amount of provider marketing changes. They live inside the same account: whoever can log into your panel can delete them, and if the account stops working the snapshots stop existing with it. They live in the same failure domain: same provider, same control plane, often the same storage cluster, which means a single bad event can plausibly take both the original and the copy. And they are opaque: a snapshot of a running machine captures a database mid-write exactly as faithfully as it captures everything else, so a snapshot restore of a busy MySQL server gives you a crash-recovery scenario rather than a clean database.

The old 3-2-1 rule — three copies, on two kinds of media, one of them off-site — is usually recited without noticing that the middle clause is meaningless on rented infrastructure. You do not have two kinds of media. You have a virtual block device, and another virtual block device, and both of them are somebody's SAN. What survives the translation to a VPS is the part that mattered all along: at least one copy must be somewhere a single bad event cannot reach. Different provider or at minimum a different region, different credentials, different account, and — if the reason you host offshore is legal rather than technical — a different jurisdiction, so that one court order does not land on both copies at once.

The destination is part of your threat model

This is the section other backup tutorials do not have, and it is the one that matters most if you paid for this server in Monero.

The default advice everywhere on the internet is to push your backups to an object store: Backblaze B2, Amazon S3, Wasabi, a Google Drive via rclone. It is cheap, it is durable, it works. It also, in one command, undoes a great deal of what you were doing. Opening that account required a card and usually an identity document. From the moment the first snapshot uploads, that provider holds a timestamped record associating your name and payment details with your server's IP address, refreshed every hour, forever. You did not verify your identity with us; you verified it with them, and then you drew a line between the two.

The second half is worse, and it is the part people miss. The API key that authorises those uploads is a file on the production server. Anyone who gets root on that box — or anyone who lawfully acquires the disk — does not merely get your data. They get a credential that resolves, in one API call, to a billing identity. The anonymous server has become a signpost pointing at your bank.

None of this makes object storage wrong. It makes it a decision rather than a default. Three ways out, in descending order of how well they preserve what you started with:

  • A second no-KYC VPS, ideally in a different jurisdiction from production, paid from the same crypto balance. The destination inherits the anonymity properties of the source instead of contradicting them. This is what we do and what the rest of this guide assumes.
  • A storage provider that takes crypto without identity. They exist; they are smaller; check whether the payment path really is identity-free before you trust the marketing, and check the egress pricing before you find out during a restore.
  • A machine you physically own, pulling over WireGuard from behind your own connection. Excellent for anonymity, poor for availability, and it means your restore speed is your home upload speed. Reasonable as the third copy, weak as the only one.

Whatever you choose, the account that holds the backups should not share credentials, email address or recovery path with the account that holds production. The whole point is that one compromised login must not reach both. This is the same discipline as keeping identities separate elsewhere, applied to the least glamorous corner of the system.

Push, pull, and the reason ransomware finds your backups

Almost every backup tutorial produces the same architecture: a cron job on the production server that authenticates to a remote repository and writes to it. It is simple, it works, and it has a property nobody mentions — the production server holds a credential that can delete the entire repository. Pruning old snapshots requires delete rights, so the key that runs your nightly job is also the key that empties the vault.

Think about what that means during a compromise. An attacker with root on the box does not need to find your backups; you have helpfully left them a working credential and a config file naming the repository. Enumerating and destroying backups before triggering anything visible is not a hypothetical refinement — it is standard practice, because it is what turns an incident into a negotiation. Your nightly job was the reconnaissance.

There are three designs, and the difference between them is entirely about which machine holds which key.

Plain push is the one above. Production has read-write-delete. Convenient, and it fails completely against exactly the scenario where you most need a backup. Use it only if the sole threat you actually care about is your own fingers.

Append-only push keeps the same shape but removes the dangerous verb. The repository is served by something that understands the protocol and refuses deletions: rest-server --append-only for restic, or borg serve --append-only forced from authorized_keys. Production can create new snapshots and cannot remove old ones. Pruning happens later, from somewhere else, using a different key. This is a large improvement for about twenty minutes of work, and for most people it is the right stopping point.

Pull inverts the connection. The backup host reaches into production over SSH, copies what it needs, and runs the backup tool locally on its own disk. Production holds no backup credential at all — there is nothing on that machine to find, because the machine does not know where its backups live. This is the strongest arrangement and it is what the step-by-step below builds.

Be honest about what pull costs, because it is not free. You have moved the key rather than eliminated it: the backup host now holds an SSH key into production, so a compromise of the backup host reaches forward into the live system. That is a better trade — the backup host runs nothing, exposes nothing but SSH, and is a far smaller target than a public web server — but it is a trade, and you close most of the remaining gap by forcing that key to a read-only command so it cannot be used for anything but reading files.

Choosing the tool: restic, Borg, or plain rsync

Three tools cover essentially every case, and the choice is less agonising than the forum threads suggest.

restic encrypts by default, deduplicates across snapshots, speaks SFTP, S3, REST and a dozen other backends, and ships as a single static binary you can drop onto anything. Its repository format is content-addressed, so a snapshot is cheap and identical data is stored once no matter how many machines send it. The costs are real but modest: it wants memory proportional to the repository index, and an interrupted run can leave a stale lock that the next run refuses to step over until you clear it with restic unlock. This is the default recommendation and what the steps below use.

Borg deduplicates better, compresses better, and is noticeably faster on repositories with millions of small files. Its trade-off is coupling: Borg must be installed on both ends and versions must match closely, one repository is really designed for one client, and it has no native object-store backend without a helper layer. If your source is a large mail spool or a filesystem full of small files, and you control both machines, Borg will use half the space. Its --append-only mode is also the cleanest implementation of the idea in either tool.

rsync is not a backup tool and pretending otherwise is how people end up with one faithfully mirrored copy of a corrupted directory. It has no versioning, no deduplication and no encryption at rest; rsync --delete propagates your mistake to the copy at wire speed. It is nevertheless the right tool for moving bytes between two machines you control, which is precisely the job it does in a pull design, with restic providing the versioning and encryption once the bytes have landed. Use each for what it is.

One thing not to do: do not roll your own with tar and a date-stamped filename. It works for about four months, until the day the disk fills because nothing ever expired, or the day you discover that a full copy every night of a 40 GB dataset is 1.2 TB a month of storage you are paying for to hold thirty nearly identical things.

What to include — and the databases that will betray you

The instinct is to back up the whole filesystem. Resist it. A root filesystem is mostly distribution packages you can reinstall in ninety seconds, and including them costs storage, transfer time and, worse, attention — a 40 GB backup nobody wants to test is less useful than a 900 MB one that gets restored quarterly.

What genuinely cannot be reconstructed is short: /etc (your entire configuration and the reason a rebuild takes an hour instead of a weekend), /home and /root, /srv and /var/www, application state under /var/lib/ and /opt/, named Docker volumes, cron and systemd units you wrote, and your database dumps. Skip /proc, /sys, /dev, /run, /tmp, /var/cache, swap files, sockets and /var/lib/docker/overlay2 — that last one is rebuildable from your compose file and is often the single largest directory on the disk.

Now the part that quietly ruins restores. Copying a database's data directory while the database is running produces a file set that is very likely to be unusable. The engine holds state in memory, writes across multiple files in an order that matters, and your copy walks that tree over several minutes catching different files at different instants. InnoDB will sometimes crash-recover from it and sometimes will not; the failure surfaces months later during a restore you are already having a bad day about.

Dump through the engine instead, then back up the dump:

# MariaDB / MySQL — InnoDB gets a consistent snapshot from one transaction
mariadb-dump --single-transaction --quick --routines --triggers --events \
  --all-databases | zstd -T0 > /var/backups/db/all-$(date -u +%F).sql.zst

# PostgreSQL — pg_dump is consistent by design; grab roles separately
pg_dumpall --globals-only > /var/backups/db/globals.sql
pg_dump -Fc -Z6 mydb    > /var/backups/db/mydb.dump

# SQLite — never cp a live database; WAL mode makes that a torn file
sqlite3 /var/lib/app/app.db ".backup /var/backups/db/app.db"

Two footnotes worth having. --single-transaction gives you consistency for InnoDB only — if any table is still MyISAM it is copied outside the transaction and can be inconsistent with the rest, so convert those tables or lock them. And once you are dumping, exclude the live data directory from the file pass. Backing up both means storing a large torn copy alongside the good one, and giving a future restore two candidates where one is quietly wrong.

Containers do not change the principle, only the path: run the dump with docker compose exec -T db mariadb-dump … and back up the resulting file, not the volume underneath it.

Where the encryption key lives

Client-side encryption is the entire reason it is acceptable to store your data on a machine that is not the one it came from. restic and Borg both encrypt before anything leaves the source, so the backup host holds ciphertext and can be treated as untrusted storage. That property is worth exactly as much as your handling of the key, and no more.

The failure mode is depressingly common. The repository password sits in /etc/restic/env on the production server, which is fine and necessary for automation. Then production is what you lose — destroyed, seized, or simply gone with the account — and you are looking at several hundred gigabytes of encrypted blocks and the dawning realisation that the only copy of the key was inside the thing you were protecting yourself against losing.

So: the password on the server is a working copy, never the record. The record lives somewhere that survives the server — a password manager whose vault is itself backed up elsewhere, or written on paper in a drawer, or both. Write down the repository location and the exact restore command next to it, because a passphrase with no context is a puzzle you will be solving under stress in eighteen months. And take five minutes to verify it: from a different machine, with only what is in the password manager, run restic snapshots against the repository. If that works, you have a backup. If it needs something that only exists on production, you have a very expensive folder.

Both tools support multiple keys on one repository (restic key add), which is the clean way to give the pruning job or a second admin access without sharing the original passphrase. And if the production disk is encrypted with LUKS, keep the two secrets genuinely separate — storing the restic passphrase inside the LUKS volume and the LUKS passphrase inside the restic repository is a loop that fails closed on both sides.

Retention: the trap of keeping only a week

Retention looks like a storage-cost question and is really a detection-latency question. The number that matters is not how much disk you want to spend; it is how long a problem can go unnoticed in your system. Whatever that period is, your oldest backup must be older than it.

Seven daily snapshots feels generous and is thin in practice. A corrupted table nobody queries, a slow deletion by a misbehaving cron job, an intrusion that sat quietly for a month before doing anything visible — all of these routinely take longer than a week to surface, and if your history is seven days deep then every snapshot you hold is already contaminated. Dwell time in real intrusions is regularly measured in weeks. Cheap monthly snapshots are the specific defence against that, and deduplication makes them very cheap indeed: a monthly kept for a year adds a fraction of a full copy, because only the blocks that actually changed are stored twice.

restic forget \
  --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 2 \
  --prune

That ladder — a week of days, a month of weeks, a year of months, a couple of years of years — is around twenty-six snapshots and, on typical data, well under twice the size of a single full copy. It is the default we would argue for unless you have a specific reason to deviate.

Two operational notes. forget without --prune only removes the labels, so the space is not reclaimed until you prune — which surprises people watching a disk that will not shrink. And --prune requires delete rights on the repository, so in an append-only or pull design it does not run on the production server. It runs on the backup host, or from your laptop, with a key that production has never seen. That separation is the whole point; do not undo it for convenience at the last step.

Automating it without the silent failure

The classic backup disaster is not a job that crashes. It is a job that stops running and tells nobody, discovered eleven months later by someone who needed it. Every element below exists to make that specific outcome impossible.

Use a systemd timer rather than cron. You get real logs in the journal with exit statuses attached, Persistent=true so a run missed while the machine was off happens at next boot instead of being skipped forever, and RandomizedDelaySec so a fleet does not stampede the backup host at 03:00 exactly. Cron's failure notification is an email to a local mailbox that, on a modern server, goes precisely nowhere.

# /etc/systemd/system/backup.timer
[Unit]
Description=Nightly encrypted backup

[Timer]
OnCalendar=*-*-* 03:17:00
RandomizedDelaySec=1800
Persistent=true

[Install]
WantedBy=timers.target

Then add a dead-man's switch, which is the single highest-value line in this entire guide. At the very end of the script — after the backup has succeeded, not before — make an HTTPS request to a monitor that expects to hear from you daily and alerts when it does not. This inverts the notification logic: instead of relying on a failure to generate a message, absence itself becomes the alarm. A job that has been dead for three days is then a message in your inbox rather than a discovery in a crisis. Self-host the monitor on the backup host if you would rather not create another third-party account; it is a few lines and a timer of its own.

Finally, the small operational hazards, all of which we have hit at least once. An interrupted run leaves a lock and every subsequent run fails with a message you stop reading after the fourth night — handle restic unlock deliberately, not reflexively. A full destination disk fails every job until someone looks; alert on free space, not just on job status. An SSH key with an expiry, a rotated host key, an nftables rule added during unrelated work — each will silently sever the pull. And systemctl enable --now, not just start: a timer that was never enabled works beautifully until the first reboot and never runs again.

The restore drill

Everything above is preparation. This is the part that converts it into a backup, and it is the part almost everyone skips.

Once a quarter, deploy a fresh VPS — the $5 Starter is ample and billing is hourly, prorated to the second, so the whole exercise costs a couple of cents. Restore into it using only what you would have in a real disaster: the repository address, the passphrase from your password manager, and the written procedure. Deliberately do not use anything from production, because in the scenario you are rehearsing production does not exist. Bring the application up, point a hosts-file entry at the new IP, click around. Then destroy it.

# on a throwaway machine, with nothing but the passphrase
export RESTIC_REPOSITORY=sftp:[email protected]:/srv/restic/prod
restic snapshots                      # can you even see the history?
restic restore latest --target /restore
restic check --read-data-subset=10%   # verify stored blocks, not just metadata

What this catches is never what you expect. It is the config file that was in a directory the include list did not cover. The database dump that has been zero bytes for five weeks because a password changed and the script did not check its exit status. The application that will not start because a secret lives in an environment variable held by the orchestrator and was never in the filesystem at all. The undocumented DNS record. The certificate that has to be reissued before anything answers on 443. Every one of those is a ten-minute fix on a quiet afternoon and an ugly two hours at three in the morning.

Write down how long the drill took, end to end. That number — not the backup frequency — is your real recovery time, and it is the only honest answer when someone asks how long you would be down. Add restic check --read-data-subset=5% to a monthly timer as well: it reads and verifies a rotating sample of the actual stored blocks rather than only the index, which is how you find silent corruption while there is still a good copy to fall back on. If you are ever moving the whole server to a new host, a rehearsed restore is also most of the migration already done.

  1. Inventory what genuinely cannot be rebuilt

    Before any tooling, write the list. Walk the machine and ask of each directory: if this vanished, could I recreate it from a package manager, a git repository or a compose file? If yes, it does not belong in the backup. What is left is usually far smaller than people expect — configuration, user data, application state, database dumps.

    # the fast way to find what is actually big and stateful
    du -x -h -d2 / 2>/dev/null | sort -rh | head -30
    docker volume ls    # named volumes are state; overlay2 is not

    Turn the result into two explicit files, /etc/restic/include.txt and /etc/restic/exclude.txt. Explicit lists beat clever find expressions because they are reviewable, and because a new directory appearing on the server should be a deliberate decision rather than a silent inclusion.

  2. Deploy the backup target in a different jurisdiction

    Order a second VPS in a region that is not the one production runs in — if production is in Paris, put the copy in Reykjavík or Bucharest. The point is that no single legal or physical event reaches both machines. A Starter at $5/mo carries 80 GB of NVMe, which after deduplication holds a very long history of a typical small server; the 12-month billing cycle halves that. Pay it from the same crypto balance so the destination inherits the anonymity of the source rather than contradicting it.

    Give it a separate account from production if you want full credential isolation. Then harden it exactly as you would anything else — keys-only SSH, default-deny firewall — and install nothing else on it. This machine's value is that it is boring: no web server, no open ports beyond SSH, nothing to exploit from the internet.

  3. Create a read-only door from the backup host into production

    This is the step that makes it a pull. On the backup host, generate a dedicated key. Then install its public half on production with a forced command so the key can do exactly one thing: read files. It cannot open a shell, forward a port, or write anything.

    # on the backup host
    ssh-keygen -t ed25519 -a 100 -f ~/.ssh/pull_prod -C "pull@backup"
    
    # on production, in /root/.ssh/authorized_keys — one line
    command="/usr/bin/rrsync -ro /",restrict ssh-ed25519 AAAAC3Nz… pull@backup

    rrsync ships with rsync (/usr/bin/rrsync on Debian 13; /usr/share/doc/rsync/scripts/rrsync on older releases) and -ro makes it refuse anything but reads. restrict disables port forwarding, agent forwarding, PTY allocation and X11 in one word. Verify the cage holds before you rely on it — ssh -i ~/.ssh/pull_prod root@production must fail to give you a shell.

  4. Dump the databases on production, on their own schedule

    Production still owns one job: producing consistent dumps into a staging directory that the pull will collect. This needs no backup credentials, which is exactly why the design works.

    # /usr/local/sbin/dump-db.sh   (chmod 700)
    set -euo pipefail
    D=/var/backups/db; install -d -m 700 "$D"
    mariadb-dump --single-transaction --quick --routines --triggers --events \
      --all-databases | zstd -T0 > "$D/all.sql.zst.tmp"
    mv "$D/all.sql.zst.tmp" "$D/all.sql.zst"

    Note the write-to-temp-then-rename: it means the pull can never collect a half-written dump, whatever the timing. set -euo pipefail is not decoration — without it a failing mariadb-dump pipes an empty stream into zstd, which succeeds, and you get a valid compressed file containing nothing at all. That is the single most common way a backup is silently useless. Run it from its own timer half an hour before the pull.

  5. Pull the data down to the backup host

    On the backup host, rsync production's include list into a staging tree. Only changed blocks cross the wire, so after the first run this is fast and cheap.

    # /usr/local/sbin/pull.sh   (chmod 700, runs on the BACKUP host)
    set -euo pipefail
    [email protected]
    # -r is spelled out on purpose: with --files-from, -a does NOT imply
    # recursion, and without it you silently copy empty directories.
    rsync -aHAX -r --delete --numeric-ids \
      -e "ssh -i /root/.ssh/pull_prod -o BatchMode=yes" \
      --files-from=/etc/backup/include.txt \
      --exclude-from=/etc/backup/exclude.txt \
      "$SRC:/" /srv/staging/prod/

    -aHAX preserves hardlinks, ACLs and extended attributes, and --numeric-ids keeps ownership meaningful across machines whose /etc/passwd differ. --delete is safe here precisely because staging is not the backup — the versioned history lives in the restic repository built in the next step, so a deletion that propagates into staging is still recoverable from yesterday's snapshot.

  6. Initialise the repository and store the key offline

    Still on the backup host, create a local restic repository and back the staging tree into it. Local means no network in the hot path, no remote credential to steal, and a restore that runs at disk speed.

    apt install -y restic
    install -d -m 700 /etc/backup
    openssl rand -base64 32 > /etc/backup/pass   # write this into your password manager NOW
    chmod 600 /etc/backup/pass
    
    export RESTIC_REPOSITORY=/srv/restic/prod
    export RESTIC_PASSWORD_FILE=/etc/backup/pass
    restic init

    Copy that passphrase into a password manager, and beside it write the repository path and the restore command. Then prove it: from a third machine, using only the password manager, run restic -r sftp:backup@…:/srv/restic/prod snapshots. If that lists snapshots, the key is genuinely recoverable. If it needs anything that only exists on either server, fix that now rather than discovering it later.

  7. Schedule it, and make silence an alarm

    Wrap the pull, the restic run and the prune into one script and drive it from a systemd timer. Note that forget --prune is safe to run here because the backup host legitimately owns the repository — production never held a delete credential at any point.

    # tail of /usr/local/sbin/backup.sh
    restic backup /srv/staging/prod --tag nightly
    restic forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 \
                   --keep-yearly 2 --prune
    curl -fsS -m 10 --retry 3 https://hc.example.net/ping/<uuid>  # only on success
    systemctl daemon-reload
    systemctl enable --now backup.timer
    systemctl list-timers backup.timer     # confirm the next run is where you expect

    The curl runs only if every command before it succeeded, because of set -e. The monitor on the other end expects a daily ping and alerts on absence, which turns a job that quietly stopped into an email rather than an archaeological finding. enable --now, not start — a timer that was never enabled survives exactly until the first reboot.

  8. Run the restore drill, and write down how long it took

    Put a recurring reminder in whatever you actually read, once a quarter. Deploy a throwaway Starter, restore into it with nothing but the passphrase and the written procedure, bring the application up, verify it serves real data, destroy the machine. Hourly billing means the entire drill costs a couple of cents.

    restic restore latest --target /restore
    zstd -dc /restore/var/backups/db/all.sql.zst | mariadb
    systemctl start nginx app
    curl -H 'Host: example.com' http://127.0.0.1/health

    Record the elapsed time and every surprise you hit, then fix the surprises in the backup script rather than in your memory. That elapsed time is your true recovery objective; until you have measured it once, any number you quote is a guess.

Comparison

Where to put the copy

Each destination against the four questions that decide it: what it costs your anonymity, what it costs in money, how fast you get your data back, and what it survives.
DestinationAnonymity costMoneyRestore speedSurvives
Second no-KYC VPS, different regionNone — paid from the same crypto balance, no identity anywhere$5/mo, halved on a 12-month cycleFast — datacentre-to-datacentre over 1–10 GbpsAccount loss, compromise, one jurisdiction, your own mistakes
Provider snapshotNoneCheapSecondsAlmost nothing — same account, same failure domain, dies with both
Object store (B2 / S3 / Wasabi)High — card and ID on the account, and the API key on your server points at itVery cheap to store, egress fees bite during a restoreFast, if you accept the egress billCompromise and account loss, at the price of a name attached to the machine
Crypto-accepting storage providerLow, if the payment path is genuinely identity-free — verify, do not assumeModerateVaries wildly — check egress limits before you need themMost things, with more counterparty risk than a VPS you control
Home NAS pulled over WireGuardNone — nothing leaves your own network unencrypted or unattributedHardware you already ownSlow — capped by your home upload speedEverything except your house; poor as a sole copy, excellent as a third
Nothing / “it is in git”NoneFreeNeverNothing. Git holds your code; it does not hold your database, your uploads or your /etc
FAQ

Questions worth answering

Isn't a provider snapshot already a backup?

No, and the distinction is not pedantic. A snapshot lives in the same account, under the same credentials, in the same failure domain as the server it copies — so it does not survive the account being lost, and anyone who reaches your panel can delete it alongside the original. It is also taken from a running machine, which means a busy database is captured mid-write. Use snapshots as an undo button before risky changes; use an off-site encrypted repository as the backup.

restic or Borg — which should I use?

restic unless you have a specific reason not to. It encrypts by default, needs nothing installed on the destination, speaks every backend worth using, and ships as one static binary. Borg deduplicates and compresses better and is meaningfully faster on filesystems with millions of small files, but it must be installed and version-matched on both ends and is really designed for one client per repository. Large mail spool and both machines under your control: Borg. Everything else: restic.

How much disk does the backup target need?

For a deduplicated repository with the retention ladder in this guide — 7 daily, 5 weekly, 12 monthly, 2 yearly — budget roughly two to three times the size of the data you are actually backing up, not of the whole server. Twenty-six snapshots do not mean twenty-six copies, because only changed blocks are stored again. A small site with 15 GB of real state fits comfortably on the 80 GB Starter with years of history to spare.

Doesn't a second VPS double my hosting bill?

Only if the backup target matches production, and it should not. It runs no application and serves no traffic; it needs disk, not CPU. A $5 Starter behind a $30 production server is a sixth of the bill, and the 12-month cycle takes 50% off it. Set against the cost of losing everything, it is the cheapest line on the invoice — and the target doubles as a rehearsal machine for the restore drill.

Where should the repository password live?

In a password manager whose vault is itself backed up somewhere else, or on paper, or both — and beside it, the repository address and the exact restore command. The copy in /etc on the server is a working copy for automation, never the record. Verify it properly: from a machine that is neither production nor the backup host, using only what is in the password manager, run restic snapshots. If anything else is required, you do not yet have a recoverable backup.

How often should backups run?

Ask how much work you are willing to redo. Nightly is right for most servers: at most a day is lost, and a single overnight run is easy to reason about. A busy database wants more — hourly dumps of the database only, with the full file pass staying nightly, is the usual shape. Frequency past that point delivers less than the same effort spent on longer retention and an actual restore drill, which is where the real risk sits.

Can I back up a server whose disk is encrypted with LUKS?

Yes, and the two are complementary rather than redundant. LUKS protects the disk while the machine is powered off; the backup protects you from deletion, corruption and losing the machine entirely. Back up the mounted filesystem exactly as you would an unencrypted one — restic encrypts again on the way out, so the repository is safe on untrusted storage. Keep the two secrets in genuinely separate places: a LUKS passphrase stored only in the restic repository, or a restic passphrase stored only inside the LUKS volume, is a loop that fails closed on both sides.

If my server gets compromised, are the backups safe?

That depends entirely on one decision you made months earlier. If the production server holds a credential with delete rights on the repository, then no — an attacker enumerates and destroys the backups first, because that is what turns an incident into a negotiation. If the repository is append-only, or the backup host pulls and production holds no backup credential at all, then the history survives and you restore to a snapshot from before the intrusion. That is the entire argument for the pull design, and the reason to prefer it before you need it.

Deploy your offshore server.

Pick a region. Pick a plan. Paste a key. Pay. The next 47 seconds are on us.