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.
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.
| Livello | Cosa verifica | Cosa non dimostra |
|---|---|---|
| Backup completato | Il job ha prodotto uno snapshot o un archivio | Che il guest possa essere ripristinato e avviato |
| Verify PBS | L'integrità dei dati del backup | Che OS, database e applicazione funzionino dopo il restore |
| Restore test | Ripristino, boot e controlli nel guest | Da 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:
- controllare storage, VM sorgente e QEMU Guest Agent;
- generare un
RUN_IDunivoco; - scrivere quel valore nella VM come canary;
- eseguire il backup;
- individuare esattamente lo snapshot appena creato;
- ripristinarlo con un VMID dedicato e
--unique; - isolare la VM restaurata dalla rete di produzione;
- avviarla;
- attendere il QEMU Guest Agent;
- eseguire dentro il guest uno script di health check;
- confrontare il canary ripristinato con quello generato prima del backup;
- produrre un risultato
PASSoFAILe 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.shgià 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;
-
qmrestoreopct restoretermina 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.