Backups with restic and R2
The starter’s backup stack couldn’t work
Section titled “The starter’s backup stack couldn’t work”Two bugs, each enough on its own:
-
Credentials arrive as literal text. Its environment used
AWS_ACCESS_KEY_ID=$${R2_ACCESS_KEY_ID}. In Compose,$$is the escape for a literal$, so restic would receive the string${R2_ACCESS_KEY_ID}. -
Wrong variable name. It set
RESTIC_REPO. restic readsRESTIC_REPOSITORY— confirmed from the binary itself:Terminal window docker run --rm restic/restic:0.19.1 backup --help | grep -E -- '--repo|--password-file'# -r, --repo repository … (default: $RESTIC_REPOSITORY)# -p, --password-file file … (default: $RESTIC_PASSWORD_FILE)
It also kept secrets in plain env vars, backed up volumes that don’t exist here
(and not Gitea), and swallowed failures with || echo "backup failed (continuing)".
What gets backed up
Section titled “What gets backed up”| Included | Why |
|---|---|
pg_dump of the Gitea database |
A dump is consistent; copying live database files isn’t |
gitea-data volume |
Repositories, attachments, container registry |
traefik-acme volume |
Certificates (re-issuable, but rate-limited) |
/opt/vps-infra/starter |
Compose files, .env, secrets/, Traefik config — encrypted by restic |
| Left out | Why |
|---|---|
| The runner’s volumes | A cache and a re-creatable registration |
| The site container | Stateless; rebuilt from git |
| The pre-rebuild archives | Deleted when the old system is retired |
The nightly script
Section titled “The nightly script”nightly-backup.sh, run from the admin user’s crontab:
docker exec gitea-db pg_dump -U gitea -d gitea -Fcinto a dump file, written atomically, keeping the newest 3.restic backupof the list above, withRESTIC_REPOSITORY=s3:https://<account-id>.r2.cloudflarestorage.com/<bucket>,RESTIC_PASSWORD_FILE, and the R2 keys in an AWS-style credentials file (AWS_SHARED_CREDENTIALS_FILE).- Sundays:
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --pruneandrestic check.
Any failure stops the run and is logged. It initializes the repository only
when restic cat config fails, so a network error can’t trigger a second init.
Finishing it
Section titled “Finishing it”-
Save the restic password (generated on the server with
openssl rand -base64 32) in a password manager. Without it, no backup can ever be read. -
R2 → Manage API Tokens → Create Account API token: permission Object Read & Write, limited to the backup bucket. Copy the Access Key ID and Secret Access Key (shown once) and note the Account ID. R2 requires a paid plan before it issues tokens.
-
Write
secrets/r2_credentials:Terminal window read -rp 'R2 Access Key ID: ' K && read -rsp 'R2 Secret Access Key: ' S && echo \&& (umask 077; printf '[default]\naws_access_key_id = %s\naws_secret_access_key = %s\n' "$K" "$S" > secrets/r2_credentials) \&& unset K S -
First run, then test-restore into a scratch directory, compare with the live data, and
pg_restore --lista dump. -
Only then install the cron line.
Whether restic honours AWS_SHARED_CREDENTIALS_FILE isn’t in its man pages,
so step 4 is also that test.