Vai al contenuto

Migrare VM Proxmox da casa alla produzione: Backup Server e snapshot incrementali

Migrare VM Proxmox da casa alla produzione: Backup Server e snapshot incrementali

Migrare VM Proxmox da casa alla produzione: Backup Server e snapshot incrementali

Una sera di maggio, fine ufficio. La VM che ho costruito in casa nelle ultime due settimane è pronta per andare in produzione: un servizio interno che fa pre-elaborazione di feed RSS per i tenant editoriali di AI Multisite, niente di esotico, ma con dipendenze precise (Python 3.12, due modelli ONNX in cache locale, un Redis dedicato, una manciata di env). L’ho sviluppato sul cluster Proxmox di casa, quello che ho montato nodo dopo nodo e raccontato in Proxmox bare-metal in casa. Ora deve atterrare sul cluster di produzione, su Hetzner, e iniziare a lavorare.

La domanda banale che si fa chi viene da Docker o da un PaaS è: doppio click in Proxmox UI? Quasi. La risposta lunga la racconto qui, perché il “quasi” è dove vivono i dettagli operativi, ed è dove un postmortem di maggio mi ha forzato ad aggiornare il runbook.

L’architettura: due cluster separati, un Backup Server condiviso

Il punto di partenza è un’architettura che molti home labber non disegnano subito, e che io stesso ho costruito tardi: due cluster Proxmox completamente separati (cluster di casa e cluster di produzione), con un singolo Proxmox Backup Server che fa da pivot per spostare le VM dall’uno all’altro.

I due cluster non sono in cluster join. Non si vedono via pvecm, non condividono un filesystem distribuito, non hanno una corosync ring fra loro. Sono due isole, ed è giusto che lo siano: il cluster di casa vive su una subnet privata /24 dietro NAT residenziale, ha latenza variabile, dischi consumer, e una connessione internet che ogni tanto fa hiccup. Il cluster di produzione vive su Hetzner Falkenstein, dietro firewall Hetzner, con dischi enterprise NVMe e uplink 1 Gbps simmetrico. Mescolarli in un unico cluster significherebbe propagare l’instabilità di casa nelle decisioni di quorum della produzione. Non si fa.

Il pivot è il Backup Server, una macchina virtuale dedicata (4 vCPU, 8 GB RAM, 2 TB di datastore ZFS in pool dedicato) che gira su Hetzner. Ho valutato di tenerlo a casa, ma il ragionamento di banda mi ha portato a tenerlo in produzione: il restore in produzione è quello che voglio veloce, il backup in casa accetto che vada via WAN. La connessione fra cluster di casa e PBS passa su un tunnel WireGuard always-on (gestito da uno dei nodi Proxmox di casa), così il backup notturno parla con il datastore PBS come se fossero in LAN, e nessuno dei due cluster espone porte sull’internet pubblico.

La regola di disegno che mi sono dato: il PBS è l’unico componente condiviso, e fa una cosa sola. Niente roba accessoria sul PBS. Niente cron extra, niente container Docker, niente servizio di monitoraggio piazzato lì perché “tanto ha CPU libera”. Quando il PBS è semplice, il restore notturno è prevedibile.

Setup PBS: install LXC, datastore ZFS, retention policy

Il PBS l’ho installato come VM (non LXC) su uno dei nodi di produzione. La scelta VM vs LXC, sul Backup Server, ha una motivazione precisa: PBS gira su un kernel Debian customizzato per il datastore ZFS-aware, e mettere un kernel sotto un kernel host condiviso (LXC) significava perdere features di tuning su arc_max, compression, recordsize. Una VM dedicata costa pochissimo overhead a fronte di un setup pulito.

Il datastore PBS sta su un pool ZFS dedicato, configurato con:

  • compression=zstd (ratio sui backup di VM Linux generaliste in zona 1.4-1.6, comodo)
  • recordsize=1M (PBS scrive in chunk grossi, il default 128K è suboptimale)
  • atime=off, relatime=off
  • snapshot hourly del dataset PBS stesso, retention 24h: questo è il backup del backup, una rete di sicurezza extra contro un eventuale corruption del datastore

