Salta al contenuto principale
Dal blog

Backup Proxmox: come capire se il restore funzionerà davvero

Backup Proxmox: una pipeline reale per creare un canary, ripristinare una VM isolata e verificare automaticamente boot, servizi e dati dopo il restore.

Foto profilo di Alessandro IannaconeAlessandro Iannacone

Un job di backup verde non dimostra che, durante un incidente, riuscirai davvero a ripristinare il servizio. Dimostra soltanto che la fase di backup è terminata senza errori noti.

Per sapere se un backup Proxmox è realmente utilizzabile bisogna andare oltre: selezionare uno snapshot, eseguire un restore reale, avviare la VM in isolamento e verificare che sistema operativo, servizi e dati siano nello stato atteso.

La pipeline che propongo in questo articolo arriva fino a questo punto. Prima del backup genera un canary, cioè un valore univoco scritto nella VM. Dopo il backup ripristina quella VM con un nuovo VMID, la scollega dalla rete di produzione, la avvia e richiama al suo interno uno script di health check. La pipeline passa solo se lo script trova il canary corretto e tutti i controlli applicativi restituiscono esito positivo.

È una differenza importante: non stiamo più verificando se esiste un backup, ma se siamo in grado di recuperare un workload da quel backup.

Se stai preparando un major upgrade, questo controllo completa il punto dedicato ai restore della mia checklist pre-upgrade Proxmox VE 9. Per la parte di creazione e rotazione dei dump trovi invece la guida sui backup automatici Proxmox.


Backup, verifica e restore test non sono la stessa cosa

Conviene separare tre livelli diversi.

LivelloCosa verificaCosa non dimostra
Backup completatoIl job ha prodotto uno snapshot o un archivioChe il guest possa essere ripristinato e avviato
Verify PBSL'integrità dei dati del backupChe OS, database e applicazione funzionino dopo il restore
Restore testRipristino, boot e controlli nel guestDa solo non copre necessariamente l'intero disaster recovery aziendale

Proxmox Backup Server mette a disposizione i Verify Job proprio per controllare l'integrità dei dati presenti nel datastore. La documentazione ufficiale raccomanda anche di riverificare periodicamente i backup già controllati, perché un backup valido oggi può degradarsi nel tempo per problemi del supporto fisico.

È un controllo che considero necessario, ma non sufficiente. Un verify job non può sapere, per esempio, se:

  • la VM ripristinata completa il boot;
  • il filesystem viene montato correttamente;
  • PostgreSQL, MariaDB o un altro database riescono ad avviarsi;
  • l'applicazione trova i propri dati;
  • un file indispensabile era escluso dal backup;
  • il QEMU Guest Agent risponde;
  • il tempo necessario al restore è compatibile con la finestra di recovery.

Per queste risposte serve un restore.


La pipeline che voglio ottenere

La sequenza completa è questa:

  1. controllare storage, VM sorgente e QEMU Guest Agent;
  2. generare un RUN_ID univoco;
  3. scrivere quel valore nella VM come canary;
  4. eseguire il backup;
  5. individuare esattamente lo snapshot appena creato;
  6. ripristinarlo con un VMID dedicato e --unique;
  7. isolare la VM restaurata dalla rete di produzione;
  8. avviarla;
  9. attendere il QEMU Guest Agent;
  10. eseguire dentro il guest uno script di health check;
  11. confrontare il canary ripristinato con quello generato prima del backup;
  12. produrre un risultato PASS o FAIL e distruggere la VM di test.

In pratica il backup diventa un artefatto sottoposto a test, esattamente come succede con una build software in una pipeline CI/CD.

Gate operativo: la pipeline è verde solo se il restore termina, la VM fa boot e lo script applicativo restituisce exit 0. Qualunque altra condizione è un restore non validato.


Perché usare un canary

Avviare una VM restaurata e vedere il login prompt non basta. Potresti aver ripristinato uno snapshot più vecchio di quello che pensavi.

Per questo aggiungo un piccolo dato sentinella prima del backup. Un esempio:

20260908T131500Z-a83f42

La pipeline conserva questo valore come EXPECTED_CANARY e lo scrive nella VM sorgente, per esempio in:

