VPS Backups with Restic and Cloudflare R2
Here’s a scenario that keeps VPS operators up at night: your hosting provider suspends your account. Maybe they detected “suspicious activity” that was actually legitimate traffic. Maybe they had a billing dispute. Maybe they just decided they didn’t want your business anymore.
Doesn’t matter why. What matters is whether you can rebuild somewhere else in an afternoon or whether you’re scrambling through support tickets hoping someone will give you access to your data.
Most people think about backups as protection against hardware failure or their own mistakes. That’s part of it. But the more interesting problem is provider independence. Can you walk away from your current host tomorrow and have everything running on a new VPS by dinner?
If the answer is “no” or “I’m not sure,” you have a portability problem.
This guide solves it. Restic for encrypted, deduplicated backups. Cloudflare R2 for cheap, provider-independent storage. A clean restore path that works whether you’re rebuilding on Hetzner, Linode, DigitalOcean, or some provider that doesn’t exist yet.
The whole setup takes maybe an hour. The peace of mind lasts until you actually need it—at which point you’ll be very glad you spent that hour.
What You’ll Need⌗
Before we start, gather these values. You’ll plug them into the scripts below.
Server Details⌗
- Server name: A friendly identifier for this machine (e.g.,
prod-vps-1) - Web root: Where your sites live (typically
/var/www) - DB dump directory: Where SQL dumps will go (we’ll use
/var/backups/db)
Cloudflare R2 Credentials⌗
- Account ID: Found in your Cloudflare dashboard
- Bucket name: Create one in R2 (e.g.,
vps-backups) - Access Key ID: Generate an R2 API token with read/write access
- Secret Access Key: The secret half of that token
- Endpoint URL:
https://<your-account-id>.r2.cloudflarestorage.com
Restic Password⌗
- Repository password: A long random passphrase. This encrypts everything. Lose it and your backups are gone. Store it somewhere safe outside your VPS.
Install Restic⌗
sudo apt update
sudo apt install -y restic
Ubuntu’s packaged version works fine for most cases. If you need the latest, grab a static binary from Restic’s releases and drop it in /usr/local/bin/restic.
Create the Environment File⌗
This file holds your credentials. Every restic command will source it.
sudo nano /root/restic-env
Add your values:
export RESTIC_REPOSITORY="s3:https://<ACCOUNT_ID>.r2.cloudflarestorage.com/<BUCKET_NAME>"
export RESTIC_PASSWORD="<YOUR_LONG_RANDOM_PASSPHRASE>"
export AWS_ACCESS_KEY_ID="<R2_ACCESS_KEY_ID>"
export AWS_SECRET_ACCESS_KEY="<R2_SECRET_ACCESS_KEY>"
export RESTIC_HOST="<SERVER_NAME>"
Lock it down:
sudo chmod 600 /root/restic-env
Initialize the Repository⌗
Run this once to create the encrypted repository structure in R2:
source /root/restic-env
restic init
You should see: created restic repository. If you get credential errors, double-check your R2 API token permissions.
Set Up Database Dumps⌗
MySQL/MariaDB data lives in /var/lib/mysql. Don’t back that up live—you’ll get corrupted files. Instead, dump to SQL files and back those up.
Create the dump directory⌗
sudo mkdir -p /var/backups/db
sudo chmod 700 /var/backups/db
Create the dump script⌗
sudo nano /usr/local/bin/db-dump.sh
#!/usr/bin/env bash
set -euo pipefail
DUMP_DIR="/var/backups/db"
DATE="$(date +%F-%H%M)"
# Dump all databases
mysqldump --all-databases --single-transaction --quick --routines \
-u root -p'<DB_PASSWORD>' > "${DUMP_DIR}/alldb-${DATE}.sql"
# Remove dumps older than 7 days
find "${DUMP_DIR}" -type f -name '*.sql' -mtime +7 -delete
Replace <DB_PASSWORD> with your MySQL root password (or use a dedicated backup user).
Make it executable:
sudo chmod +x /usr/local/bin/db-dump.sh
Schedule the dump⌗
sudo crontab -e
Add:
0 3 * * * /usr/local/bin/db-dump.sh
This runs at 3:00 AM daily, before the backup job.
Create the Backup Script⌗
sudo nano /usr/local/bin/restic-backup.sh
#!/usr/bin/env bash
set -euo pipefail
source /root/restic-env
WWW_ROOT="/var/www"
DB_DUMP_DIR="/var/backups/db"
EXTRA_PATHS="/etc /home"
EXCLUDES=(
--exclude=/dev
--exclude=/proc
--exclude=/sys
--exclude=/run
--exclude=/tmp
--exclude=/mnt
--exclude=/media
--exclude=/lost+found
--exclude=/var/cache
--exclude=/var/tmp
)
restic backup \
"${EXCLUDES[@]}" \
"${WWW_ROOT}" \
"${DB_DUMP_DIR}" \
${EXTRA_PATHS}
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--prune
This backs up:
/var/www— your web files/var/backups/db— your SQL dumps/etc— system configs (nginx, cron, etc.)/home— user home directories
The retention policy keeps 7 daily, 4 weekly, and 12 monthly snapshots. Adjust to taste.
Make it executable:
sudo chmod +x /usr/local/bin/restic-backup.sh
Schedule the Backup⌗
Option A: Cron (simple)⌗
sudo crontab -e
Add:
30 3 * * * /usr/local/bin/restic-backup.sh
Runs at 3:30 AM, after the database dump completes.
Option B: Systemd Timer (more robust)⌗
Create the service:
sudo nano /etc/systemd/system/restic-backup.service
[Unit]
Description=Restic backup to Cloudflare R2
[Service]
Type=oneshot
EnvironmentFile=/root/restic-env
ExecStart=/usr/local/bin/restic-backup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
Create the timer:
sudo nano /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily Restic backup timer
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
Enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
The Persistent=true flag means if the server was off at 3:30 AM, it’ll run the backup when it comes back online.
Verify Your Backups⌗
Don’t just assume it’s working. Check periodically.
source /root/restic-env
# List all snapshots
restic snapshots
# Show the most recent snapshot
restic snapshots --last
# Verify repository integrity
restic check
Set a calendar reminder to run restic check monthly. Backups you can’t restore from aren’t backups.
Disaster Recovery⌗
Your VPS is gone. Time to rebuild. Here’s the process:
1. Spin up a fresh Ubuntu VPS⌗
Any provider. Any region. Doesn’t matter.
2. Install the basics⌗
sudo apt update
sudo apt install -y restic mysql-server nginx python3-venv
Adjust based on your stack.
3. Recreate the environment file⌗
sudo nano /root/restic-env
Paste in your credentials (you did save these somewhere safe, right?).
sudo chmod 600 /root/restic-env
4. Verify access⌗
source /root/restic-env
restic snapshots
You should see your snapshot history. If not, check credentials.
5. Restore your files⌗
# Web files
sudo mkdir -p /var/www
sudo restic restore latest --include "/var/www" --target /
# System configs
sudo restic restore latest --include "/etc" --target /
# Database dumps
sudo mkdir -p /var/backups/db
sudo restic restore latest --include "/var/backups/db" --target /
6. Import the database⌗
LATEST_DUMP=$(ls -t /var/backups/db/*.sql | head -n 1)
mysql < "$LATEST_DUMP"
7. Restart services⌗
sudo systemctl restart nginx
sudo systemctl restart mysql
Your server is rebuilt. Total time: maybe an hour, most of it waiting for file transfers.
Wrapping Up⌗
That’s the whole system. Database dumps, file backups, encrypted storage on infrastructure you control (or at least, infrastructure that’s separate from your primary provider), and a restore process you can run from memory if you need to.
The actual time investment here is minimal. An hour to set it up, maybe 30 minutes a year to verify it still works. The storage costs are negligible—R2’s pricing means you’re probably looking at a few dollars a month even with daily backups and 12 months of retention.
What you get in return is the ability to walk away from any provider dispute, any infrastructure failure, any “we’re shutting down your region” email, and have your stuff running somewhere else the same day.
That’s not paranoia. That’s just good infrastructure hygiene.