Il PBS espone il datastore via la sua API nativa (porta 8007), e i due cluster Proxmox lo registrano come storage di tipo pbs con la fingerprint del certificato e una credential di accesso dedicata (ho una credential per cluster casa, una per cluster produzione, con namespace separati nel datastore: home/... e prod/...).

La retention policy iniziale, sbagliata, era quella del default Proxmox: keep-last 3, keep-daily 7, keep-weekly 4, keep-monthly 6. Era sbagliata, e l’ho capito dopo, perché trattava un backup “sperimentale di casa” e un backup “candidato a produzione” allo stesso modo. Su questo torno nel postmortem.

Backup notturno: dirty bitmap e incrementale a livello di byte

Il workflow di backup è il cuore della macchina ed è dove Proxmox Backup Server fa la cosa che mi piace di più: incrementale a livello di chunk. La meccanica, in due righe:

  1. Al primo backup di una VM, PBS legge tutti i blocchi del disco virtuale, li suddivide in chunk (4 MB di default), calcola lo SHA-256 di ciascun chunk, e li invia al datastore. Il datastore deduplica chunk già presenti (fra VM diverse, o fra backup diversi della stessa VM): se due VM Debian condividono mezzo gigabyte di binari /usr/lib, quel mezzo gigabyte sta sul datastore una volta sola.
  2. Dai backup successivi, se la VM è stata in esecuzione continua dal backup precedente, QEMU mantiene una dirty bitmap in RAM: una mappa bit-per-blocco che marca i blocchi modificati dall’ultimo backup. PBS legge solo i blocchi marcati dirty, calcola lo SHA, e invia solo i chunk nuovi.

Il risultato è che un backup pieno di una VM da 80 GB richiede sui 12 minuti la prima volta (limited by uplink di casa), e i backup successivi della stessa VM, in serata, durano fra 30 secondi e 2 minuti, perché la dirty bitmap dice “in tutta la giornata sono cambiati 800 MB di blocchi, mandali e basta”. Per chi ha visto solo backup full notturni di vecchia generazione, è un cambio di passo radicale.

Il caveat operativo importante, che la documentazione PBS ripete e che il forum Proxmox tiene a battesimo nelle FAQ: la dirty bitmap vive in RAM del processo QEMU. Se la VM viene spenta e riaccesa (o se il nodo Proxmox riavvia), la bitmap si perde, e il backup successivo legge tutta l’immagine, calcola gli SHA, e si fida del datastore per la dedup. Niente disastro, ma il backup torna a durare 10-12 minuti invece di 2. La regola che mi sono dato: se possibile, niente reboot dei nodi nelle 4 ore prima della finestra di backup. È una regola morbida, non hard-blocking, ma evita di sprecare banda e tempo di finestra.

La schedulazione è semplice: un job giornaliero alle 02:10 sul cluster di casa che backuppa tutte le VM marcate per produzione, e un job giornaliero alle 03:30 sul cluster di produzione che backuppa la produzione vera (per disaster recovery). Niente sovrapposizione, niente concorrenza sul datastore.

Migrazione casa a produzione: restore, IP, riassegnazione

Questa è la parte che chi parte da un PaaS si aspetta complessa, e invece è semplice. Una volta che la VM “candidata a produzione” è stata backuppata almeno una volta sul PBS, sul cluster di produzione faccio:

  1. Apro la web UI del cluster produzione, sezione Datacenter > Storage > pbs-shared
  2. Trovo il backup della VM, sotto namespace home/, snapshot della notte precedente
  3. Click destro su Restore sul nodo di produzione di destinazione, scelgo VM ID nuovo, scelgo storage ZFS di produzione
  4. PBS streama i chunk dal datastore al nodo di produzione, ricostruisce l’immagine, registra la nuova VM nella qm.conf del nodo

Il restore di una VM da 80 GB su Hetzner, con uplink 1 Gbps verso il PBS che è sullo stesso datacenter, dura sui 6 minuti. È più veloce del backup iniziale perché la rete è migliore.

