No description
  • Python 58%
  • Shell 42%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Michele Di Maio fe2a1e88b0 Fix gap: i 4 moduli Hetzner non salvavano docker-compose.yml/.env
Salvavano solo appdata/ (dati/config interna del servizio), mai la
definizione del servizio stesso — immagine, versione, volumi, network,
label Traefik. Senza quello un restore richiederebbe ricostruire tutto
a memoria. Aggiunta copia di docker-compose.yml + .env (se presente)
in staging/compose/ per vaultwarden, paperless, radicale, authelia —
tutti file di proprietà diretta di micheledm, nessun trucco necessario.

Verificato su Hetzner: tutti e 4 i moduli includono ora compose/ nello
snapshot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:48:14 +02:00
config Fase 3 (setup): trantor condivide il bucket B2 critical, non uno dedicato 2026-09-23 15:34:10 +02:00
modules Fix gap: i 4 moduli Hetzner non salvavano docker-compose.yml/.env 2026-09-23 15:48:14 +02:00
orchestrator Fase 2b: radicale e authelia in produzione; paperless separato dai documenti 2026-09-23 15:24:38 +02:00
scripts Fase 3a: modulo org-finance in produzione, scheduling trantor 2026-09-23 15:39:17 +02:00
tests Scaffold iniziale: orchestratore di backup per trantor + Hetzner 2026-09-23 14:36:17 +02:00
.env.example Fase 3 (setup): trantor condivide il bucket B2 critical, non uno dedicato 2026-09-23 15:34:10 +02:00
.gitignore Fase 3a: modulo org-finance in produzione, scheduling trantor 2026-09-23 15:39:17 +02:00
README.md README: conferma scheduling trantor (cron 02:40, deploy ~/backup-orchestrator) 2026-09-23 15:40:31 +02:00

backup-orchestrator

Orchestratore di backup per lo stack di Michele (trantor + Hetzner), pensato per affiancarsi al backup attuale (tar notturno di ~/docker su trantor) e non sostituirlo finché non è validato.

Contesto completo: ~/workspace/misc/dr-audit-2026-09-23.md (audit disaster recovery da cui nasce questo progetto).

Filosofia

  • Un modulo per container/dataset, non un mega-script. Ogni modulo è una cartella autocontenuta con un contratto fisso: dichiara la propria criticità, sa produrre dati coerenti (dump/stop breve/snapshot) e non deve sapere nulla degli altri moduli.
  • Organizzazione per criticità, un'unica cartella modules/. Non c'è separazione per host a livello di directory: modules/critical/, high/, medium/, bulk/ contengono moduli di qualunque host — è module.yml a dichiarare su quale host (hetzner o trantor) il modulo va eseguito. L'orchestratore, lanciato con --host <nome>, esegue solo i moduli il cui campo host corrisponde. La criticità determina il repository restic di destinazione e la policy di retention.
  • L'orchestratore non sa come funziona un container. Scopre i moduli, filtra per host, li esegue nell'ordine di criticità, chiama restic (o rclone per il tier bulk), aggrega i risultati, notifica. Aggiungere un container = aggiungere una cartella, zero modifiche al codice dell'orchestratore.

Struttura

config/            hosts.yml, retention.yml — template, MAI segreti veri
orchestrator/       run.py + lib/ (restic.py, notify.py, lock.py)
modules/            tutti i moduli, per tier di criticità; l'host è nel module.yml
tests/              restore-drill.sh (drill trimestrale)

Contratto di un modulo

Ogni modulo vive in modules/<tier>/<nome>/ e contiene:

  • module.yml — metadata: host (hetzner/trantor), tier, repo restic target, tag, tipo (restic-backup o rclone-sync)
  • backup.sh — riceve una staging dir, ci scrive dentro i dati pronti per restic (dump SQL, .backup sqlite, file copiati). Deve uscire con exit code 0 solo se il backup dei suoi dati è riuscito.
  • pre.sh / post.sh — opzionali, per i container senza un meccanismo di dump a caldo (stop breve prima, restart dopo).

Uso

# tutti i moduli di un host
orchestrator/run.py --host hetzner

# solo un tier
orchestrator/run.py --host hetzner --tier critical

# senza eseguire davvero backup.sh/restic (solo discovery)
orchestrator/run.py --host hetzner --dry-run

Stato del progetto

Vedi le fasi in fondo a questo file. Il vecchio backup tar su trantor resta attivo e invariato finché la Fase 3 non è validata (2 settimane di run puliti

  • 1 restore drill riuscito).

