Disaster Recovery
This is the operational runbook for restoring Orimora after a real incident. It is the companion to Backup & Restore (what is backed up + scheduling). Keep a copy of this page — and your recovery code — somewhere you can reach when the instance is down.
Targets
Section titled “Targets”| Metric | Target | Notes |
|---|---|---|
| RPO (max acceptable loss) | 24 h | Daily backup at 03:00 UTC (BACKUP_SCHEDULE). |
| RTO (max acceptable outage) | 4 h | Manual restore + boot; achievable with this runbook in hand. |
What you need to restore
Section titled “What you need to restore”- The latest DB dump (
orimora-db-<ts>.dump,pg_dumpcustom format). - The latest uploads archive (
uploads-<ts>.tar.gz). - The recovery code — the
ageprivate key (AGE-SECRET-KEY-…) shown once at setup under Settings → Admin → Backups.
Off-site artifacts are named *.age (encrypted). Local on-host copies under BACKUP_PATH are plaintext (the host already holds the live DB) — if you still have the host volume, skip the decrypt step.
Recovery procedure (DB + uploads)
Section titled “Recovery procedure (DB + uploads)”The PostgreSQL client must match the server major version (16) — the image ships postgresql16-client. age and rclone are bundled in the image too.
# 0. Stop the app so nothing writes during the restore.docker compose stop app
# 1. Fetch the latest off-site artifacts (example: an rclone remote named "offsite").rclone copy offsite:orimora ./restore --include '*.age'
# 2. Decrypt with the recovery code (age private key).printf '%s\n' "$AGE_SECRET_KEY" > /tmp/orimora-recovery.keyage -d -i /tmp/orimora-recovery.key -o ./restore/db.dump ./restore/orimora-db-<ts>.dump.ageage -d -i /tmp/orimora-recovery.key -o ./restore/uploads.tar.gz ./restore/uploads-<ts>.tar.gz.ageshred -u /tmp/orimora-recovery.key # don't leave the key on disk
# 3. Restore the database (into a clean DB).# Either drop+recreate the existing DB, or restore into a fresh one and repoint DATABASE_URL.pg_restore --clean --if-exists --no-owner --no-privileges \ --dbname="$DATABASE_URL" ./restore/db.dump
# 4. Restore uploads (unpacks back to ./uploads/...).tar -xzf ./restore/uploads.tar.gz -C /app # adjust target to your uploads parent
# 5. Start the app. The entrypoint runs migrations fail-closed before serving.docker compose up -d appVerify after boot: login works, a document with an attachment renders its image (blob resolved), and Settings → Admin → Backups health is green.
Failure scenarios
Section titled “Failure scenarios”| Scenario | What happened | Action |
|---|---|---|
| (a) Volume / host loss | The server or its disk is gone. | Provision a new host, install the image, pull off-site *.age, run the full procedure. |
| (b) Corrupt DB | DB unreadable but host intact. | Use the local plaintext dump (skip decrypt); pg_restore --clean into the same DB. |
| (c) Bad migration | A migration broke the schema/data. | Restore the most recent pre-incident dump, then redeploy the fixed build. |
| (d) Ransomware / full loss | Host compromised, local backups gone. | Restore from the off-site copy only. This is why off-site + encryption are required. |
Off-site targets (S3-free) — rclone recipes
Section titled “Off-site targets (S3-free) — rclone recipes”Off-site uses an rclone remote (BACKUP_RCLONE_REMOTE). rclone reads its config from rclone.conf or RCLONE_CONFIG_* env. Prefer key/password backends (no OAuth on a headless server). Set up once with rclone config, then set the remote path.
# SFTP to a second machine (most universal — anyone with a second Linux box)[offsite]type = sftphost = backup.example.comuser = orimorakey_file = /home/orimora/.ssh/id_ed25519# → BACKUP_RCLONE_REMOTE=offsite:orimora-backups
# Backblaze B2 (cheap object storage, S3-free, key-based)[offsite]type = b2account = <keyId>key = <applicationKey># → BACKUP_RCLONE_REMOTE=offsite:my-orimora-bucket
# WebDAV (Nextcloud / generic)[offsite]type = webdavurl = https://cloud.example.com/remote.php/dav/files/orimora/vendor = nextclouduser = orimorapass = <obscured — set via `rclone obscure`># → BACKUP_RCLONE_REMOTE=offsite:orimora-backupsRun a full restore drill at least monthly, and after any change to the backup pipeline. CI runs the round-trip restore and the encrypt→off-site round-trip on every push; the monthly drill exercises the real, end-to-end manual procedure above against a throwaway environment.
Checklist:
- Off-site artifacts present and recent.
- Decrypt with the recovery code.
pg_restoreinto a throwaway DB.- Table count matches source.
- A known document and its attachment resolve.
- Record the wall-clock time — it must be ≤ RTO (4 h).
See also
Section titled “See also”- Backup & Restore — what is backed up, scheduling, encryption setup
- Configuration —
BACKUP_*and off-site variables