La parte che resta a mano, e che ogni runbook serio deve dichiarare, è la gestione di rete. La VM viene da una subnet privata di casa (192.168.x.0/24 semplificato a “subnet privata /24 casa” nel runbook), su un bridge vmbr0 che in casa è il bridge LAN. In produzione la stessa VM deve atterrare su una nuova subnet privata diversa, su un bridge vmbr1 che è il bridge VPC interno del cluster di produzione.

Il flusso operativo è:

  1. Prima dell’avvio: edito /etc/network/interfaces della VM via qm guest exec o montando il disco e modificandolo offline. Cambio static IP, gateway, DNS interno.
  2. Bridge: in qm.conf cambio la net0 da bridge=vmbr0 a bridge=vmbr1.
  3. MAC address: lascio il MAC originale o ne genero uno nuovo? Questione di gusto. Io ne genero uno nuovo, così se per errore la VM viene riaccesa anche a casa (debugging), non confliggo con la nuova versione in produzione sui DHCP lease.
  4. DNS interno: aggiorno il record A del DNS privato del cluster produzione (Bind che gira su una LXC dedicata) per il nome host della VM.
  5. Avvio: qm start <vmid> e controllo che la VM agganci la nuova subnet, che ping interni passino, che il monitoring veda il nuovo nodo.

Tempo totale dell’operazione “restore + adattamento rete + verifica”: fra 12 e 20 minuti per VM. È un workflow che ho automatizzato a metà via uno script bash interno che legge un YAML di mapping (vm-name → prod-ip → prod-bridge → prod-dns) e applica i cambi via qm set e qm guest exec. La parte di verifica resta manuale: i ping non li delego a uno script, li guardo io.

Postmortem: il backup pruned a metà restore

Il postmortem di maggio è il motivo per cui questo post esce ora invece che a giugno. Una mattina, finestra di migrazione di una VM secondaria pianificata, ho aperto la web UI di produzione, ho cliccato sul backup notturno della VM, ho selezionato Restore, e il restore è partito. Dopo 4 minuti il job è fallito con un errore secco lato PBS: chunk not found in datastore.

La diagnosi mi ha richiesto venti minuti di lettura log del PBS e di confronto con il job di prune notturno. La causa: il backup che stavo restorando era uno snapshot di tre giorni prima (avevo fatto il backup, poi avevo lavorato altri due giorni sulla VM in casa, poi avevo deciso di restorare lo snapshot intermedio perché quello successivo aveva un bug introdotto). Nel frattempo, il job di prune notturno del PBS aveva applicato la retention policy del namespace home/ (default Proxmox) e aveva pruned, dopo le 02:00 di stanotte, lo snapshot specifico che volevo restorare. Il prune aveva eliminato i chunk usati esclusivamente da quello snapshot (quelli condivisi con altri snapshot via dedup erano restati). Quando il restore ha cercato i chunk mancanti, il datastore ha risposto onestamente: non li ho.

La lezione operativa, durissima ma chiara: una retention policy unica per backup sperimentali e backup candidati a produzione è sbagliata di principio. Sono due regimi di criticità diversi. Il backup di casa di una VM su cui sto sperimentando può essere prunato in 7 giorni senza che mi importi. Il backup di una VM “candidata a produzione” deve sopravvivere abbastanza a lungo da coprire il cycle reale di test, validazione e cutover.

Il fix, applicato lo stesso giorno:

  1. Ho creato due namespace distinti nel datastore PBS: home-experimental/ e home-production-candidate/
  2. Retention policy diverse: home-experimental/ mantiene keep-last 3, keep-daily 7; home-production-candidate/ mantiene keep-last 7, keep-daily 30, keep-weekly 12, keep-monthly 6
  3. Sui job di backup del cluster casa, ho aggiunto una tag VM “production-candidate” (Proxmox supporta tag native): le VM con quella tag vengono backuppate su home-production-candidate/, le altre su home-experimental/
  4. Quando una VM passa dallo stato “sviluppo” allo stato “pronta per migrare”, io aggiungo a mano la tag prima del backup notturno

