ADR-0008 · VPS Agnosticism + Disaster Recovery
We chose to architect the Agent Host so that Hermes can be migrated to any Ubuntu VPS provider within hours, with at most one day of Hermes state loss.
Context
Section titled “Context”The Agent Host runs on Hostinger VPS KVM2. If Hostinger has an outage, a breach, a pricing change, or goes bankrupt, we need to be back online quickly without losing Hermes’s memory, skills, sessions, or configuration.
Decision
Section titled “Decision”All Hermes state is backed up daily as encrypted archives to a private personal GitHub repository, plus weekly to the External HD. A fresh VPS can be provisioned and restored in under 2 hours without storing readable Hermes memory or secrets in GitHub.
What lives ONLY on the VPS (and must be backed up)
Section titled “What lives ONLY on the VPS (and must be backed up)”| Data | Path | Criticality | Recovery without backup |
|---|---|---|---|
| Sessions DB (conversation history) | ~/.hermes/sessions.db | High — irreplaceable memories | ❌ Lost forever |
| Skills (learned procedures) | ~/.hermes/skills/ | High — weeks of learning | ❌ Must re-learn from scratch |
| Memory (cross-session knowledge) | ~/.hermes/memory.db | High — agent’s long-term knowledge | ❌ Lost forever |
| Config | ~/.hermes/config.yaml | Medium — can be recreated from docs | ✅ Recreate from ADRs + SOUL.md |
| Cron jobs | ~/.hermes/cron/ | Medium — recreatable | ✅ Recreate from docs |
| SOUL.md + CONTEXT.md | ~/.hermes/SOUL.md, ~/.hermes/CONTEXT.md | Low — source is GitHub repo | ✅ Copy from GitHub |
| .env (secrets) | ~/.hermes/.env | Low — source is Bitwarden | ✅ Re-copy from Bitwarden |
What is already safe (not on VPS)
Section titled “What is already safe (not on VPS)”| Data | Where | Why it’s safe |
|---|---|---|
| Architecture docs (ADRs, diagrams, glossary) | GitHub repo | Git history, redundant |
| Knowledge Vault (PARA notes, docs) | Google Drive | Google’s infrastructure |
| Code Projects | GitHub repos | Git history, redundant |
| API keys & passwords | Bitwarden | Cloud-synced, encrypted |
| Static sites | Cloudflare Pages | Built from GitHub, stateless |
Daily encrypted backup procedure
Section titled “Daily encrypted backup procedure”Run this with an OS-level systemd timer, not Hermes cron. Backups must continue even if the Hermes Agent or its scheduler is broken.
# Run daily from the Agent Host — backs up critical Hermes state, excluding secretsBACKUP_DATE="$(date +%Y%m%d)"BACKUP_DIR="/tmp/hermes-backup-$BACKUP_DATE"ARCHIVE="/tmp/hermes-backup-$BACKUP_DATE.tar.gz"ENCRYPTED="/tmp/hermes-backup-$BACKUP_DATE.tar.gz.age"mkdir -p "$BACKUP_DIR"
# Sessions, memory, skills (the irreplaceable data)cp ~/.hermes/sessions.db "$BACKUP_DIR/"cp ~/.hermes/memory.db "$BACKUP_DIR/"cp -r ~/.hermes/skills/ "$BACKUP_DIR/skills/"cp -r ~/.hermes/cron/ "$BACKUP_DIR/cron/"cp ~/.hermes/config.yaml "$BACKUP_DIR/"
# Compress, encrypt, and push only ciphertexttar -czf "$ARCHIVE" -C "$BACKUP_DIR" .age -r "<AGE_PUBLIC_RECIPIENT>" -o "$ENCRYPTED" "$ARCHIVE"cp "$ENCRYPTED" ~/hermes-backups/cd ~/hermes-backupsgit add "$(basename "$ENCRYPTED")"git commit -m "backup: Hermes state $BACKUP_DATE"git push
rm -rf "$BACKUP_DIR" "$ARCHIVE" "$ENCRYPTED"The private GitHub backup repo uses a fine-grained PAT scoped only to that single repo with contents write permission. The repo must contain only encrypted .age archives — never .env, plaintext SQLite databases, OAuth tokens, API keys, SSH keys, or unencrypted tarballs.
Weekly offline backup procedure
Section titled “Weekly offline backup procedure”Once per week, copy the latest encrypted archive from the private GitHub backup repo to the External HD. Keep the External HD as the offline recovery path if GitHub, the PAT, or the account is unavailable.
Decryption and restore
Section titled “Decryption and restore”The age private identity is stored in the Credential Vault, not on GitHub. To decrypt a backup on a fresh machine:
# 1. Retrieve age private identity from Bitwarden into a local filechmod 600 hermes-backup-age-key.txt
# 2. Decrypt the selected archiveage -d -i hermes-backup-age-key.txt \ -o hermes-backup-YYYYMMDD.tar.gz \ hermes-backup-YYYYMMDD.tar.gz.age
# 3. Extract for inspection or restoremkdir -p /tmp/hermes-restoretar -xzf hermes-backup-YYYYMMDD.tar.gz -C /tmp/hermes-restore
# 4. Restore after Hermes is installed and stoppedsystemctl --user stop hermes-gatewaycp /tmp/hermes-restore/sessions.db ~/.hermes/sessions.dbcp /tmp/hermes-restore/memory.db ~/.hermes/memory.dbrsync -a /tmp/hermes-restore/skills/ ~/.hermes/skills/rsync -a /tmp/hermes-restore/cron/ ~/.hermes/cron/cp /tmp/hermes-restore/config.yaml ~/.hermes/config.yamlsystemctl --user start hermes-gatewayRestore .env separately from Bitwarden. Never restore secrets from the backup repo because secrets should not be there.
Disaster Recovery Plan (DRP)
Section titled “Disaster Recovery Plan (DRP)”Scenario: Hostinger VPS is gone. Rebuild on a new provider.
| Step | Action | Time | Source |
|---|---|---|---|
| 1 | Provision new Ubuntu VPS (any provider: Hetzner, DigitalOcean, Scaleway, OVH…) | 10 min | Any KVM provider, €5-10/mo |
| 2 | Run Hermes quickstart installer | 8 min | FlipZ3ro/hermes-quickstart or pipx install hermes-agent |
| 3 | Copy .env secrets from Bitwarden | 5 min | Bitwarden vault |
| 4 | Decrypt and restore sessions.db, memory.db, skills/ from latest backup | 10 min | Private GitHub backup repo or External HD |
| 5 | Copy SOUL.md + CONTEXT.md from GitHub repo | 2 min | git clone fducat18/docs.ducatillon.net |
| 6 | Configure rclone (Google Drive OAuth2) | 15 min | Google Cloud Console |
| 7 | Install cloudflared, update Cloudflare Tunnel to point to new IP | 10 min | Cloudflare dashboard |
| 8 | Update Telegram webhook URL (via Cloudflare Tunnel — same domain, no change needed) | 0 min | Automatic — tunnel handles routing |
| 9 | Verify: send Telegram message, check dashboard | 5 min | — |
| Total | ~60 min |
What makes this possible
Section titled “What makes this possible”- No vendor-specific features — Hermes runs on any Linux with Python 3.11. No Hostinger-specific APIs or tools.
- Cloudflare Tunnel abstracts the IP — hermes.ducatillon.net points to a tunnel, not an IP. Changing VPS = updating tunnel config, not DNS.
- Secrets in Bitwarden, not only on VPS — .env can be reconstructed from the vault.
- Architecture docs in GitHub — SOUL.md and CONTEXT.md are versioned and recoverable.
- The only truly VPS-bound data is the SQLite DBs — and those get daily encrypted backups.
RTO and RPO
Section titled “RTO and RPO”- RTO (Recovery Time Objective): ~1 hour (provision + restore + verify)
- RPO (Recovery Point Objective): 1 day for Hermes state (daily encrypted GitHub backup), 1 week for offline recovery (External HD). Acceptable for personal use — worst case, you lose one day of conversations and any new skills learned that day.
Upgrade path
Section titled “Upgrade path”| Trigger | Improvement |
|---|---|
| Hermes becomes business-critical (Djuly’s venture depends on it) | Add a second encrypted cloud backup target (e.g., Backblaze B2, ~€0.50/mo) |
| Multiple agents or high-availability needed | Docker Compose + volume mounts → portable container image |
| Regulatory requirement | Full audit trail + geo-redundant backup |