· 7 min read

Server Backup Strategy: 3-2-1 Rule, Scripts, Restore Tests

A solid server backup strategy follows the 3-2-1 rule: keep three copies of your data, on two different types of storage, with one copy offsite, and prove regularly that you can restore from it. A backup you have never restored is only a hope, so this guide covers the rule, the difference between snapshots and backups, working scripts for MySQL and files, and a restore test you can automate.

The 3-2-1 Rule in Practice

The rule is simple, but each number protects against a different failure:

A practical layout for a single web or game server:

Copy Where Purpose
Live data Production VDS or dedicated server Serving traffic
Local backup /var/backups on the same host Fast restore of a deleted file or table
Remote backup Separate server in another location Survives loss of the primary host

The local copy is convenient but does not count as protection against host loss. Treat it as a staging area for the remote copy.

Snapshot vs Backup: What Is the Difference?

Snapshots and backups are often confused, and the confusion causes real data loss.

A snapshot is a point-in-time marker of a disk or VM, usually stored on the same storage as the original. It is fast and ideal for a quick rollback before an upgrade. A backup is an independent copy of the data, stored somewhere else, that you can restore even if the original system is gone.

Snapshot Backup
Stored where Same storage as the source Separate system or location
Speed Seconds Minutes to hours
Survives storage failure No Yes
Survives host or account loss No Yes, if offsite
Retention Short (hours to days) Days to months
Best for Rollback before a change Disaster recovery, long-term history

Snapshots also have caveats. A snapshot of a running database is only crash-consistent unless the application is quiesced, so a MySQL server may need crash recovery when you restore it. Long-lived snapshots on VMware-based platforms can also slow down disk I/O as the delta grows. Use them for short windows, and rely on application-aware backups for the long term.

What to Back Up

Before writing scripts, list what you would need to rebuild the server from scratch:

You do not need to back up the OS itself if you can reprovision it quickly and reapply configuration. Many operators back up /etc and data, and keep a short rebuild document.

MySQL Backup Script

Use mysqldump with --single-transaction for InnoDB tables. It takes a consistent snapshot of the data without locking tables. Note that it does not give consistency for MyISAM tables; convert them to InnoDB if you can.

First, store credentials in a root-only file so passwords never appear in process lists:

# /root/.my.cnf  (chmod 600)
[client]
user=backup
password=CHANGE_ME

Create a limited user:

CREATE USER 'backup'@'localhost' IDENTIFIED BY 'CHANGE_ME';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS ON *.* TO 'backup'@'localhost';

Then the script:

#!/usr/bin/env bash
# /usr/local/sbin/mysql-backup.sh
set -euo pipefail
umask 077

BACKUP_DIR=/var/backups/mysql
KEEP_DAYS=7
STAMP=$(date +%F_%H%M)
CNF=/root/.my.cnf

mkdir -p "$BACKUP_DIR"

for db in $(mysql --defaults-extra-file="$CNF" -N -e "SHOW DATABASES" \
  | grep -Ev '^(information_schema|performance_schema|sys|mysql)$'); do
  mysqldump --defaults-extra-file="$CNF" \
    --single-transaction --quick --routines --triggers --events \
    "$db" | gzip -9 > "$BACKUP_DIR/${db}_${STAMP}.sql.gz"
done

find "$BACKUP_DIR" -type f -name '*.sql.gz' -mtime +"$KEEP_DAYS" -delete

pipefail matters here: without it, a failed mysqldump piped into gzip would still exit successfully and leave you with an empty-looking but valid archive.

File and Config Backup with Restic

For files, a deduplicating tool such as restic (or BorgBackup) is better than plain tar or rsync because it keeps multiple versions, encrypts data client-side, and handles retention. Restic can write straight to a remote server over SFTP.

On the production server, create a dedicated SSH key and initialize the repository on the backup host (example address from the documentation range):

ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
export RESTIC_REPOSITORY=sftp:backup@192.0.2.50:/srv/restic/web01
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic init

Store the repository password somewhere outside the server as well. If the server dies and the password was only on it, the backup is useless.

The backup script:

#!/usr/bin/env bash
# /usr/local/sbin/files-backup.sh
set -euo pipefail

export RESTIC_REPOSITORY=sftp:backup@192.0.2.50:/srv/restic/web01
export RESTIC_PASSWORD_FILE=/root/.restic-pass

/usr/local/sbin/mysql-backup.sh

restic backup /etc /var/www /var/backups/mysql /home \
  --exclude-caches --exclude '*.log'

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic check --read-data-subset=5%

Schedule it with cron or a systemd timer:

# /etc/cron.d/backup
30 3 * * * root /usr/local/sbin/files-backup.sh >> /var/log/backup.log 2>&1

Remote Backup Done Right

The offsite copy is where most strategies quietly fail. Check these points:

Test Your Restores

Run a restore test on a schedule, not only after an incident. A minimal automated check:

#!/usr/bin/env bash
set -euo pipefail
export RESTIC_REPOSITORY=sftp:backup@192.0.2.50:/srv/restic/web01
export RESTIC_PASSWORD_FILE=/root/.restic-pass

TMP=$(mktemp -d)
restic restore latest --target "$TMP" --include /var/backups/mysql

LATEST=$(ls -t "$TMP"/var/backups/mysql/*.sql.gz | head -n1)
gunzip -t "$LATEST"

mysql -e "DROP DATABASE IF EXISTS restore_test; CREATE DATABASE restore_test;"
gunzip -c "$LATEST" | mysql restore_test
mysql -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='restore_test';"

rm -rf "$TMP"

Better still, restore to a separate scratch VM and start the actual application against it. Once or twice a year, do a full rebuild drill: provision a clean server, restore, and time it. That time is your real recovery time, and it is usually longer than people assume.

Common Mistakes and Fixes

Putting It Together

A reasonable baseline for most servers: a nightly database dump plus a file backup to a remote repository, a snapshot before every upgrade or risky change, retention of daily, weekly, and monthly versions, alerting on missed runs, and a restore test every month. Host the backup target on a separate machine in a different location from production. Backups protect data, but not uptime, so combine them with DDoS protection and firewall rules to reduce the chance you need to restore in the first place.

Frequently asked questions

What is the 3-2-1 backup rule?

Keep three copies of your data (the live data and two backups), store them on two different types of storage or systems, and keep one copy offsite. It protects against disk failure, host loss, and site-level incidents.

Is a snapshot a backup?

No. A snapshot usually lives on the same storage as the original, so it is lost if that storage or the host fails. It is great for short-term rollback before a change, but a real backup must be independent and stored elsewhere.

How often should I run MySQL backups?

Nightly dumps suit many workloads. If you cannot afford to lose a day of data, add binary log archiving for point-in-time recovery or run dumps more frequently. Choose the interval based on how much data loss you can accept.

How often should I test restores?

Run an automated restore check at least monthly, and do a full rebuild drill on a clean server once or twice a year. Also test after any change to the backup tooling, credentials, or storage target.

Need a separate server for offsite backups? Explore Nixcon VDS and dedicated.Learn more