
Backups don’t count until you’ve restored them. A Linux VPS disaster recovery plan turns “we have snapshots” into a repeatable drill. You rebuild from scratch, verify the data, and move traffic with a clear rollback.
This guide stays hands-on. You’ll set recovery objectives, automate VPS backups, run a full restore test, and prepare a DNS failover playbook.
Examples use Ubuntu 24.04 LTS and AlmaLinux 9/10. The same process works for most Linux stacks (Nginx/Apache, PHP, MySQL/MariaDB, PostgreSQL, WordPress).
What you’re building (and what “done” looks like)
A workable plan delivers three things:
- You can rebuild fast: OS + firewall + web server + app config come back the same way every time.
- You can restore cleanly: files + databases + secrets restore, then pass basic sanity checks.
- You can cut over traffic safely: DNS changes are planned, reversible, and practiced.
If you’re still at the “brand new VPS” stage, skim HostMyCode’s Linux VPS setup checklist first. Then return here.
Set realistic RPO/RTO for a VPS (and write them down)
Before you touch tooling, decide what you’re protecting. Then decide what you can tolerate losing.
- RPO (Recovery Point Objective): maximum data loss. Example: 15 minutes for orders, 24 hours for a brochure site.
- RTO (Recovery Time Objective): maximum downtime. Example: 30–60 minutes for a small WooCommerce store, 4 hours for a personal blog.
Put these in a short runbook. Store it off-server (password manager, team wiki, private Git repo).
During an incident, teams waste time debating what “acceptable” means.
# DR objectives
Site: example.com
RPO: 1 hour
RTO: 60 minutes
Primary region: VPS-1
Recovery target: VPS-DR
Inventory your stack: what must be backed up
Recoveries often fail because of a missing “small” dependency. Make a concrete list.
Update it after every meaningful change.
- System config: /etc (Nginx/Apache, PHP-FPM, cron, systemd units)
- App files: /var/www, uploads, .env files (or equivalents)
- Databases: MySQL/MariaDB or PostgreSQL dumps (and optionally WAL/binlog strategy)
- Secrets: TLS certs, API keys, SMTP creds, SSH keys, wp-config.php salts
- DNS: zone records, TTL values, and where they’re managed
- Email (if self-hosted): mailboxes, DKIM keys, and the queue (if relevant)
If you self-host mail, treat it as its own project. This tutorial maps what lives on disk: Postfix + Dovecot mail server setup on Ubuntu 24.04 (2026).
Pick a backup approach: snapshots + file backups (you want both)
For most VPS setups, the best balance looks like this:
- Snapshots for quick rollback after a bad update or deployment. They’re fast, but not always portable.
- File-level backups (encrypted, off-server) for actual disaster recovery. You can restore them onto a clean VPS.
HostMyCode customers commonly run production on a HostMyCode VPS. They use snapshots as the “fast undo” layer.
They also keep encrypted off-box backups as the “rebuild anywhere” layer.
If you already run restic, this workflow will feel familiar. For a deeper restic + S3 setup, keep this guide handy: Linux VPS backup strategy with restic + S3 (2026).
Step 1 — Create a dedicated backup user and lock down access
Don’t run routine backup jobs as root unless you have no choice. A dedicated user gives tighter permissions and cleaner auditing.
# Ubuntu / Debian
sudo adduser --disabled-password --gecos "" backup
sudo usermod -aG www-data backup
# AlmaLinux / Rocky
sudo useradd -m -s /bin/bash backup
sudo usermod -aG apache backup
Give the user read access to the web root and the config paths you plan to capture.
If you must include /etc, prefer scoped sudo for specific commands. Avoid full root shell access.
Step 2 — Back up web files and config with tar + zstd (fast and predictable)
tar + zstd is boring in the best way. It’s common, fast, and easy to restore.
Start with a local backup directory. Then sync it off-server.
sudo mkdir -p /var/backups/dr
sudo chown backup:backup /var/backups/dr
Create an archive. Adjust paths for your stack:
sudo -u backup bash -lc '
TS=$(date -u +%Y%m%dT%H%M%SZ)
OUT=/var/backups/dr/files-$TS.tar.zst
tar --zstd -cf "$OUT" \
/var/www \
/etc/nginx /etc/apache2 /etc/httpd \
/etc/php /etc/php-fpm.d /etc/php.d \
/etc/letsencrypt \
/etc/cron.d /var/spool/cron \
/etc/systemd/system \
2>/var/backups/dr/files-$TS.warnings.log
ls -lh "$OUT"
'
Pitfall: don’t blindly scoop up all of /etc on multi-tenant servers. Keep the DR set narrow and intentional, especially around secrets.
Step 3 — Back up databases (MySQL/MariaDB and PostgreSQL)
File backups without consistent database dumps often produce a “restored” site that acts corrupted.
Make DB exports part of every run.
MySQL / MariaDB: consistent dump with mysqldump
Create a dedicated DB user. Grant only the privileges needed for dumps.
mysql -u root -p -e "CREATE USER 'backup'@'localhost' IDENTIFIED BY 'REPLACE_ME';"
mysql -u root -p -e "GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES ON *.* TO 'backup'@'localhost';"
mysql -u root -p -e "FLUSH PRIVILEGES;"
Then generate the dump:
sudo -u backup bash -lc '
TS=$(date -u +%Y%m%dT%H%M%SZ)
OUT=/var/backups/dr/mysql-$TS.sql.zst
mysqldump --single-transaction --routines --triggers --events \
-ubackup -p"REPLACE_ME" --all-databases \
| zstd -T0 -19 -o "$OUT"
ls -lh "$OUT"
'
PostgreSQL: pg_dumpall or per-db pg_dump
For single-server recovery, pg_dumpall is usually the easiest.
For larger instances, dump per database with pg_dump.
sudo -u postgres createuser backup
sudo -u postgres psql -c "ALTER USER backup WITH PASSWORD 'REPLACE_ME';"
sudo -u postgres psql -c "GRANT pg_read_all_data TO backup;"
sudo -u backup bash -lc '
TS=$(date -u +%Y%m%dT%H%M%SZ)
OUT=/var/backups/dr/postgres-$TS.sql.zst
PGPASSWORD="REPLACE_ME" pg_dumpall -h 127.0.0.1 -U backup \
| zstd -T0 -19 -o "$OUT"
ls -lh "$OUT"
'
Verification step: confirm the dump is real. It should be more than a few lines.
It also shouldn’t look like an HTML error page.
Step 4 — Encrypt and copy backups off the VPS
Off-server storage is non-negotiable. If the VPS is gone, anything stored only on that disk is gone too.
Two common choices:
- Object storage (S3-compatible): durable, and lifecycle/retention policies are easy to apply.
- Second VPS “vault”: rsync over SSH to a locked-down storage box.
The example below uses rsync-to-vault. It’s simple and easy to restore from.
If you prefer object storage, use restic or rclone. Pair it with server-side lifecycle rules.
Option A: rsync to a backup vault VPS
Provision a small second VPS in a different location. Keep it minimal: SSH, firewall, storage.
On HostMyCode, a separate managed VPS hosting plan can cover patching and baseline hardening. That leaves you to focus on restore and cutover procedures.
On the vault server, create a restricted user and directory:
sudo adduser --disabled-password --gecos "" vault
sudo mkdir -p /srv/vault/example-com
sudo chown vault:vault /srv/vault/example-com
On the primary server, generate a key for the backup user:
sudo -u backup ssh-keygen -t ed25519 -f /home/backup/.ssh/id_ed25519 -N ""
Install the public key on the vault user. Then run rsync:
sudo -u backup rsync -a --delete \
-e "ssh -i /home/backup/.ssh/id_ed25519" \
/var/backups/dr/ vault@VAULT_IP:/srv/vault/example-com/
Pitfall: don’t enable --delete until you’ve triple-checked the destination path. One typo can wipe the wrong directory.
Step 5 — Automate with systemd timers (cleaner than cron for DR jobs)
systemd timers make job status and logs easier to inspect. Start with a script you can run by hand.
sudo tee /usr/local/sbin/dr-backup.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
TS=$(date -u +%Y%m%dT%H%M%SZ)
DIR=/var/backups/dr
mkdir -p "$DIR"
# Files + config
OUT_FILES="$DIR/files-$TS.tar.zst"
tar --zstd -cf "$OUT_FILES" \
/var/www \
/etc/nginx /etc/apache2 /etc/httpd \
/etc/php /etc/php-fpm.d /etc/php.d \
/etc/letsencrypt \
/etc/cron.d /var/spool/cron \
/etc/systemd/system \
2>"$DIR/files-$TS.warnings.log" || true
# MySQL (skip if not installed)
if command -v mysqldump >/dev/null 2>&1; then
OUT_MY="$DIR/mysql-$TS.sql.zst"
mysqldump --single-transaction --routines --triggers --events \
-ubackup -p"${MYSQL_BACKUP_PASSWORD:-}" --all-databases \
| zstd -T0 -19 -o "$OUT_MY"
fi
# PostgreSQL (skip if not installed)
if command -v pg_dumpall >/dev/null 2>&1; then
OUT_PG="$DIR/postgres-$TS.sql.zst"
PGPASSWORD="${PG_BACKUP_PASSWORD:-}" pg_dumpall -h 127.0.0.1 -U backup \
| zstd -T0 -19 -o "$OUT_PG"
fi
# Retention (local): keep 7 days
find "$DIR" -type f -mtime +7 -name '*.zst' -delete
find "$DIR" -type f -mtime +7 -name '*.log' -delete
EOF
sudo chmod 750 /usr/local/sbin/dr-backup.sh
Create an environment file for secrets. Then lock it down:
sudo tee /etc/dr-backup.env > /dev/null <<'EOF'
MYSQL_BACKUP_PASSWORD=REPLACE_ME
PG_BACKUP_PASSWORD=REPLACE_ME
EOF
sudo chmod 600 /etc/dr-backup.env
Create the service and timer:
sudo tee /etc/systemd/system/dr-backup.service > /dev/null <<'EOF'
[Unit]
Description=Disaster recovery backup job
[Service]
Type=oneshot
User=backup
Group=backup
EnvironmentFile=/etc/dr-backup.env
ExecStart=/usr/local/sbin/dr-backup.sh
EOF
sudo tee /etc/systemd/system/dr-backup.timer > /dev/null <<'EOF'
[Unit]
Description=Run DR backups every night
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now dr-backup.timer
sudo systemctl status dr-backup.timer
Verification step:
sudo systemctl start dr-backup.service
sudo journalctl -u dr-backup.service --no-pager -n 100
ls -lh /var/backups/dr | tail
If you already ship logs off-box, you’ll catch backup failures earlier. For that setup, see Linux VPS log shipping with rsyslog (2026).
Step 6 — Run a full restore test on a clean recovery VPS
This is the step people skip. It’s also the step that saves you during an outage.
Restore tests expose missing files, wrong ownership, forgotten firewall rules, and bad assumptions about DNS.
Bring up a fresh recovery server in the same OS family as production (Ubuntu 24.04 LTS or AlmaLinux 9/10).
To reduce scramble later, keep a DR HostMyCode VPS patched and ready for cutover.
6.1 Install your base packages
Ubuntu example:
sudo apt update
sudo apt -y install nginx php-fpm php-mysql php-cli mariadb-server \
unzip zstd rsync
AlmaLinux/Rocky example (package names vary by repo choices):
sudo dnf -y install nginx php-fpm php-mysqlnd mariadb-server \
unzip zstd rsync
6.2 Copy backups from the vault
sudo mkdir -p /var/backups/dr
sudo rsync -a vault@VAULT_IP:/srv/vault/example-com/ /var/backups/dr/
Select the newest archive and dump:
ls -1 /var/backups/dr/files-*.tar.zst | tail -n 1
ls -1 /var/backups/dr/mysql-*.sql.zst | tail -n 1
6.3 Restore files and config
Restore to root because the archive includes system paths:
sudo tar --zstd -xpf /var/backups/dr/files-REPLACE.tar.zst -C /
Common pitfall: WordPress uploads and ownership. After the restore, set ownership for your web user:
# Ubuntu/Debian typical
sudo chown -R www-data:www-data /var/www
# AlmaLinux/Rocky typical
sudo chown -R apache:apache /var/www
6.4 Restore MySQL/MariaDB
sudo systemctl enable --now mariadb
zstd -dc /var/backups/dr/mysql-REPLACE.sql.zst | sudo mysql
Verify databases exist:
sudo mysql -e "SHOW DATABASES;"
6.5 Restore PostgreSQL (if used)
sudo systemctl enable --now postgresql
zstd -dc /var/backups/dr/postgres-REPLACE.sql.zst | sudo -u postgres psql
Verify:
sudo -u postgres psql -c "\l"
6.6 Bring up web services and validate locally
sudo systemctl enable --now nginx
sudo nginx -t
sudo systemctl reload nginx
Test with curl and force the Host header (use your domain):
curl -I http://127.0.0.1 -H 'Host: example.com'
If this is WordPress, confirm wp-config.php points at the local DB host. Also confirm it includes salts/keys.
Then perform one real action (login, submit a form, place a test order). Read the logs while you do it.
Step 7 — Prepare DNS cutover with low TTL and a rollback path
DNS is part of the plan. Treat it like one.
Two habits keep cutovers predictable:
- Lower TTL before an incident (e.g., 60–300 seconds) for records you might flip.
- Document rollback (old IPs, old records, and what propagation typically looks like for your users).
If your domain registrar and DNS provider are different, you have one more place to troubleshoot during an outage.
HostMyCode customers often keep both together to reduce moving parts: domains and DNS at HostMyCode.
Verification step: check resolution from multiple resolvers before and after:
dig +short A example.com @1.1.1.1
dig +short A example.com @8.8.8.8
Rollback note: keep the previous A/AAAA values in your runbook. In a live incident, people overwrite history and then guess.
Step 8 — Add “failure modes” to your runbook (so you don’t improvise)
Most outages repeat a few patterns. Write down what you’ll do for each one.
Include the “stop digging” point. That’s when you switch from repair to rebuild.
- Bad deploy / config change: roll back from snapshot, or restore last-known-good archive to the same VPS.
- Disk corruption / full disk: restore onto a fresh VPS; don’t spend hours trying to rehab a failing disk.
- Compromise suspicion: rebuild from a clean OS, rotate passwords/keys, restore only verified content.
- Provider/network outage: DNS cutover to DR VPS.
Two internal references help you avoid duplicating work:
- VPS hosting security checklist for 2026 (use it to define your “clean rebuild” baseline).
- VPS migration checklist (near-zero downtime) (many steps mirror DR cutover discipline).
Step 9 — Make restore testing a recurring task
DR plans rot quietly. Plugins change, PHP moves forward, and configs drift.
Over time, the “simple restore” stops being simple.
Put restore testing on the calendar:
- Monthly: restore to a staging VPS, verify login + one transaction (order, form submit, admin change).
- Quarterly: DNS cutover rehearsal with a low-risk subdomain (e.g., dr.example.com).
- After major changes: new control panel, major PHP upgrade, DB migration, or new caching layer.
Also keep log growth under control. Otherwise you can lose a server to “disk full” before DR even comes into play.
If you haven’t tuned rotation, use this: Linux VPS log rotation setup (2026).
Quick troubleshooting: the restore worked, but the site is broken
- 502/504 errors: check PHP-FPM socket path and pool config. Verify
systemctl status php8.3-fpm(Ubuntu) or your distro’s PHP-FPM unit. - Wrong redirects: WordPress
siteurlandhomein the DB may still point to the old scheme/host. Also check Nginx/Apache vhost configs. - Missing media: permissions or an incomplete /var/www restore. Confirm file counts and ownership under
wp-content/uploads. - SSL errors: validate
/etc/letsencryptrestored and Nginx/Apache points to the right cert paths. Runnginx -torapachectl configtest. - DB login failures: confirm DB users/passwords match what the app config expects, and that the DB service is actually running.
Summary: your Linux VPS disaster recovery plan, operationalized
If you followed the steps above, you now have a Linux VPS disaster recovery plan you can execute.
It includes consistent file + DB backups, off-server copies, a repeatable restore test, and a DNS cutover runbook with rollback.
Keep the runbook current as your stack changes. Keep testing restores so you’re not learning under pressure.
If you want a stable foundation for both production and recovery servers, start with a HostMyCode VPS. Consider managed VPS hosting if you’d rather hand off patching and baseline hardening while you own the procedures.
DR is much easier to run when your servers are consistent and quick to redeploy. HostMyCode provides reliable VPS hosting for both production and recovery targets, plus managed VPS hosting if you want routine maintenance and security updates handled for you.
FAQ
How often should I test restores for a VPS?
Monthly is a solid baseline for most workloads. If you run eCommerce or ship frequent updates, test after major releases and at least monthly.
Are VPS snapshots enough for disaster recovery?
No. Snapshots help with fast rollback, but a real DR plan also needs portable, off-server backups you can restore onto a fresh VPS.
What’s the simplest DNS failover approach for small sites?
Lower TTL ahead of time, keep a standby VPS ready, then update A/AAAA records during an incident. Document the previous IP for rollback.
Should I back up /etc/letsencrypt?
Yes, if you want faster restores. It prevents you from scrambling to re-issue certificates during an outage. Secure off-server backups carefully because private keys are sensitive.
What’s the fastest way to validate a restore worked?
Run nginx -t, confirm the DB is reachable, then perform one real application action (login, submit a form, place a test order) and review logs for errors.