/var/lib/restore-test/canary

Quando la VM viene ripristinata, lo script finale legge quel file e pretende di trovare esattamente lo stesso valore.

Se trova un canary precedente, il backup selezionato non è quello appena generato. Se il file non esiste, qualcosa nella catena non è stato incluso. Se il valore coincide, abbiamo invece una prova semplice che il restore contiene lo stato prodotto da quella specifica esecuzione.

Il canary da solo non dimostra che l'applicazione funzioni, quindi deve essere affiancato ai test sui servizi e sui dati.


Prerequisiti del test

Per l'esempio uso una VM Linux e questi elementi:

  • QEMU Guest Agent installato e abilitato;
  • uno storage di backup visibile da Proxmox VE, incluso PBS;
  • uno storage separato o comunque sufficiente per il restore temporaneo;
  • un VMID riservato ai test, per esempio 9900;
  • spazio sufficiente per materializzare la VM;
  • lo script /usr/local/sbin/restore-healthcheck.sh già presente nella VM sorgente e quindi incluso nel backup.

Verifica prima che il Guest Agent risponda:

qm guest cmd 120 ping

E controlla gli storage:

pvesm status

Per vedere i backup di una singola VM:

pvesm list pbs-backup --content backup --vmid 120

Con PBS il volume restituito avrà una forma simile a:

pbs-backup:backup/vm/120/2026-09-08T13:15:00Z

Quel riferimento può essere passato direttamente a qmrestore.


Lo script che decide se il restore è davvero valido

Questo è il componente più importante della pipeline. Lo salvo dentro la VM sorgente come:

/usr/local/sbin/restore-healthcheck.sh

Esempio:

#!/usr/bin/env bash
set -Eeuo pipefail

EXPECTED_CANARY="${EXPECTED_CANARY:?EXPECTED_CANARY non impostato}"
SERVICE_NAME="${SERVICE_NAME:-nginx}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1/healthz}"
CANARY_FILE="${CANARY_FILE:-/var/lib/restore-test/canary}"

failures=0

check() {
  local name="$1"
  shift

  if "$@"; then
    printf '[PASS] %s\n' "$name"
  else
    printf '[FAIL] %s\n' "$name"
    failures=$((failures + 1))
  fi
}

actual_canary="$(cat "$CANARY_FILE" 2>/dev/null || true)"

check 'filesystem root leggibile' test -r /etc/os-release
check 'canary del backup corretto' test "$actual_canary" = "$EXPECTED_CANARY"
check "servizio ${SERVICE_NAME} attivo" systemctl is-active --quiet "$SERVICE_NAME"
check 'health endpoint locale' curl --fail --silent --show-error --max-time 5 "$HEALTH_URL"

if (( failures == 0 )); then
  echo 'RESTORE_TEST=PASS'
  exit 0
fi

echo "RESTORE_TEST=FAIL failures=${failures}"
exit 1

Rendilo eseguibile:

chmod 0755 /usr/local/sbin/restore-healthcheck.sh

Qui ho usato nginx e /healthz solo come esempio. In un ambiente reale aggiungerei test realmente significativi, per esempio:

mysqladmin ping --silent

oppure:

sudo -u postgres psql -Atqc 'select 1'

oppure ancora un controllo Docker:

docker inspect --format '{{.State.Health.Status}}' my-app

La regola è semplice: lo script deve fallire se il servizio sarebbe inutilizzabile per un utente reale, non soltanto se il processo è spento.


Una pipeline Bash reale sul nodo Proxmox

Il seguente esempio può essere eseguito direttamente sul nodo PVE oppure richiamato da Jenkins, Semaphore, GitLab CI o un altro orchestratore.

Uso questi valori di esempio:

SOURCE_VMID=120
BACKUP_STORAGE=pbs-backup
TEST_VMID=9900
TEST_STORAGE=local-zfs

Lo script completo:

#!/usr/bin/env bash
set -Eeuo pipefail

