BTCPay backup operations
This runbook documents the verified backup and restore path used by the current Sprey Processing reference deployment at pay.sprey.win.
The backup pipeline uses BTCPay Server’s native backup format. Sprey does not create a proprietary backup format: btcpay-backup.sh creates the canonical backup, GPG encrypts it, and the Sprey wrapper delivers and verifies the encrypted archive in Cloudflare R2.
Recovery layers
Section titled “Recovery layers”The reference deployment uses three independent recovery layers:
| Layer | Purpose | Retention |
|---|---|---|
| Local BTCPay backup | Fast access to the latest native encrypted backup | Latest backup only |
| Cloudflare R2 | Independent encrypted off-site history | 21 daily + 3 monthly |
| Hetzner Backup | Full-VPS recovery checkpoint | 7 daily automatic backups + manual checkpoints |
The local BTCPay backup is stored at:
/var/lib/docker/volumes/backup_datadir/_data/backup.tar.gz.gpgDo not keep an unencrypted BTCPay backup as the normal local recovery artifact.
Encryption key
Section titled “Encryption key”The backup passphrase is stored on the VPS in a root-only file:
/root/.config/sprey-backup/btcpay-backup-passphraseRequired permissions:
600 root:rootAn independent recovery copy of this passphrase must exist outside the VPS. The reference deployment stores it in Bitwarden. Never print the passphrase in documentation, logs, tickets, screenshots, or shell commands that embed its value directly.
R2 storage
Section titled “R2 storage”The private Cloudflare R2 bucket is sprey-backups.
BTCPay objects use these prefixes:
btcpay/daily/btcpay/monthly/R2 Lifecycle owns retention:
btcpay/daily/: delete after 21 days.btcpay/monthly/: delete after 93 days.
The VPS backup wrapper does not delete remote backups. Public bucket access is disabled. The R2 service credential is restricted to the backup bucket and to the production VPS public IPv4 and IPv6 addresses.
A Cloudflare budget alert is configured at USD 1 to surface unexpected R2 charges early.
R2 transport
Section titled “R2 transport”The reference deployment uses the official upstream rclone release rather than the older Ubuntu package build.
Verified version:
rclone v1.75.0The downloaded Debian package was verified against the upstream SHA256 checksum before installation.
The R2 remote is named:
r2:The rclone configuration is root-only. Do not expose its access key or secret key in diagnostics or documentation.
Backup wrapper
Section titled “Backup wrapper”The production wrapper is:
/usr/local/sbin/sprey-btcpay-backupRequired permissions:
700 root:rootIts responsibilities are deliberately narrow:
- Read the root-only backup passphrase.
- Run BTCPay Server’s native
btcpay-backup.sh. - Verify that the encrypted archive decrypts with the recovery key and that the resulting gzip stream passes its integrity test.
- Calculate the local encrypted archive SHA256.
- Upload the encrypted archive to
btcpay/daily/using a unique UTC timestamped object name. - Read the uploaded object back from R2 and calculate its SHA256 locally.
- Fail unless the local and read-back hashes match.
- On the first day of a UTC month, upload the same backup to
btcpay/monthly/and verify it in the same way.
A typical object name is:
btcpay-2026-09-03T16-15-44Z.tar.gz.gpgDo not use a successful upload alone as proof of a valid off-site backup. The verified path requires a read-back comparison.
R2 does not provide a usable remote SHA256 through the tested rclone sha256sum path. The verified implementation therefore reads the object with rclone cat and pipes the bytes through local sha256sum. This proves that the bytes retrieved from R2 match the local encrypted archive.
Automation
Section titled “Automation”The wrapper is executed by:
/etc/systemd/system/sprey-btcpay-backup.serviceThe daily timer is:
/etc/systemd/system/sprey-btcpay-backup.timerSchedule:
03:15 UTC dailyThe timer uses Persistent=true, so systemd can trigger a missed run after the server returns from downtime.
Check the timer with:
systemctl status sprey-btcpay-backup.timer --no-pagersystemctl list-timers sprey-btcpay-backup.timer --no-pagerCheck backup execution with:
systemctl status sprey-btcpay-backup.service --no-pagerjournalctl -u sprey-btcpay-backup.service -n 100 --no-pagerA successful run must include the wrapper’s remote verification success and must leave the BTCPay containers running.
Verified backup checkpoint
Section titled “Verified backup checkpoint”The following path was verified end to end on 2026-09-03:
BTCPay native backup ↓PostgreSQL dump ↓GPG encrypted archive ↓local decrypt + gzip integrity test ↓rclone upload ↓Cloudflare R2 ↓remote read-back ↓SHA256 comparison ↓verified matchThe first verified encrypted archive was approximately 21 MB. Multiple timestamped daily uploads were successfully created, and the systemd service was exercised independently before the timer was enabled.
The local backup directory was cleaned after verification so that only the latest encrypted backup.tar.gz.gpg remains as the normal local backup artifact.
Verified restore checkpoint
Section titled “Verified restore checkpoint”A native BTCPay restore was successfully tested on the production VPS on 2026-09-03 while the deployment had no merchant wallets or customer activity. A fresh manual Hetzner Backup was confirmed Available immediately before the test as the rollback checkpoint.
The encrypted local backup was restored with BTCPay Server’s native btcpay-restore.sh using the root-only recovery passphrase. The script successfully:
- Decrypted and extracted the GPG backup.
- Stopped the BTCPay deployment.
- Restored the backed-up Docker volumes.
- Started PostgreSQL and restored the database dump.
- Restarted the complete BTCPay deployment.
- Completed with exit status
0.
Post-restore verification confirmed:
All BTCPay containers runningBitcoin chain: mainBitcoin blocks = headers: 965346Bitcoin verificationprogress: 1Bitcoin initialblockdownload: falseBitcoin pruned: trueBTCPay /api/v1/health: {"synchronized":true}Sprey Processing store present and accessibleThe existing pruned Bitcoin blockchain survived the native restore, so no full blockchain resynchronization was required.
Native BTCPay backup restore — VERIFIED.
This checkpoint proves that the current encrypted native backup can restore the BTCPay application state on the existing deployment. It does not replace a future clean-host disaster-recovery exercise. Once the service contains merchant wallets or customer activity, perform destructive restore testing only in an isolated environment rather than on production.
Restore procedure
Section titled “Restore procedure”Before a production restore, confirm that a current independent recovery checkpoint exists and that the backup passphrase is available from its independent recovery copy.
From the BTCPay Docker directory:
export BTCPAY_BACKUP_PASSPHRASE="$(cat /root/.config/sprey-backup/btcpay-backup-passphrase)"
./btcpay-restore.sh \ /var/lib/docker/volumes/backup_datadir/_data/backup.tar.gz.gpg
RESTORE_STATUS=$?unset BTCPAY_BACKUP_PASSPHRASEecho "Restore exit status: $RESTORE_STATUS"Do not interrupt the restore while containers, volumes, or PostgreSQL are being restored. If the restore fails, inspect the failure before making further changes or invoking the provider-level rollback.
After a successful restore, verify containers, Bitcoin state, and BTCPay health:
docker ps --format 'table {{.Names}}\t{{.Status}}'./bitcoin-cli.sh getblockchaininfocurl -fsS https://pay.sprey.win/api/v1/health && echoThen verify the expected Store and configuration in the BTCPay UI.
Operational boundary
Section titled “Operational boundary”Backup ownership is intentionally separated:
BTCPay Server → canonical backup creation and restoreSprey wrapper → scheduling, encrypted off-site delivery, and verificationCloudflare R2 → object storage and retention lifecycleHetzner → independent full-VPS recovery layerBitwarden → independent recovery copy of the encryption keyKeep this boundary when extending the system. Do not add remote deletion, a proprietary backup format, or unrelated infrastructure management to the backup wrapper.