È una pratica leggera (un click sulla web UI per aggiungere la tag) ma costringe a una decisione cosciente prima della migrazione. È esattamente lo stesso principio che ho applicato al check di integrità restic post-migrazione B2: ogni step critico vuole un esito esplicito, non un’inferenza implicita.

Numeri reali del workflow

Tre mesi di backup notturno e una decina di migrazioni casa to produzione. I numeri che mi tengo, misurati e non stimati:

  • Full backup VM 80 GB (primo backup, da casa via tunnel WireGuard a PBS Hetzner): 11-13 minuti, throughput sostenuto in zona 100-110 Mbps (limited by uplink di casa, non da PBS)
  • Backup incrementale serale della stessa VM, dopo una giornata di lavoro normale (deploy, log, qualche update di pacchetti): 30 secondi a 2 minuti, dato dirty in zona 200 MB-1.5 GB
  • Restore PBS to nodo produzione (dentro Hetzner, banda 1 Gbps): 5-7 minuti per VM da 80 GB
  • Adattamento rete + DNS + verifica: 8-15 minuti
  • Tempo totale operazione casa to produzione, dalla decisione al “VM in produzione che gira”: tipicamente fra 15 e 25 minuti

Il datastore PBS, dopo tre mesi di operatività con circa 12 VM totali fra casa e produzione, occupa 640 GB su 2 TB: la dedup ZFS-aware del PBS lavora bene, e la compression zstd aggiunge un altro 30-35%. Il rapporto chunk fisici su dati logici è di 1 a 4.2: per ogni MB scritto sul datastore, ho 4.2 MB di dati restorabili. È esattamente la promessa di PBS, e l’ho misurata su carico reale.

Costo del PBS: una VM su Hetzner CCX13 (4 vCPU, 16 GB RAM, 80 GB SSD) più un volume Hetzner Cloud da 2 TB attaccato come datastore. Sui 35 euro al mese, IVA esclusa. Niente di drammatico per un componente che è il cardine di tutto il workflow di sviluppo R&D di Romiltec.

Cosa rifarei diversamente

Tre cose, in ordine di importanza.

Una. Avrei separato dal day uno i namespace experimental e production-candidate. Il postmortem di maggio non è stato catastrofico (la VM aveva un backup precedente sopravvissuto, ho restorato quello e ho ricostruito le ultime modifiche a mano in mezz’ora), ma è stata una lezione che potevo evitare leggendo bene la documentazione PBS sui namespace e ragionando in anticipo sui regimi di criticità. La retention policy di un backup non è un dettaglio infra, è una decisione di workflow.

Due. Avrei tenuto il PBS in produzione dal day zero, e non in casa per i primi mesi. La mia versione 0.1 del setup aveva il PBS su una VM in casa, perché “tanto serve a backuppare casa”. Era miope: il valore del PBS è nel restore, e il restore lo voglio veloce dove serve, cioè in produzione. La versione attuale (PBS in Hetzner, tunnel verso casa) è quella corretta, ma ci sono volute due settimane di indecisione e un trasloco di datastore per arrivarci.

Tre. Avrei automatizzato dal day uno lo script di adattamento rete. La parte manuale (edit di interfaces, cambio bridge, riassegnazione DNS) è piccola ma è errore-prone, e l’ho fatta troppe volte a mano prima di scrivere lo YAML di mapping e lo script bash che lo applica. Il momento giusto per scrivere uno script è quando fai la stessa operazione la seconda volta, non la decima.

L’ultima cosa che voglio mettere a verbale: questo workflow casa to produzione è il vero motivo per cui un home lab serve, nel 2026 di Romiltec. Non è “sperimentazione fine a se stessa”, è R&D a costo marginale zero che produce VM concrete che entrano in servizio. Il perché home lab nel 2026 lo racconto a parte. Qui ho voluto raccontare il come, perché chi parte da un PaaS o da una serie di VPS spesso non sospetta che quel ponte fra “ferro in casa” e “produzione che fattura” sia così breve e così solido. Con un Proxmox Backup Server in mezzo, lo è.