Back up your VPS with restic
Every self-hoster loses data once. Mine was a Docker volume I deleted with docker compose down -v because I thought it would only remove the containers. After that I set up restic, and it's been running quietly every night since.
restic is a single binary. It encrypts everything before it leaves the server, only uploads what changed since last time, and can restore one file or the whole lot. Setup takes about twenty minutes.
Where do the backups go?
Not the same server. If the disk dies or you get locked out of the account, your backups go with it. Use one of these:
- A second small VPS at a different provider or location. restic talks to it over SFTP, so all it needs is SSH and some disk space.
- S3-compatible storage like Backblaze B2, Wasabi or Cloudflare R2. Cheap, and nothing to maintain.
I'll use SFTP here because it needs nothing beyond a second box you can SSH into.
Install and connect
restic needs to read everything, so it runs as root. Open a root shell for the rest of this post:
sudo -i apt install restic
Root needs an SSH key that can log into the backup box:
ssh-keygen -t ed25519 -f /root/.ssh/backup -N "" ssh-copy-id -i /root/.ssh/backup.pub backup@BACKUP_HOST
Then tell SSH to use that key for that host, in /root/.ssh/config:
Host BACKUP_HOST
User backup
IdentityFile /root/.ssh/backup
Create the repository
openssl rand -base64 32 > /root/.restic-pass chmod 600 /root/.restic-pass export RESTIC_REPOSITORY=sftp:BACKUP_HOST:/srv/restic/myvps export RESTIC_PASSWORD_FILE=/root/.restic-pass restic init
Copy that password somewhere that isn't the server, like your password manager. Without it the backups are useless random bytes. That's the point of the encryption, and also the trap.
Databases need a dump first
Copying the files of a running Postgres or MySQL can give you a broken backup. Dump them to a file first and back up the dump:
docker compose exec -T db pg_dumpall -U postgres > /srv/dumps/postgres.sql
SQLite (Vaultwarden, Uptime Kuma and plenty of others use it) has its own safe copy command:
sqlite3 /path/to/db.sqlite3 ".backup '/srv/dumps/vaultwarden.sqlite3'"
The backup script
Save this as /usr/local/bin/backup.sh:
#!/bin/sh set -e export RESTIC_REPOSITORY=sftp:BACKUP_HOST:/srv/restic/myvps export RESTIC_PASSWORD_FILE=/root/.restic-pass # dump databases first (delete this if you don't run Postgres, # and change /srv/stack to wherever your compose.yaml lives) mkdir -p /srv/dumps cd /srv/stack && docker compose exec -T db pg_dumpall -U postgres > /srv/dumps/postgres.sql restic backup /etc /srv /root --exclude-caches restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
chmod 700 /usr/local/bin/backup.sh crontab -e
30 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
That keeps a week of dailies, a month of weeklies and half a year of monthlies. Because restic only stores each piece of data once, all of those snapshots together take up surprisingly little space.
Test the restore. Really.
Run the script by hand once, then try to get something back:
restic snapshots restic restore latest --target /tmp/restore-test --include /etc/hostname cat /tmp/restore-test/etc/hostname
If that prints your hostname, you have a working backup. Run restic check every month or so too. It verifies the repository isn't damaged. I put a reminder in my calendar, because nobody remembers this on their own.