Fasi di sviluppo

  • Fase 0 — scaffold repo
  • Fase 1 — orchestratore minimo + modulo vaultwarden (discovery/dry-run verificati)
  • Fase 2a — modulo vaultwarden in produzione. Bucket B2 michele-backup-critical (Private, Object Lock Governance 30gg, no default encryption, no CORS, "keep all versions"), Application Key scoped al solo bucket (senza bypassGovernance/writeKeys — la prima key generata dalla web UI aveva permessi account-wide, sostituita e revocata), repository restic su endpoint S3-compatibile B2 (il backend nativo b2: non funziona con l'API B2 attuale). Repo sincronizzato su Hetzner via rsync in ~/backup-orchestrator (deploy via git da fare quando Hetzner avrà una sua chiave SSH su Forgejo), segreti in ~/.secrets/backup-orchestrator/ (permessi 600, mai in git). Backup reale eseguito con successo, verificato con restic check + drill di restore (dump ripristinato e interrogabile). Container vaultwarden-server non ha sqlite3 CLI e la dir /data è bind-mount di proprietà di un uid diverso da quello che esegue lo script: il modulo usa un container Alpine effimero (docker run --rm -v ...:/data:ro) per leggere il volume e scrivere in staging con i permessi corretti — pattern riusabile per altri moduli con lo stesso problema di ownership.
  • Fase 2b — tier high su Hetzner in produzione: paperless, radicale, authelia. Nel farlo, aggiunta la retention mancante all'orchestratore: apply_retention() in run.py applica forget --prune per modulo (scoped per --tag, non sull'intero repo condiviso) usando la policy del tier da config/retention.yml.
    • paperless: pg_dump a caldo del DB (metadata: tag, corrispondenti, tipi documento) + data/ (indice full-text, ricostruibile se persa). media/ (documenti originali, archivio PDF, thumbnail — i file veri) non è qui: trattata come le foto Immich, modulo bulk a parte con rclone-sync (Fase 4) — questo modulo da solo non basta per un restore completo di Paperless, serve anche quello. Snapshot verificato: 88,6 MiB.
    • radicale: nessun DB, nessuna API di backup a caldo per i file flat .ics/.vcf — stop breve del container (pre.sh/post.sh) prima della copia, restart subito dopo. Nel farlo, trovato e corretto un bug nell'orchestratore: se backup.sh falliva dopo che pre.sh aveva fermato il container, post.sh non veniva mai eseguito e il container restava giù — ora run_module() garantisce il restart in un blocco finally, indipendentemente da dove fallisce il resto. Snapshot verificato: 32 KiB.
    • authelia: DB sqlite (utenti/2FA) via backup online, zero downtime — file di proprietà diretta di micheledm, nessun trucco di ownership necessario. Redis di sessione escluso (cache, non dati). Snapshot verificato: 307 KiB.
  • Fase 2c — scheduling automatico su Hetzner: scripts/run-hetzner-cron.sh (carica i segreti da ~/.secrets/backup-orchestrator/, esporta PATH/env, lancia l'orchestratore) in crontab alle 03:00, prima del job notturno di Immich (03:30) per evitare contesa I/O. Notifiche via ntfy su https://node.micheledimaio.it/alerts (dominio pubblico del Traefik locale di trantor — raggiungibile da Hetzner, verificato). Log in ~/backup-orchestrator/logs/cron.log su Hetzner (non in git).
  • Fase 3a — modulo org-finance (tier critical) in produzione: copia diretta di ~/Sync/general/orgmode, ~/Sync/general/finance, ~/Sync/finance (17 MiB). Niente bucket dedicato a trantor: stesso repo/bucket michele-backup-critical di Hetzner, key B2 scoped separata (restic-trantor, isolamento tra host), stessa password del repo (è lo stesso repository, non due repository diversi). Scheduling: scripts/run-trantor-cron.sh in crontab alle 02:40 (affiancato, non sostituito, al cron esistente cron_night_trantor.sh delle 02:00 — 40 minuti di margine per non litigare su I/O disco). Deploy su un path stabile ~/backup-orchestrator separato dal checkout di sviluppo ~/dev/backup-orchestrator, stesso pattern usato su Hetzner (rsync, non git pull — Forgejo non ha ancora una chiave SSH per l'automazione locale).
    • docker-config (tier high), dev-projects (tier medium) — restano da fare
    • decisione da prendere: le chiavi SSH (~/.ssh) vanno in un modulo o restano escluse deliberatamente (backuppare le chiavi nel sistema che proteggono è concettualmente storto — gestirle solo via emergency kit cartaceo?)
  • Fase 4 — tier bulk (foto Immich via rclone)
  • Fase 4 — tier bulk (foto Immich via rclone)
  • Fase 5 — monitoring (Uptime Kuma push) e drill di restore
  • Fase 6 — documentazione (DR.md) e hardening