SOURCE_VMID="${SOURCE_VMID:-120}"
BACKUP_STORAGE="${BACKUP_STORAGE:-pbs-backup}"
TEST_VMID="${TEST_VMID:-9900}"
TEST_STORAGE="${TEST_STORAGE:-local-zfs}"
SERVICE_NAME="${SERVICE_NAME:-nginx}"
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1/healthz}"
BOOT_TIMEOUT="${BOOT_TIMEOUT:-240}"

RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$(printf '%04x' "$RANDOM")"
CANARY_FILE='/var/lib/restore-test/canary'

log() {
  printf '[%s] %s\n' "$(date -u +%FT%TZ)" "$*"
}

cleanup() {
  if qm status "$TEST_VMID" >/dev/null 2>&1; then
    log "Cleanup VM di test ${TEST_VMID}"
    qm stop "$TEST_VMID" >/dev/null 2>&1 || true
    qm destroy "$TEST_VMID" >/dev/null 2>&1 || true
  fi
}

log 'STAGE 1/7 - preflight'
command -v jq >/dev/null || { echo 'jq non installato' >&2; exit 10; }
qm guest cmd "$SOURCE_VMID" ping >/dev/null
pvesm status --storage "$BACKUP_STORAGE" | grep -q 'active'
pvesm status --storage "$TEST_STORAGE" | grep -q 'active'

if qm status "$TEST_VMID" >/dev/null 2>&1; then
  echo "TEST_VMID ${TEST_VMID} già esistente: interrompo" >&2
  exit 20
fi

# Da questo punto il VMID era libero: se compare, appartiene al test.
trap cleanup EXIT

log "STAGE 2/7 - genero canary ${RUN_ID}"
CANARY_RESULT="$(
  qm guest exec "$SOURCE_VMID" -- \
    /bin/sh -c "install -d -m 0755 /var/lib/restore-test && printf '%s\\n' '${RUN_ID}' > '${CANARY_FILE}'"
)"

CANARY_EXIT="$(printf '%s' "$CANARY_RESULT" | jq -r '.exitcode // 255')"
test "$CANARY_EXIT" -eq 0

log 'STAGE 3/7 - eseguo il backup'
vzdump "$SOURCE_VMID" \
  --storage "$BACKUP_STORAGE" \
  --mode snapshot

BACKUP_REF="$(
  pvesm list "$BACKUP_STORAGE" --content backup --vmid "$SOURCE_VMID" \
    | awk 'NR > 1 {print $1}' \
    | sort \
    | tail -n 1
)"

test -n "$BACKUP_REF"
log "Backup selezionato: ${BACKUP_REF}"

log 'STAGE 4/7 - restore reale in storage di test'
RESTORE_START_EPOCH="$(date +%s)"
qmrestore "$BACKUP_REF" "$TEST_VMID" \
  --storage "$TEST_STORAGE" \
  --unique 1
RESTORE_SECONDS=$(( $(date +%s) - RESTORE_START_EPOCH ))

log 'STAGE 5/7 - isolo la rete prima del boot'
for nic in $(qm config "$TEST_VMID" | sed -n 's/^\(net[0-9]\+\):.*/\1/p'); do
  qm set "$TEST_VMID" --delete "$nic" >/dev/null
done
qm set "$TEST_VMID" --onboot 0 >/dev/null

log 'STAGE 6/7 - avvio e attendo il Guest Agent'
qm start "$TEST_VMID"

DEADLINE=$(( $(date +%s) + BOOT_TIMEOUT ))
until qm guest cmd "$TEST_VMID" ping >/dev/null 2>&1; do
  if (( $(date +%s) >= DEADLINE )); then
    echo "Guest Agent non disponibile entro ${BOOT_TIMEOUT}s" >&2
    exit 30
  fi
  sleep 5
done

log 'STAGE 7/7 - eseguo i test dentro la VM restaurata'
RESULT="$(
  qm guest exec "$TEST_VMID" -- \
    /usr/bin/env \
    EXPECTED_CANARY="$RUN_ID" \
    SERVICE_NAME="$SERVICE_NAME" \
    HEALTH_URL="$HEALTH_URL" \
    /usr/local/sbin/restore-healthcheck.sh
)"

