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.