Backing up a small server with restic
I wanted three things from backups: encrypted before they leave the machine, deduplicated so daily snapshots cost almost nothing, and a restore I have actually performed rather than assumed. restic does all three with one static binary.
Repository
The repository can live on anything restic can talk to — SFTP, S3-compatible storage, a local disk. I use an S3-compatible bucket because it is cheap and off the machine. Credentials and the repository password go in a root-only file so cron jobs can read them:
sudo install -m 600 /dev/null /root/.restic.env
sudo tee /root/.restic.env >/dev/null <<'EOF'
export RESTIC_REPOSITORY="s3:https://s3.example.net/my-bucket"
export RESTIC_PASSWORD="a long passphrase kept somewhere else too"
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
EOF
. /root/.restic.env && restic init
The passphrase is the whole ballgame: lose it and the repository is noise. It is written down in two places that are not this server.
What to back up
Not the whole disk. The operating system is reproducible; my data is not. For this box that means /etc, /home, /var/www, and a dump of any database, taken just before the snapshot:
#!/bin/bash
set -euo pipefail
. /root/.restic.env
restic backup /etc /home /var/www \
--exclude-caches \
--exclude '/home/*/.cache' \
--tag daily
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune
Saved as /usr/local/sbin/backup.sh (mode 700) and run from root's crontab at 03:40. The forget --prune line is what keeps the bucket from growing forever; the retention numbers are a guess I have not needed to revisit.
The part people skip
Once a month I restore something on purpose:
restic snapshots --last
restic restore latest --target /tmp/restore-test --include /etc/nginx
diff -r /etc/nginx /tmp/restore-test/etc/nginx && echo "restore OK"
And a few times a year, restic check --read-data-subset=10%, which actually reads a slice of the stored blobs instead of trusting the index. The first time I ran it I found a repository I had initialised with a typo in the bucket name — empty, and faithfully backed up to for two weeks.
Things I got wrong first
- Running backups as my user instead of root, so half of
/etcwas silently skipped. - Not excluding
.cache: several hundred megabytes of browser and package caches per snapshot, deduplicated but still uploaded once. - Assuming the S3 endpoint clock was in sync with mine. It was not; signature errors until I fixed NTP on the server.