EXIT_CODE="$(printf '%s' "$RESULT" | jq -r '.exitcode // 255')"
STDOUT="$(printf '%s' "$RESULT" | jq -r '."out-data" // ""')"
STDERR="$(printf '%s' "$RESULT" | jq -r '."err-data" // ""')"

printf '%s\n' "$STDOUT"
[ -z "$STDERR" ] || printf '%s\n' "$STDERR" >&2

if [ "$EXIT_CODE" -ne 0 ] || ! grep -q '^RESTORE_TEST=PASS$' <<<"$STDOUT"; then
  echo "RESTORE_PIPELINE=FAIL backup=${BACKUP_REF}" >&2
  exit 40
fi

echo "RESTORE_PIPELINE=PASS backup=${BACKUP_REF} restore_seconds=${RESTORE_SECONDS} canary=${RUN_ID}"

Lo script usa jq per leggere in modo affidabile il JSON restituito da qm guest exec. Se non è presente sul nodo:

apt update && apt install jq

Il risultato finale sarà simile a:

[PASS] filesystem root leggibile
[PASS] canary del backup corretto
[PASS] servizio nginx attivo
[PASS] health endpoint locale
RESTORE_TEST=PASS
RESTORE_PIPELINE=PASS backup=pbs-backup:backup/vm/120/2026-09-08T13:15:00Z restore_seconds=94 canary=20260908T131500Z-a83f

Questo output è molto più utile di un semplice TASK OK: identifica il backup testato, il canary atteso e il tempo impiegato per materializzare il restore.


Perché uso --unique

qmrestore supporta l'opzione:

--unique 1

che assegna una nuova identità di rete al guest restaurato, evitando di riutilizzare la stessa MAC address del workload originale.

È comunque una misura aggiuntiva, non una sostituzione dell'isolamento. Una VM restaurata può conservare lo stesso indirizzo IP configurato nel sistema operativo, gli stessi hostname, gli stessi client ID applicativi e le stesse credenziali.

Per questo, nello script di esempio, rimuovo tutte le NIC prima del primo boot e uso il QEMU Guest Agent per eseguire i controlli senza rete.

Per un test più completo puoi invece collegare la VM a un bridge o a una VLAN dedicata al disaster recovery, senza route verso la produzione. In quel caso puoi verificare DNS, reverse proxy, database remoti e dipendenze applicative mantenendo comunque il clone confinato.


Il Guest Agent che risponde non è ancora un PASS

Questa riga:

qm guest cmd 9900 ping

è utile perché dimostra che il sistema operativo è arrivato abbastanza avanti da far rispondere il QEMU Guest Agent.

Ma il test non deve fermarsi qui.

Una VM può rispondere al Guest Agent e avere contemporaneamente:

  • database in recovery permanente;
  • filesystem dati non montato;
  • applicazione in errore;
  • configurazioni mancanti;
  • credenziali scadute;
  • un health endpoint che restituisce 500;
  • dati più vecchi dell'RPO previsto.

Il Guest Agent è quindi un segnale di boot, mentre restore-healthcheck.sh è il gate applicativo.


Come trasformarlo in una pipeline Jenkins

Se il nodo Proxmox viene gestito da Jenkins, la logica può rimanere nello script e Jenkins può limitarsi a trattarne l'exit code come quality gate.

Un Jenkinsfile minimale potrebbe essere:

pipeline {
  agent any

  stages {
    stage('Restore test Proxmox') {
      steps {
        sshagent(credentials: ['proxmox-restore-test']) {
          sh '''
            ssh -o BatchMode=yes root@pve-test \
              SOURCE_VMID=120 \
              BACKUP_STORAGE=pbs-backup \
              TEST_VMID=9900 \
              TEST_STORAGE=local-zfs \
              SERVICE_NAME=nginx \
              HEALTH_URL=http://127.0.0.1/healthz \
              /root/scripts/proxmox-restore-pipeline.sh
          '''
        }
      }
    }
  }
}

Se lo script restituisce exit 40, Jenkins marca lo stage come fallito. Se restituisce 0, il restore test è verde.

Lo stesso principio vale con Semaphore, GitLab CI, GitHub Actions o un timer systemd: l'orchestratore non deve reinterpretare il risultato. È lo script di test a stabilire se il workload recuperato è valido.

