
The question isn't if you'll need a backup. It's when. Your backup strategy decides whether a deleted file, a broken update or a lost server ends with a restore or with lost data. On a self-managed VPS that strategy is yours to build: what to copy, a script that runs every night, a copy off the server, a restore test and storage that fits your budget.
The 3-2-1 Rule
The UK's National Cyber Security Centre sums the rule up as "at least 3 copies, on 2 devices, and 1 offsite":
| Rule | Meaning | Example |
|---|---|---|
| 3 copies | Keep three copies of data | Original + 2 backups |
| 2 devices | Store the copies on two different devices | VPS disk + object storage |
| 1 off-site | At least one copy off-site | Remote server or cloud |
What to Back Up
| Data | Priority | Frequency |
|---|---|---|
| Databases | Critical | Every 6 hours |
| Application code | High | Daily (or use Git) |
| Configuration files | High | After every change |
| User uploads | Critical | Daily |
| SSL certificates | Medium | After renewal |
| Cron jobs and scripts | Medium | After changes |
Don't Back Up
- OS files (reinstall from scratch faster than restoring)
- Package cache (/var/cache/apt/)
- Temporary files (/tmp/)
- Log files (unless needed for compliance)
Automated Backup Script
This script dumps each database to its own file, archives the web files and the configuration, and removes local copies after a week. Set DATABASES to the names of your databases.
#!/bin/bash
# /opt/backups/backup.sh
set -e
DATE=$(date +%Y%m%d-%H%M)
BACKUP_DIR="/opt/backups/local"
RETENTION_DAYS=7
DATABASES="app_db" # your database names, separated by spaces
mkdir -p "$BACKUP_DIR"
echo "Starting backup: $DATE"
# Database backup: one dump per database
echo "Backing up databases..."
for DB in $DATABASES; do
mysqldump --single-transaction "$DB" > "$BACKUP_DIR/db-$DB-$DATE.sql"
done
# Web files backup
echo "Backing up web files..."
tar czf "$BACKUP_DIR/www-$DATE.tar.gz" \
--exclude='node_modules' \
--exclude='.git' \
/var/www/
# Configuration backup: keep only the paths that exist on your server
echo "Backing up configurations..."
tar czf "$BACKUP_DIR/config-$DATE.tar.gz" \
/etc/nginx/ \
/etc/php/ \
/etc/mysql/ \
/etc/letsencrypt/ \
/etc/crontab
# Remove old local copies
echo "Cleaning old backups..."
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "*.sql" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: $DATE"
What the details do:
--single-transactiondumps a consistent state without blocking your application, but only for InnoDB tables. The MySQL manual warns that MyISAM or MEMORY tables can still change during the dump.- One dump per database. Without
--databases, a dump of one database contains noCREATE DATABASEorUSEstatements, so you can load it into any database name you choose. The restore test below depends on that. An--all-databasesdump writes those statements for every database, so it always loads back into the original names. - MariaDB calls the tool
mariadb-dump;mysqldumpstill works through a symlink on Linux, but MariaDB has marked that name deprecated since 11.0, so on MariaDB 11 or newer writemariadb-dumpin the script. A per-database dump does not hold your users and grants: our MariaDB and MySQL backup guide covers those. set -estops the script at the first command that fails, so a failed dump ends the run with an error in the log instead of passing unnoticed. GNU tar exits with status 1 when a file changed while it was archived and with 2 on a fatal error, such as a path that doesn't exist, so trim the configuration list to what your server has.-mtime +7matches files at least eight days old:findignores the fractional part when it counts 24-hour periods, so+7means more than seven whole periods.
Schedule it from root's crontab, so it can read /etc and write to /var/log:
sudo chmod +x /opt/backups/backup.sh
sudo crontab -e
# Run daily at 2 AM
0 2 * * * /opt/backups/backup.sh >> /var/log/backup.log 2>&1
Off-Site Backup with rclone
# Install rclone
sudo apt install rclone
sudo rclone config # Set up your remote storage
Run rclone config as root: rclone keeps its configuration per user (in ~/.config/rclone/rclone.conf), and the cron job above runs as root. Ubuntu 24.04 and Debian 12 ship rclone 1.60.1. rclone's own install page says distribution packages are often out of date and recommends its other install methods where possible.
Copy to Cloud Storage
# Copy local backups to the remote; nothing is deleted there
sudo rclone copy /opt/backups/local remote:vps-backups/
# Or add this as the last line of backup.sh (it already runs as root)
rclone copy "$BACKUP_DIR" remote:vps-backups/
Use copy, not sync. rclone sync makes the destination match the source, deleting files there if necessary, and rclone's docs warn that this can cause data loss. With sync, your off-site copy would lose every file the local cleanup removes, and an emptied local folder would empty the remote as well. rclone copy skips identical files and never deletes from the destination.
Keep the off-site copies longer than the local ones and clean them up on their own schedule:
# Check what would be deleted, then delete off-site copies older than 30 days
sudo rclone --dry-run --min-age 30d delete remote:vps-backups/
sudo rclone --min-age 30d delete remote:vps-backups/
rclone delete obeys filters, and --min-age 30d limits it to files at least 30 days old. Database dumps hold your users' data, so consider encrypting them before they leave the server: an rclone crypt remote wraps another remote and encrypts what you store through it.
To keep encrypted, versioned copies on a second server you control instead, follow our restic over SSH guide.
Where to Store the Copies
Prices in US dollars as each vendor's own pricing page listed them on 31 October 2026. Prices change, so check the page again before you choose.
| Storage | Price (checked 31 October 2026) | Also on the pricing page | rclone backend |
|---|---|---|---|
| Backblaze B2 | $6.95 per TB per month, pay as you go | No minimum storage duration fees; egress free up to 3x your average monthly storage, then $0.01 per GB | Backblaze B2 |
| Amazon S3 Standard | $0.023 per GB per month for the first 50 TB in EU (Ireland), $0.0245 in EU (Frankfurt) | No minimum charge; requests and data transfer are billed separately | Amazon S3 (provider AWS) |
| Wasabi | $7.99 per TB per month, Pay as You Go | Minimum monthly charge tied to 1 TB; 90-day minimum storage duration; no fees for egress or API requests | Amazon S3 (provider Wasabi) |
| Google Cloud Storage Standard | $0.020 per GiB per month in Netherlands (europe-west4), $0.023 in Frankfurt (europe-west3) | Operations and network usage are billed separately | Google Cloud Storage |
| Another server you control | That server's price | Encrypted, versioned copies with restic over SFTP | SFTP |
Two things move the real bill. Restores download data: Amazon S3 bills data transfer out to the internet beyond the first 100 GB a month (counted across all AWS services), Google Cloud Storage bills network usage, and B2's free egress stops at three times your average stored data. And Wasabi's Pay as You Go plan has a 90-day minimum storage duration, so check how that fits a short retention before you pick it. To estimate how much space your backups take, see our backup storage guide.
Backup Verification
A backup that doesn't restore is not a backup. This script loads the newest dump of one database into a scratch database and reads the newest web archive from start to end:
#!/bin/bash
# /opt/backups/verify.sh
set -e
BACKUP_DIR="/opt/backups/local"
DB="app_db"
DUMP=$(ls -t "$BACKUP_DIR"/db-"$DB"-*.sql | head -n 1)
WWW=$(ls -t "$BACKUP_DIR"/www-*.tar.gz | head -n 1)
# Test database restore into a scratch database
mysql -e "DROP DATABASE IF EXISTS backup_test; CREATE DATABASE backup_test;"
mysql backup_test < "$DUMP"
TABLES=$(mysql -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='backup_test';")
echo "Restored $TABLES tables from $DUMP"
mysql -e "DROP DATABASE backup_test;"
# Test archive integrity: tar lists the whole archive and fails on damage
tar tzf "$WWW" > /dev/null
echo "Archive integrity OK: $WWW"
Never point a test like this at an --all-databases dump on a live server. That file creates and selects each original database with CREATE DATABASE and USE, so the "test" would write old data into your real databases instead of backup_test. The scratch database also needs free disk space for a second copy of the data. For a full drill on a spare VPS, with the app started and the time measured, see our restore test guide.
Backup Schedule Recommendation
| Data Type | Frequency | Retention | Location |
|---|---|---|---|
| Database | Every 6 hours | 14 days | Local + off-site |
| Web files | Daily | 7 days | Local + off-site |
| Configs | After change | 30 days | Local + off-site |
| Server-level backup | Weekly | 4 weeks | Through your provider |
What Decides Your Recovery Time
| Scenario | Recovery From | What sets the time |
|---|---|---|
| Deleted file | File backup | Finding the right archive and extracting one path |
| Corrupted database | Database dump | The size of the dump and how fast it imports |
| Hacked server | Clean reinstall + backups from before the break-in | Reinstalling, hardening and finding a backup you trust |
| Hardware failure | Off-site backup + new VPS | How much you download and how fast you rebuild the stack |
The minutes depend on your data and your server, so time your own restore test and write the number down: that is your real recovery time. To turn it into recovery targets, with account access, contacts and the first hour of an outage, see our disaster recovery checklist.
On a Space-Node VPS, backups are not included in the plan price. For a server-level backup, open a support ticket: support tells you the backup option, how long backups are kept, how a restore works and what it costs before anything is set up. Keep the off-site copy from this guide either way.
Sources
All opened on 31 October 2026:
- NCSC: Offline backups in an online world (the 3-2-1 rule)
- MySQL 8.4 Reference Manual: mysqldump, dumping data in SQL format, reloading SQL-format backups, mysql client options; MariaDB: mariadb-dump
- rclone: install, configuration file, copy, sync, delete, filtering, crypt, Backblaze B2, Amazon S3 and compatible providers, Google Cloud Storage, SFTP; rclone packages for Ubuntu 24.04 and Debian 12
- Pricing pages: Backblaze B2, Amazon S3, Wasabi, Google Cloud Storage
- Ubuntu 24.04 man pages: tar(1), find(1), crontab(5), ls(1), head(1), bash(1)