Self-Hosting
A home-server backup you've actually restored
Prove your backup recovers, don't assume it — 3-2-1 extended to 3-2-1-1-0 (one immutable copy, zero errors after a tested restore), application-consistent database dumps, restic check, and a real restore drill to scratch.
Problem summary
Most home-server operators conflate 'the backup job ran' with 'I have a recoverable backup' — different claims. A job can succeed for months while producing a non-restorable archive: a torn database write, a forgotten volume, bit rot, an expired key. The operator-grade answer extends 3-2-1 to 3-2-1-1-0 — one copy that's immutable/offline/air-gapped, and zero errors after a verified restore test. The two disciplines that make it real: application-consistent database backups (stop the container or dump with `pg_dump`/`mysqldump`/SQLite `.backup` — never `cp` a hot DB file), and a restore drill that recovers to a scratch location and proves the app boots, not just that bytes copied.
Count your copies, media types, and off-site location.
restic check
At least 3 copies, on 2 media, with 1 off-site.
If the app won't boot on restored data, the backup isn't done.
Layer path
Step-by-step runbook
Start here. Do each check in order, compare it to the expected result, and stop when the evidence explains the failure or the safe stop point applies.
Reach 3-2-1
Check: Ensure 3 copies, 2 media, 1 off-site.
Expected result: A single event can't destroy everything.
If not: Add whatever copy/media/location is missing.
Add the immutable '1'
Check: Make one copy object-locked, append-only, or offline.
Expected result: Ransomware/delete can't reach every copy.
If not: Object-lock a bucket or keep an unplugged drive.
Make DB backups consistent
Check: Dump databases or back them up while stopped.
Expected result: DB backups reopen cleanly.
If not: Stop copying live DB files.
Schedule integrity checks
Check: Run the "Check repository structure" command below regularly and --read-data periodically.
Expected result: Corruption is caught early.
If not: Bound --read-data with a subset on large repos.
Run a restore drill
Check: Restore to scratch, verify checksums, boot the app.
Expected result: Recovery is proven end-to-end (the '0').
If not: Never restore over live data during a drill.
Safe stop: If the app won't boot on restored data, the backup isn't done.
Escrow the key
Check: Store the repo key/passphrase separately and test access.
Expected result: You can actually decrypt in an emergency.
If not: A lost key makes the backup worthless.
Decision tree
If: All copies are online/mutable
Then: No ransomware/accidental-delete resistance.
Action: Add an immutable (object-lock/append-only) or offline copy.
Safe stop: Don't trust a backup set that one compromise can erase.
If: Databases are copied as live files
Then: Backups may be torn/inconsistent.
Action: Dump the DB or stop the container before backing up its files.
If: Repo never integrity-checked
Then: Silent corruption is possible.
Action: Run restic check regularly; --read-data periodically on a schedule you can afford.
If: Never restore-tested
Then: Recoverability is unproven.
Action: Restore to scratch, verify checksums, and boot the app.
Safe stop: Treat 'never restored' as 'no backup' for critical data.
If: Restore needs a key you don't have
Then: Encryption key not escrowed.
Action: Back up the key/passphrase separately and test access.
Evidence table
| Symptom | Evidence to collect | Likely layer | Next action |
|---|---|---|---|
| Ransomware/delete wiped 'all' backups | Whether any copy was immutable/offline | No air-gapped/immutable copy | Add object-lock or an offline copy (the '1'). |
| Restored DB won't open / is corrupt | How the DB was captured (live copy vs dump) | Crash-inconsistent DB backup | Use logical dumps or quiesce the container. |
| Old snapshots fail to read | restic check / --read-data results | Repo bit rot | Schedule integrity checks; keep redundant copies. |
| Backups exist but nobody has restored one | Date of last restore drill | Unproven recoverability | Run a scratch restore + app boot test. |
| Can't decrypt the backup in an emergency | Where the key/passphrase lives | Key not escrowed | Store the key separately and test it. |
Commands and settings paths
Check repository structure
restic check
Where: On the backup host (or any machine with repo access)
Expected: Reports no errors in the repository structure.
Failure means: Errors indicate metadata/index corruption.
Safe next step: Investigate before relying on the repo; keep a second copy.
Re-hash data to catch bit rot
restic check --read-data-subset=10% # or --read-data for everything
Where: On the backup host
Expected: The sampled (or full) pack data re-hashes cleanly.
Failure means: A hash mismatch means stored data is damaged.
Safe next step: Restore affected data from another copy; rotate media.
Take an application-consistent DB dump
pg_dump -Fc <db> > db.dump # or mysqldump / sqlite3 .backup
Where: On the DB host/container
Expected: A complete logical dump you can restore independently.
Failure means: A partial dump means the DB was unreachable or creds are wrong.
Safe next step: Back up the dump, not the live data file.
Hardware and platform boundary
Change only when
- Add an immutable/offline copy as soon as the data matters (photos, documents, password vault).
- Schedule periodic --read-data and a calendar'd restore drill once the stack is real.
Evidence that matters
- 3-2-1-1-0: three copies, two media, one off-site, one immutable/offline, zero errors after a tested restore.
- Application-consistent database dumps.
- A restore drill that boots the app on restored data, plus an escrowed key.
Evidence that does not matter
- Backing up disposable, regenerable artifacts (caches, thumbnails).
- Raw RAID as a 'backup' — it's availability, not recovery.
Avoid
- Trusting 'the job ran' as proof of recoverability.
- Copying live database files.
- Keeping every copy online and mutable.
Related tool
Use the linked tool to turn this runbook into a guided check for your exact setup.
Backup plan builderRelated problems
Last reviewed
2026-06-03 · Reviewed by HomeTechOps. Built from 2026-06 research verified against Veeam and Backblaze on 3-2-1-1-0, restic's integrity-check docs (check / --read-data / --read-data-subset), and NIST SP 800-34 on testing recovery rather than assuming it. The operator differentiator is the tested-restore '0' and application-consistent DB dumps, not just a green backup job.
Sources/assumptions
- Assumes a self-hosted stack with one or more databases (Postgres/MySQL/SQLite) plus file data, backed up with a dedup tool (restic/Kopia/Borg are the common choice — stated only as supporting encrypted, deduplicated snapshots).
- 3-2-1-1-0 framing is from vendor guidance (Veeam/Backblaze) — the extra '1' is the family immutable/offline/air-gapped, whose exact wording varies; the '0' is zero errors after a verified restore test. NIST SP 800-34 grounds the test-don't-assume principle.
- restic check / --read-data / --read-data-subset are stated from restic's own docs as the integrity mechanism; equivalent verification exists in other tools.
Source-backed checks
HomeTechOps turns official docs and conservative safety rules into a shorter runbook. These links are the source trail for the page direction.