In produzione eviterei un accesso SSH root generico da un runner condiviso. Preferirei un nodo di test dedicato, una chiave limitata e un comando autorizzato specifico per questa procedura.


Cosa aggiungerei per un database

Per un database non mi fermerei a systemctl is-active.

Un test MySQL/MariaDB potrebbe controllare:

mysqladmin ping --silent
mysql -NBe 'SELECT COUNT(*) FROM app.orders;'

La pipeline potrebbe avere un valore atteso:

MIN_EXPECTED_ORDERS=125000

Lo script finale fallisce se il numero ripristinato è inferiore alla soglia prevista.

Ancora meglio: prima del backup puoi creare una riga canary in una tabella tecnica e poi verificarne la presenza dopo il restore. In questo modo non stai controllando soltanto che il database si avvii, ma che il dato scritto prima del backup sia effettivamente recuperabile.

Per applicazioni con più componenti adotterei quindi canary differenti:

  • file sul filesystem;
  • record nel database;
  • oggetto su storage applicativo;
  • risposta dell'endpoint locale;
  • stato di eventuali code o worker.

E se il workload è un container LXC?

Il principio non cambia.

Il restore si esegue con:

pct restore 9901 <backup-volume> --storage local-zfs --unique 1

E il test interno diventa ancora più semplice perché Proxmox può eseguire direttamente un comando nel container:

pct exec 9901 -- /usr/bin/env \
  EXPECTED_CANARY=20260908T131500Z-a83f \
  /usr/local/sbin/restore-healthcheck.sh

C'è però una verifica importante per LXC: la documentazione Proxmox specifica che bind mount e device mount non vengono inclusi nel backup. Durante il restore può essere ripristinata la relativa configurazione, ma non i dati presenti su quei mount.

Se un container dipende da /mnt/data, NFS, bind mount o device host, il restore test deve verificare esplicitamente anche quella dipendenza. È uno dei casi classici in cui il backup può risultare formalmente corretto e il servizio essere comunque incompleto.


Dove entra PBS Verify nella pipeline

Se utilizzi Proxmox Backup Server, mantengo il Verify Job come controllo separato e periodico.

Sul PBS è possibile verificare manualmente un datastore con:

proxmox-backup-manager verify <datastore> \
  --read-threads 1 \
  --verify-threads 4 \
  --ignore-verified false

Non eseguirei necessariamente una verifica completa di tutto il datastore prima di ogni restore test: può essere un'operazione costosa. Preferisco due livelli complementari:

  • verify periodico PBS per individuare problemi di integrità e degradazione dei backup;
  • restore test periodico su workload rappresentativi o critici per provare la recovery reale.

Per i sistemi più importanti, dopo modifiche a storage, policy di backup, filesystem, database o configurazioni applicative, eseguirei un restore test aggiuntivo senza aspettare la schedulazione ordinaria.


Misurare anche il tempo di recovery

Nel mio esempio salvo:

RESTORE_SECONDS

Non è ancora l'RTO completo, perché un vero recovery può includere DNS, firewall, reverse proxy, database esterni e procedure organizzative. Però è una misura concreta del tempo tecnico di restore.

Registrare questo valore nel tempo permette di accorgersi di problemi che un semplice PASS/FAIL non mostra.

Esempio:

settimana 1: 88 secondi
settimana 2: 91 secondi
settimana 3: 96 secondi
settimana 4: 412 secondi

La quarta esecuzione è tecnicamente riuscita, ma merita un'indagine su datastore, rete PBS, deduplica, storage di destinazione o carico del nodo.

Il monitoraggio della durata trasforma quindi il restore test anche in un piccolo controllo di capacity e performance della catena di recovery.


Quando considero davvero verificato un backup

