← all writing

Storage

Where your world goes when you hit Backup

Click Backup and a few seconds later there's a restore point. Behind that button: restic snapshots, a dedicated Hetzner worker so your game box never lags, and Cloudflare R2 holding it all — with no egress bill to make us flinch when you restore.

You spent three weekends on that base. Then a plugin update eats the world file, or a friend with op runs something they shouldn’t, and it’s gone. This is the moment backups exist for — and it should be a non-event: click Restore, wait a few seconds, keep playing.

This post is the trip your world data takes between hitting Backup and that restore point showing up in your dashboard.

What we’re actually backing up

A game server is two things: a process (the running server) and its data (the world, configs, plugins, player files). The process we can recreate from an image any time. The data is the irreplaceable part, so that’s what a backup captures — the server’s data directory, frozen at a point in time.

For a busy Minecraft world that’s often a few gigabytes; for a modpack with a big map it can be tens. Whatever the size, we want a backup to be fast to take, cheap to keep, and safe to restore — without your server stuttering while it happens.

The obvious way, and why it lags

The naive version is one line: tar the data directory on the game box, gzip it, upload it. It works on a laptop. It’s a bad idea on a box with other people’s servers on it.

Compressing several gigabytes pins a CPU core and hammers the disk. On a shared node that means the other servers — running live games right then — get choppy. You’d be trading your backup for somebody else’s lag spike. Backups should be invisible to everyone, including the neighbors.

So the first rule of our backup system is: the heavy lifting never happens on the box your friends are playing on.

A dedicated backup worker

Instead, the work is handed to a separate backup worker — a Hetzner VM whose entire job is chewing through backups. The game node’s only role is to make the data available; the worker does the expensive part (chunking, dedup, compression, encryption) and ships it off.

game nodeyour server lives herebackup workerHetzner VM · runs resticCloudflare R2object storagedatasnapshotrestore
Fig 1. The game node hands data to a backup worker; the worker talks to R2. Heavy CPU/IO stays off the box running your game.

Because the worker is its own machine, a backup of a giant modpack can run flat-out without a single player noticing. And if we ever need more backup throughput, we add workers — they’re stateless; all the durable state lives in R2.

restic does the clever part

We don’t hand-roll the archive format. The worker runs restic, an open-source backup tool that gives us three things we’d otherwise have to build and get wrong:

  • Deduplication. restic splits data into content-defined chunks and only stores chunks it hasn’t seen before. Your world barely changes between two backups, so the second snapshot stores the diff, not another full copy.
  • Compression. Chunks are compressed (zstd) before they’re stored.
  • Encryption. Everything is encrypted before it leaves the worker, so what lands in R2 is opaque.

The payoff shows up the moment you take a second backup:

on the backup worker (not your game box)
restic -r s3:r2/backups backup /mnt/work/srv_9aFk2 --tag srv_9aFk2

# files:        24,817
# logical size: 5.418 GiB
# added to repo: 143.2 MiB        ← only the changed chunks were new
# snapshot a1b2c3d4 saved in 6.108s

A 5.4 GiB world, but only ~143 MiB actually written — that’s dedup and compression earning their keep. It’s why backups after the first one are quick and why keeping a deep history of restore points stays cheap.

Each finished backup is recorded as a small manifest the dashboard reads:

{
  "backup_id": "bkp_3xZ9k2",
  "server_id": "srv_9aFk2",
  "snapshot": "a1b2c3d4",
  "created": "2026-06-26T14:08:11Z",
  "logical_bytes": 5817342976,
  "stored_bytes": 150216704,
  "files": 24817
}

Why Cloudflare R2

restic needs somewhere to put its repository, and it speaks the S3 API, so almost any object store would technically work. We chose Cloudflare R2, and the deciding reason is delightfully boring.

R2 is also durable and S3-compatible, which means restic treats it as just another bucket and we get to use a battle-tested tool instead of inventing one. Boring, durable, and free to read from — exactly what you want under your saves.

Restoring without taking your server down

Restoring runs the same path in reverse, and the trick is keeping your server up for almost all of it.

  1. You pick a restore point in the dashboard.
  2. The backup worker pulls that snapshot from R2 and rebuilds the files into a scratch directory — verifying chunk checksums as it goes. Your server is still running normally through this whole step.
  3. Only once the restored copy is complete and verified does the game node do the quick part: stop the server, swap the new data directory in, start it again.

The multi-gigabyte work happens off to the side on the worker. The actual interruption is just the stop-swap-start at the end — seconds, not minutes. Nobody sits watching a progress bar while a 20 GB download trickles onto a live node.

What you actually see

From the dashboard it’s a list of restore points with timestamps and sizes, a Backup now button, and a Restore next to each one. Click, confirm, done. Everything above — the worker, restic’s dedup, the encrypted chunks in R2 — is invisible, which is the whole point.

Backups should be the most boring feature you own: there when you need them, silent when you don’t. Start it up, configure, play — and if it all goes sideways, roll it back and keep going.