Disaster Recovery Planning: RTO, RPO, Runbooks, and Actually Testing Your Backups
A backup you haven’t tested is not a backup. It’s a hope.
That distinction matters enormously at 2am when your primary database is gone, your storage array is throwing errors you’ve never seen before, and you’re trying to remember if you ever actually verified that the weekly backup job ran successfully. Most people have backups configured. Very few have disaster recovery.
Backups are an input. Disaster recovery is the process — the documented, tested, rehearsed sequence of steps that gets your systems back to a known-good state within a time window your organization can survive. This guide covers what that actually means: how to define your recovery targets, classify your systems, build real runbooks, inject controlled failures through chaos engineering, and most importantly, test your restores before you need them.
The Uncomfortable Truth About Outages
Let’s talk about what an outage actually costs before we get into the mechanics.
For a business, the math is direct: revenue per hour multiplied by duration, plus staff time chasing the incident, plus the longer-tail cost of customer trust lost. For a homelab or self-hosted setup, the math is different but still real: your weekend, your family’s patience with “I just need to fix one more thing,” the media server that’s down while guests are over, the Home Assistant automations that stopped working three days before anyone noticed.
Real disaster scenarios that happen to real people:
- Ransomware: A single phishing email or unpatched vulnerability encrypts everything attached to your network, including your NAS and any backup destination mounted as a network share
- Hardware failure: A drive dies in a RAID array that was already degraded, or a controller card fails and takes all drives with it
- Accidental deletion:
rm -rf /dataaimed at the wrong directory, aDROP TABLErun against production instead of staging, a misconfigured Ansible playbook that wipes configs - Datacenter/provider outage: Your VPS provider goes dark for 6 hours, or your home loses power for 3 days after a storm
- Filesystem corruption: Power loss during a write, a kernel bug, or a hardware glitch leaves your ext4 or ZFS pool unclean
- Bad deployment: A code change or config update makes your service fail in a way that corrupts data before anyone notices
The difference between a disaster and an incident is whether you have a plan. An incident is something you solve. A disaster is something that happens to you while you scramble.
RTO and RPO: The Two Numbers That Define Your Strategy
Every conversation about disaster recovery eventually arrives at two acronyms. They look similar but measure fundamentally different things.
RPO — Recovery Point Objective is the maximum amount of data loss you can tolerate, expressed as time. If your RPO is 1 hour, you can afford to lose up to 1 hour of data. This directly drives your backup frequency: your most recent backup must be no older than your RPO.
RTO — Recovery Time Objective is the maximum amount of time your system can be unavailable before the impact becomes unacceptable. If your RTO is 4 hours, you need to be fully operational within 4 hours of declaring a disaster.
These numbers are not technical decisions. They are business decisions that have technical and financial consequences.
The Cost Curve
Achieving lower RPO and RTO costs money — not linearly, but exponentially:
| RPO Target | What It Requires | Approximate Approach |
|---|---|---|
| ~0 seconds | Synchronous replication, active-active | Very expensive, complex |
| 5-15 minutes | Async streaming replication, frequent snapshots | Moderate cost |
| 1 hour | Hourly snapshots, WAL shipping | Reasonable |
| 24 hours | Daily backups | Minimal |
| RTO Target | What It Requires | Approach |
|---|---|---|
| < 5 minutes | Hot standby, active-active, automatic failover | Very expensive |
| 15-60 minutes | Warm standby, semi-automated failover | Moderate |
| 1-4 hours | Snapshot restore, manual failover | Reasonable |
| 4-24 hours | Cold restore from backup | Minimal |
| > 24 hours | Tape, offline backups, manual rebuild | Lowest cost |
The combination of these targets drives your entire architecture. A RPO of 0 and RTO of 5 minutes for every system would be prohibitively expensive for almost any organization. The discipline is being honest about which systems actually need which targets.
System Tiers: Matching Recovery Targets to Criticality
Not all systems are equal. Classify them:
| Tier | Name | RTO | RPO | Examples |
|---|---|---|---|---|
| 0 | Critical Infrastructure | < 15 min | < 5 min | DNS, authentication, monitoring |
| 1 | Critical Applications | < 1 hour | < 15 min | Primary databases, customer-facing apps |
| 2 | Important Applications | < 4 hours | < 1 hour | Internal tools, secondary services |
| 3 | Standard | < 24 hours | < 4 hours | Dev environments, internal wikis |
| 4 | Low Priority | Best effort | 24 hours | Archives, test systems, batch jobs |
There’s an uncomfortable irony in Tier 0: it includes DNS, authentication, and monitoring — the very things you need to recover everything else. If your monitoring system goes down in a disaster, you lose visibility. If your auth server is gone, you can’t log into other systems. These need to be recovered first and require special handling.
Applying this to a homelab:
- Tier 0: Pi-hole/AdGuard (DNS), Authelia/authentik (auth), Uptime Kuma (monitoring)
- Tier 1: Home Assistant, primary file storage, media server database
- Tier 2: Git server, password manager sync, personal wiki
- Tier 3: Dev VMs, experimental containers
- Tier 4: Build artifacts, downloaded ISOs, cached data
Classifying Your Systems and Mapping Dependencies
Before you can recover, you need to know what you have. Create a service inventory. For each service, document:
- Name and description
- Where it runs (host, VM, container, cloud)
- Data it owns (databases, volumes, config files)
- Upstream dependencies (what it needs to function)
- Downstream dependencies (what breaks if this goes down)
- Tier classification
- Backup location and schedule
- Responsible owner
Keep this in a simple markdown table or spreadsheet. Keep it in git. The only inventory that matters is one that’s current.
Dependency mapping matters more than people realize. A service failure cascades through dependencies in ways that aren’t obvious until they happen. If your Redis cache goes down and your application doesn’t handle cache misses gracefully, your app is effectively down even though nothing touched it directly. If your Vault server is unavailable and your apps fetch secrets at startup, restarting a container causes it to fail even though the original problem had nothing to do with it.
Draw the dependency graph. For each system, ask: “If this goes down right now, what else breaks within 5 minutes? Within an hour?”
Backup Strategies by Data Type
Different data types have different backup tools. Using the right tool matters.
Databases
PostgreSQL
For small to medium databases, pg_dump is the baseline. It produces a logical dump — SQL statements that can restore the database on any Postgres version:
|
|
For production workloads, pg_basebackup creates a physical backup of the entire cluster and can be combined with WAL archiving for point-in-time recovery (PITR):
|
|
With WAL archiving enabled, you can restore to any point in time — not just when you took the backup. This is how you recover from “someone ran DELETE without WHERE at 3:47pm and we need everything as it was at 3:46pm.”
MySQL / MariaDB
|
|
For hot backups without locking, Percona XtraBackup is the standard for InnoDB:
|
|
Redis
Redis has two persistence modes. RDB (Redis Database) is point-in-time snapshots. AOF (Append Only File) logs every write operation and survives crashes better:
|
|
For DR purposes, treat Redis as cache-tier unless your application absolutely depends on its data surviving a restart. If it’s storing sessions, queues, or rate limit state that must survive, treat it like a database.
MongoDB
|
|
Replica sets are also a form of DR for MongoDB — a secondary can be promoted in seconds if the primary fails.
Files and Object Storage with Restic
Restic is the best general-purpose backup tool for files. It’s encrypted at rest, deduplicated (so backing up the same files again costs almost nothing in storage), supports multiple backends, and has a consistent interface regardless of where you’re storing data.
|
|
A production-ready Restic backup script with retention:
|
|
Restic backends:
local:/path/to/dir— local filesystem or NAS mountsftp:user@host:/path— SSH/SFTP (great for a second machine)b2:bucket:path— Backblaze B2s3:s3.amazonaws.com/bucket— AWS S3 or compatible (Wasabi, MinIO)rclone:remote:path— any rclone-supported backend
rclone for cloud sync:
|
|
VM and Container State
Proxmox vzdump:
|
|
Proxmox Backup Server (PBS) adds deduplication, encryption, and incremental backups on top of vzdump — if you have a Proxmox cluster, PBS is worth setting up as your primary backup target.
Docker volume backups:
Named volumes don’t live in a path you can just rsync. The standard pattern for backing them up:
|
|
Configuration and Secrets: Cattle, Not Pets
If your configuration lives in git and your secrets are in a secrets manager, you don’t need to back up the VM. You can rebuild it from scratch in minutes. This is the “cattle, not pets” principle applied to disaster recovery.
SOPS for encrypted secrets in git:
|
|
HashiCorp Vault snapshots:
|
|
The 3-2-1-1-0 Backup Rule
The classic 3-2-1 rule says: keep 3 copies of your data, on 2 different storage media, with 1 copy offsite.
The extended 3-2-1-1-0 rule adds two more requirements: 1 copy offline or air-gapped (protecting against ransomware that encrypts everything reachable on the network), and 0 errors — meaning you verify your backups actually work.
In practice for a homelab:
- Copy 1: Live data on your primary storage
- Copy 2: Local NAS or backup drive (different media)
- Copy 3: Cloud storage (Backblaze B2, Wasabi, S3) — offsite
- Copy 4: Offline encrypted drive rotated periodically — air-gapped, ransomware-proof
Immutable backups against ransomware:
Backblaze B2 and AWS S3 both support Object Lock, which makes objects immutable for a specified period. Even if ransomware compromises your machine and has valid API credentials, it cannot delete or overwrite locked objects:
|
|
With a 30-day retention lock, an attacker who compromises your credentials can still write new objects but cannot delete objects that are less than 30 days old. Your backups survive.
Writing Runbooks
A runbook is a documented, step-by-step procedure for responding to a specific failure scenario. It is not a general guide. It is not a “how Postgres works” explainer. It is: here is the exact situation, here are the exact steps to execute, here is how you verify you’re done.
Anatomy of a Good Runbook
Every runbook should have the same structure:
# Runbook: [Descriptive Title of the Failure Scenario]
## Metadata
- Severity: [Critical / High / Medium / Low]
- Service: [What service is affected]
- Owner: [Team or person responsible]
- Last reviewed: [Date]
- Last tested: [Date and by whom]
## Impact
[What the user-visible impact is. Be specific: "Users cannot log in" not "auth is down"]
## Detection
[How you know this situation is happening. Alert name, dashboard link, error message]
## Prerequisites
- [ ] Access to [specific system] via [method]
- [ ] [Tool] installed on your workstation
- [ ] [Credentials/keys] from [location]
## Recovery Procedure
### Step 1: [First action]
[Explanation of why this step comes first]
```bash
command to run
Expected output: [what you should see]
Step 2: [Next action]
…
Verification
[Specific checks that confirm recovery is complete]
|
|
Expected: HTTP 200 with {"status": "ok"}
Escalation
If recovery fails after [X] minutes, contact [person/team] via [method].
Post-Incident
- Update this runbook with any gaps found
- File an incident report
- Schedule a postmortem if severity >= High
### Runbook: Database Corruption
Runbook: PostgreSQL Database Corruption / Data Loss
Metadata
- Severity: Critical
- Service: PostgreSQL (myapp database)
- Owner: Platform team
- Last reviewed: 2026-03-25
- Last tested: 2026-02-15 by jmoon
Impact
Application is throwing database errors. Data may be missing or inconsistent. Users may see errors, missing content, or failed transactions.
Detection
- Alert: “Postgres query errors > 5% for 5 minutes”
- OR: Application logs showing: ERROR: invalid page in block X of relation
- OR: pg_catalog.pg_stat_database.checksum_failures > 0
Prerequisites
- SSH access to db-primary.internal
- psql access as postgres user (or pgpass configured)
- Access to /backups/postgres/ on backup NAS
- Downtime window approved (coordinate with team in #incidents)
Recovery Procedure
Step 1: Stop the application (prevent further writes to corrupted data)
|
|
Verify no connections to the database (except your own):
|
|
Step 2: Identify the extent of corruption
|
|
Step 3: Identify the last good backup
|
|
If using Restic: restic snapshots --tag postgres --repo /mnt/backup/restic
Step 4: Create a snapshot of current (corrupted) state
|
|
Step 5: Drop and recreate the database
|
|
Step 6: Restore from backup
|
|
Watch for errors in the output. Warnings about ownership are usually safe to ignore.
Step 7: Verify data integrity
|
|
Step 8: Restart the application
|
|
Verification
- Application health endpoint returns 200:
curl -f https://myapp.example.com/health - Login works end-to-end
- Data from before the corruption point is present
- Error rate in monitoring has dropped to baseline
Post-Incident
- Identify the root cause of corruption (hardware? bug? power loss?)
- Calculate actual data loss window (time of last good backup to incident)
- Notify affected users if data was lost
- Update runbook with anything that was unclear
### Runbook: VM Host Failure
Runbook: Proxmox Host Node Failure
Metadata
- Severity: Critical
- Service: All VMs on affected Proxmox node
- Last tested: 2026-01-20
Impact
All virtual machines on [failed-node] are unavailable. Depending on which VMs are affected, multiple downstream services may be down.
Detection
- Alert: “Host failed-node unreachable for > 5 minutes”
- OR: VMs not responding to ping/HTTP checks
- OR: Proxmox cluster shows node as offline
Prerequisites
- Access to Proxmox web UI or pve node CLI (alternate node)
- Access to Proxmox Backup Server or NAS backup storage
- List of VMs on the failed node and their IDs (from CMDB or
pvesh get /nodes/failed-node/qemu)
Recovery Procedure
Step 1: Confirm the failure and identify affected VMs
|
|
Step 2: For each critical VM — restore from latest backup
|
|
Step 3: Restore in tier order
Restore Tier 0 services first (DNS, auth, monitoring), then Tier 1, etc. Do not move to the next tier until the current tier’s services are verified.
Step 4: Update DNS / load balancer records if IPs changed
|
|
Step 5: Notify users
Post in #status channel: “We are experiencing an infrastructure incident. [Affected services] are being restored. ETA: [X] minutes.”
Verification
For each restored VM:
- SSH access works
- Service is running (
systemctl is-active <service>) - Application responds to health checks
- Metrics appear in Grafana
### Runbook: Ransomware Response
Runbook: Ransomware Incident Response
Metadata
- Severity: Critical — treat as a security incident, not just an outage
- This runbook assumes ransomware has been confirmed or is strongly suspected
STOP. READ THIS FIRST.
Do NOT attempt to recover data or remove the ransomware while affected systems are still on the network. Isolation comes first, always.
Step 1: Isolate immediately
|
|
Step 2: Identify the blast radius
- Which systems are encrypted or behaving abnormally?
- What shares, volumes, and backup destinations were mounted on affected systems?
- Timeline: when did the behavior start? (check logs, file modification timestamps)
Step 3: DO NOT REBOOT affected systems yet
Rebooting may destroy forensic evidence. Capture volatile data first if you have the skills:
|
|
Step 4: Identify last clean offline backup
Your offline/air-gapped backup is your lifeline here. Network-connected backups may also have been encrypted if they were reachable.
|
|
Step 5: Restore from clean backup
- Wipe and rebuild affected systems from scratch (do not trust anything that was running)
- Restore data only from backups predating the infection timeline
- Verify restored data before bringing systems online
Step 6: Rotate ALL credentials
Everything that was accessible from compromised systems must be treated as compromised:
- All SSH keys
- All API tokens and service account passwords
- All secrets in Vault or similar (rotate before restoring from Vault snapshot)
- Cloud provider credentials
- Database passwords
- Any credentials stored in config files
Step 7: Harden before reconnecting
- Patch the vulnerability that was exploited (if known)
- Review firewall rules — what should not have been reachable?
- Enable network monitoring before bringing systems back online
Post-Incident
- Full incident timeline documentation
- Root cause analysis: how did attacker gain access?
- Regulatory/legal notification if personal data was involved
- Blameless postmortem with the team
### Where to Store Runbooks
Your runbooks must be accessible when your systems are down. That means:
- **Primary**: In git, in the same repository as your infrastructure code
- **Secondary**: A wiki platform (Notion, Confluence, Obsidian + sync) accessible from outside your infrastructure
- **Tertiary**: Printed or exported to PDF on a USB drive in your server rack
The **2am test**: Hand your runbook to a competent engineer who didn't write it. Can they execute it successfully at 2am, half-asleep, without asking you a single question? If not, it needs more detail.
---
## Chaos Engineering: Finding Weaknesses Before Production Does
Chaos engineering is the practice of intentionally introducing failures into your systems to verify they behave as expected and to discover weaknesses before an unplanned outage does it for you.
Netflix famously ran Chaos Monkey in production, randomly terminating instances to prove their services could survive it. For most organizations, controlled fault injection in a staging or homelab environment is the more appropriate starting point.
The core principle: **the only way to know your system survives a failure is to make it fail on your terms.**
### Tools
**Chaos Mesh** (Kubernetes-native)
Install via Helm:
```bash
helm repo add chaos-mesh https://charts.chaos-mesh.org
helm install chaos-mesh chaos-mesh/chaos-mesh \
--namespace=chaos-mesh \
--create-namespace \
--version 2.7.0
Kill a random pod every 5 minutes:
|
|
Inject network latency and packet loss:
|
|
Fill a disk to trigger out-of-space conditions:
|
|
Pumba (Docker chaos)
|
|
Linux traffic control (tc netem) — no special tools required:
|
|
stress-ng for resource pressure:
|
|
Running a Game Day
A game day is a scheduled, time-boxed chaos exercise. It’s not random — it’s structured.
How to run one:
-
Define scope and hypothesis — What are we testing? What do we expect to happen?
- Example: “Kill the primary database. We expect the application to detect failure within 30 seconds and fail over to the replica within 2 minutes. User-visible errors should last no longer than 3 minutes.”
-
Define success and abort criteria — What does a pass look like? When do we stop?
- Success: Failover completes within 2 minutes, no data loss
- Abort: If failover hasn’t completed within 10 minutes, restore manually
-
Notify stakeholders — Even in staging, let people know.
-
Execute the experiment — One person runs the chaos. Others observe metrics, logs, and user experience.
-
Document observations in real time — Don’t rely on memory.
-
Stop and restore — Return to a known-good state.
-
Debrief — What happened vs. what you expected? What do you need to fix?
Example game day scenarios:
- “Kill the primary database — how long to fail over? Do alerts fire? Does the application recover gracefully or do we need a manual restart?”
- “Fill the disk on the web server — do alerts fire with enough lead time? Does the application degrade gracefully or does it start throwing 500s silently?”
- “Disconnect the NAS — what services break immediately? What degrades gradually? What keeps working?”
- “Revoke the API key your app uses — does it fail with a clear error or silently do nothing?”
- “Simulate a slow database — inject 2 second latency on db queries — do timeouts kick in or does the app hang?”
Starting without dedicated chaos tools:
You don’t need Chaos Mesh to start. The most valuable experiments you can run today require nothing but your existing access:
|
|
Run the experiment, observe what happens, put it back.
DR Testing: Making It Real
Testing your DR plan is not optional. It is the plan. A DR plan that has never been tested is a document, not a plan.
The Restore Test Schedule
| Frequency | Test | Goal |
|---|---|---|
| Monthly | Restore one random file from backup | Verify basic restore works |
| Quarterly | Full application restore to test environment | Verify full stack recovery |
| Annually | Full DR exercise — primary site is “gone” | Verify you can actually survive |
Monthly file restore test:
Pick a random file that was backed up. Restore it to a temporary directory. Verify it matches the original.
|
|
Quarterly application restore test:
- Provision a clean test VM or Docker environment
- Restore the latest application backup to it (database, volumes, config)
- Start the application stack
- Run smoke tests against it (automated or manual)
- Measure how long the entire process took — this is your actual RTO
- Document gaps and clean up
|
|
Automated Backup Integrity Verification
Add this as a weekly cron job:
|
|
The Gap Between Documented and Actual RTO
Every organization discovers the same thing when they run their first real DR exercise: it takes longer than the documented RTO. Always.
Common reasons:
- The person who knows the restore procedure is on vacation
- The runbook references a file path that changed six months ago
- The test environment doesn’t have the same version of the restore tool
- The backup credential expired and nobody noticed
- The restore process works fine but the application won’t start because of a config file that wasn’t backed up
- DNS still points at the old IP after the restore
Measure your actual RTO during every test. Track it over time. If your documented RTO is 2 hours and your actual RTO is 8 hours, that’s your real RTO until you fix the gaps.
Monitoring for DR Readiness
The best time to know your backup job stopped running is not when you need it. These alerts tell you disaster is coming, not that it has arrived.
Proactive Alerts
Disk filling up — predict before it’s full:
|
|
Backup job hasn’t run:
|
|
RAID degraded:
|
|
Replication lag too high:
|
|
Dead Man’s Switch Monitoring
A dead man’s switch inverts the normal alerting model. Instead of alerting when something bad happens, it alerts when something stops happening. Perfect for backup jobs.
Healthchecks.io (free tier available) or self-hosted with Uptime Kuma:
|
|
Configure the check with a period of 24 hours and a grace period of 1 hour. If your daily backup job doesn’t ping within 25 hours, you get notified before you need the backup, not after.
DR for Common Homelab Stacks
Proxmox + Proxmox Backup Server
PBS is the right backup target for Proxmox. It deduplicates across VMs and snapshots, supports encryption, and integrates directly with the Proxmox scheduler.
|
|
Docker Compose Stack
The target state: your entire stack can be rebuilt from git + restored volumes in one session.
|
|
K3s Cluster
|
|
Keep your Helm values files and all Kubernetes manifests in git. If you can kubectl apply -f your entire cluster from scratch, your RTO for cluster rebuild is as short as your provisioning scripts allow.
Home Assistant
|
|
The most important HA backup: the configuration.yaml, automations, scripts, and custom components. The history database (home-assistant_v2.db) is large and less critical — define your RPO for it separately.
Putting It Together: Your DR Readiness Checklist
Use this as a self-assessment. Each “no” is a gap in your DR posture.
Documentation
- Service inventory exists and is current
- Dependency map exists for Tier 0 and Tier 1 services
- RTO/RPO targets are defined for each service tier
- Runbooks exist for your top 5 failure scenarios
- Runbooks are stored in at least 2 locations, one accessible without your infrastructure
Backups
- All Tier 0 and Tier 1 data is backed up on a schedule meeting RPO targets
- Backups are encrypted
- At least one offsite backup exists
- At least one offline or immutable backup exists (ransomware protection)
- Backup jobs are monitored (dead man’s switch, alerting on failure)
Testing
- You have restored from backup in the last 30 days (any file)
- You have run a full application restore test in the last 90 days
- You have measured actual RTO and compared it to your target
- Your runbooks have been executed by someone who didn’t write them
Monitoring
- Disk fill prediction alerts are configured
- Backup job missed alerts are configured
- RAID/storage health alerts are configured
- You have a status page or incident communication channel
Chaos
- You have intentionally failed at least one service in the last quarter and observed recovery
- You know what breaks when your most critical dependency goes down
Disaster recovery is not a one-time project. It’s a practice — something you do regularly, test relentlessly, and improve incrementally. The backup job you set up and forgot about three years ago is not your safety net. The runbook you wrote but never tested is not your plan.
Your DR strategy is exactly as strong as the last time you actually used it. So use it. Schedule a restore test this week. Run a game day next month. Find the gaps before they find you at 2am.
Comments