No description
- Python 58%
- Shell 42%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| config | ||
| modules | ||
| orchestrator | ||
| scripts | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| README.md | ||
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.ymla dichiarare su quale host (hetznerotrantor) il modulo va eseguito. L'orchestratore, lanciato con--host <nome>, esegue solo i moduli il cui campohostcorrisponde. 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-backuporclone-sync)backup.sh— riceve una staging dir, ci scrive dentro i dati pronti per restic (dump SQL,.backupsqlite, 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 nativob2: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 conrestic check+ drill di restore (dump ripristinato e interrogabile). Containervaultwarden-servernon hasqlite3CLI 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()inrun.pyapplicaforget --pruneper modulo (scoped per--tag, non sull'intero repo condiviso) usando la policy del tier daconfig/retention.yml.- paperless:
pg_dumpa 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: sebackup.shfalliva dopo chepre.shaveva fermato il container,post.shnon veniva mai eseguito e il container restava giù — orarun_module()garantisce il restart in un bloccofinally, 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.
- paperless:
- Fase 2c — scheduling automatico su Hetzner:
scripts/run-hetzner-cron.sh(carica i segreti da~/.secrets/backup-orchestrator/, esportaPATH/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 suhttps://node.micheledimaio.it/alerts(dominio pubblico del Traefik locale di trantor — raggiungibile da Hetzner, verificato). Log in~/backup-orchestrator/logs/cron.logsu 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/bucketmichele-backup-criticaldi 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.shin crontab alle 02:40 (affiancato, non sostituito, al cron esistentecron_night_trantor.shdelle 02:00 — 40 minuti di margine per non litigare su I/O disco). Deploy su un path stabile~/backup-orchestratorseparato 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