Per un workload critico considero il test superato solo quando posso spuntare tutto:

  • il backup selezionato è recente e identificato con precisione;
  • il datastore è raggiungibile;
  • PBS Verify è configurato, se uso PBS;
  • qmrestore o pct restore termina senza errori;
  • il guest viene ripristinato con identità separata;
  • il primo boot avviene in una rete isolata o senza rete;
  • il sistema operativo arriva allo stato operativo;
  • il canary è quello previsto per quel backup;
  • i servizi critici risultano attivi;
  • almeno un test applicativo reale passa;
  • eventuali database rispondono e contengono dati coerenti;
  • la durata del restore è registrata;
  • la VM temporanea viene rimossa anche in caso di errore;
  • l'esito della pipeline genera una notifica o un alert.

Un backup che non è mai arrivato almeno una volta fino a questo punto, per me, resta non provato.


Tre errori che falsano il test

1. Avviare il clone sulla rete di produzione

È il rischio più serio. La VM restaurata può avere lo stesso IP della VM originale e può iniziare a rispondere, inviare email, registrarsi a cluster o eseguire cron.

Il clone deve essere isolato prima del boot.

2. Controllare soltanto che la VM sia running

Questo comando:

qm status 9900

prova soltanto che QEMU sta eseguendo la VM. Non dice nulla sullo stato del guest.

3. Usare un health check troppo debole

systemctl is-active nginx può essere verde mentre l'applicazione dietro Nginx è rotta.

Il test finale deve arrivare almeno fino al componente che rappresenta il servizio reale: endpoint HTTP, query al database, file atteso o una combinazione di questi controlli.


La pipeline ideale non deve essere eseguita durante l'incidente

Il momento peggiore per scoprire che qmrestore non riesce a scrivere sullo storage, che il Guest Agent non parte o che un bind mount non era incluso è durante un outage.

Il restore test deve essere una normale attività operativa, schedulata e osservabile.

Una frequenza sensata dipende dalla criticità del servizio e dall'RPO/RTO richiesto. Per i workload importanti preferisco comunque un test ricorrente e un'esecuzione extra dopo cambiamenti rilevanti alla catena di backup o all'applicazione.

L'obiettivo è poter rispondere a una domanda molto concreta:

Qual è l'ultimo backup che abbiamo effettivamente ripristinato e validato?

Se la risposta è un VMID, un timestamp, un RUN_ID e un log RESTORE_PIPELINE=PASS, hai un'evidenza. Se la risposta è "i job sono tutti verdi", hai soltanto una speranza.


Quando serve un assessment della strategia di backup

Negli ambienti semplici questa pipeline può essere implementata con poche decine di righe di Bash. In cluster, infrastrutture con PBS remoto, Ceph, database, mount esterni, dipendenze applicative o RTO stretti, il test deve invece rispecchiare davvero l'architettura del servizio.

Posso aiutarti con un assessment Proxmox e della strategia di disaster recovery: verifica dei backup, restore controllati, analisi dei log, hardening della catena di backup, definizione dei test applicativi e automazione della pipeline di recovery.

Contattami se vuoi trasformare i backup esistenti in una procedura di restore misurabile e verificabile.


FAQ

Un Verify Job di PBS sostituisce un restore test?

No. PBS Verify controlla l'integrità dei dati del backup. Un restore test verifica invece che quel backup possa essere materializzato e che il workload risultante funzioni. Sono controlli complementari.

Devo accendere la VM restaurata?

Se vuoi dimostrare che il sistema operativo e l'applicazione sono recuperabili, sì. Fallo però in isolamento: senza NIC, con link_down, oppure su una rete di recovery dedicata e senza route verso la produzione.

Posso testare automaticamente l'ultimo backup PBS?

Sì. pvesm list <storage> --content backup --vmid <vmid> restituisce i backup visibili a PVE; il volume selezionato può essere usato con qmrestore. Nella pipeline conviene comunque usare un canary per evitare che una selezione errata di snapshot produca un falso positivo.

Il QEMU Guest Agent è obbligatorio?

Non per eseguire un restore, ma è molto utile per un test automatizzato perché permette di controllare e interrogare la VM senza esporla sulla rete. Senza Guest Agent puoi usare una rete di test isolata e collegarti tramite SSH o un altro meccanismo di management.

Il test del canary basta?

No. Il canary dimostra che hai recuperato lo stato atteso del backup. Devi aggiungere almeno i controlli sui servizi e, per i workload critici, una verifica applicativa o sul database.


Fonti ufficiali