Disaster Recovery¶
Disaster recovery (DR) answers one question: when the server dies, how long until the platform is back and how much data can we afford to lose? For Argus the answer is simple, because only one piece of state matters:
The only state you must protect is the PostgreSQL database. Everything else — the app, worker, scheduler, Caddy/WAF, OTel Collector, RabbitMQ — is rebuilt from images and configuration.
Backups and the restore mechanics are covered in the Deployment Guide and the Upgrade & Rollback Runbook. This page turns them into a DR plan with concrete RPO/RTO targets, per-scenario runbooks, and the drill you should run before you ever need them.
Defining your targets¶
| Term | Meaning | Your lever |
|---|---|---|
| RPO (Recovery Point Objective) | How much data you accept losing | Backup frequency. Every backup is a full dump, so RPO ≈ the interval between backups |
| RTO (Recovery Time Objective) | How long until the platform is back | Restore speed + re-provisioning + verification time |
The defaults in the stack target a daily-backup, hours-to-recover profile:
BACKUP_RETENTION=7 with a manual or cron-driven just db-backup. If you need
a tighter RPO, back up more often (a full dump every few hours is fine at this
scale) and script the off-box copy.
Lower RPO if you need it
For near-zero RPO you would need PostgreSQL streaming/WAL archiving, which the stack does not ship by default. A more frequent full-dump schedule (e.g. hourly via cron) is the supported, low-effort way to shrink RPO.
What survives and what does not¶
| Asset | Survives a server loss? | How to protect it |
|---|---|---|
| PostgreSQL data | Only via a backup | just db-backup, kept off-box |
app/worker/scheduler images |
Yes | Pulled from GHCR (APP_VERSION tag) |
backup image |
Yes | Built/pushed once; see Backups on Kubernetes |
.env configuration |
No | Back it up or re-derive it (see below) |
| RabbitMQ queued tasks | No | Irrelevant — a restart starts from an empty queue |
| Caddy certs | Re-issued | Let's Encrypt re-issues automatically after DNS is back |
.env is state. Losing the server also loses your secrets and settings.
Keep a copy of .env (or the values) somewhere safe but not in git — a
password manager, a sealed vault, or an encrypted backup. Recovery without it
means regenerating every secret and reconfiguring SMTP, Slack, and rate limits.
See Secrets Management.
Recovery scenarios¶
Scenario 1 — Database corrupted or a bad migration (same host)¶
Use the restore path, which drops and recreates the database. Full procedure in the Upgrade & Rollback Runbook — the short form:
docker compose stop app worker scheduler # free DB connections
just db-restore argus_<timestamp>.sql.gz --reset
docker compose start app worker scheduler
Verify (counts should match the backup):
Scenario 2 — Full server loss (new host)¶
The goal is a from-scratch re-provision using only: the GHCR images, a
.env copy, and the latest off-box backup.
- Provision a new server and install Docker Compose (or Podman) — follow Deployment Guide Steps 1–3.
- Recreate
.envfrom your saved copy (or re-generate per the guide) and setCOMPOSE_FILE=docker-compose.remote.yml. Do not changeSECRET_KEYunless you accept invalidating every active session. - Restore the database before starting the app — the app runs migrations on boot, so restore first, then start:
# fetch the backup from your off-box storage (see below)
cp /path/to/off-box/argus_20260818_123456.sql.gz backups/
# start only postgres + rabbitmq + backup (not app yet)
docker compose up -d postgres rabbitmq backup
# wait for postgres to be healthy, then restore
just db-restore argus_20260818_123456.sql.gz --reset
- Start the rest of the stack and verify:
docker compose up -d
docker compose ps # all healthy
curl -sS -o /dev/null -w "%{http_code}\n" https://argus.example.com/readyz # 200
- Point DNS at the new host (lower the TTL beforehand if you planned for this) and confirm TLS is re-issued by Caddy.
- Take a fresh backup so the new host's baseline is captured.
Order matters
Restore before the first docker compose up -d, because the app
entrypoint runs alembic upgrade head on boot. If the app boots against an
empty database it will create a new (empty) schema and the later restore
still works (it drops and recreates), but you avoid confusing states by
restoring first.
Scenario 3 — Ransomware or accidental deletion¶
Same as Scenario 1, but the trigger is logical damage rather than hardware. Restore the newest backup that predates the damage. This is exactly why the off-box copy exists — an attacker or a bad script that can reach the server can also reach backups stored only on it.
The off-box copy¶
A backup that lives only on the server is not a backup for DR purposes. After
each just db-backup, copy BACKUP_DIR elsewhere. Any tool works; two common
patterns:
The Upgrade & Rollback Runbook already makes the off-box copy a mandatory pre-upgrade step — make it a scheduled job too, so it is never dependent on a human remembering it.
The DR drill¶
Do not discover restore on the day of the incident. Run this drill on a scratch host (or a staging copy of the stack) on a schedule — monthly is a sane cadence. The existing restore drill validates the same-host restore; this one validates the full new-host path:
- Create and off-box a backup:
just db-backup, then syncbackups/off-box (the exact artifact you would recover from). - Provision a fresh host and reproduce Scenario 2: clone the repo, restore
.env, restore the database from the off-box copy. - Verify data:
select count(*)onprojects,sbomsandsbom_vulnerabilitiesmust match the backup. Sign in, open a project, view an SBOM and the Vulnerabilities page. - Verify a scan still works: upload a small SBOM and confirm the worker
scans it without errors (
docker compose logs worker). - Record the elapsed time (your measured RTO) and any friction in your incident runbook.
Fresh host is the honest test
Restoring onto the same host hides two common failure modes: you cannot
re-provision a host, or your backup tooling depends on something the new
host does not have (a locally built backup image, host paths, etc.).
Every few drills, build the backup image from scratch on the new host to
prove the image pipeline, not just the dump.
Incident checklist (print this)¶
- Identify scope: DB only, or full host loss?
- Stop app/worker/scheduler if restoring into a live stack.
- Pick the newest backup that predates the damage / is intact.
- Verify the backup integrity (
gzip -ton the file) before starting. - Restore (Scenario 1 or 2 above).
- Start the stack and verify
/readyz+ row counts. - Sign in and smoke-test: project, SBOM, vulnerabilities, a fresh scan.
- Take a fresh backup immediately so the recovered state is the baseline.
- Log what happened and which backup was restored (per the runbook).