Backup di Proxmox con BorgBackup e controllo via Borg-UI
Nel mio homelab ho un nodo Proxmox VE (host01) e un NAS Synology. Volevo un backup automatico, deduplicato e cifrato di VM, container e configurazione dell’host, con un’interfaccia web per consultare gli archivi senza dover ricordare ogni comando.
La soluzione è composta da due parti con ruoli ben distinti:
| Componente | Dove gira | Ruolo |
|---|---|---|
| Script bash + cron | Host Proxmox | Esecutore: crea i dump, li archivia con Borg, fa prune e compact |
| Borg-UI | Container LXC dedicato | Interfaccia di controllo: elenco archivi, statistiche, restore, controlli |
Il repository Borg sta sul NAS, esportato via NFS e montato sull’host in /mnt/nas-backup.
Perché non far eseguire tutto a Borg-UI?
vzdumpe la cartella dei dump esistono solo sull’host. Borg-UI gira in un container e dovrebbe far transitare centinaia di GB dall’host al container e di nuovo verso il NAS. Lo script sull’host è più semplice e più efficiente. Borg-UI resta il posto dove guardare cosa è successo.
1. Lo script di backup
Lo script fa, nell’ordine:
- verifica che il NAS sia montato;
- esegue
vzdumpdi tutte le VM e i container (modalità snapshot, compressione zstd); - archivia la cartella dei dump in Borg;
- archivia la configurazione di Proxmox in un archivio separato;
- applica la retention (prune) e libera lo spazio (compact);
- sempre, anche in caso di errore, cancella i dump locali più vecchi di 3 ore;
- manda una mail con l’esito.
Salvalo in /usr/local/bin/proxmox-borg-backup.sh:
#!/bin/bash
# ==============================================================
# proxmox-borg-backup.sh
# Backup completo Proxmox VE → BorgBackup su NAS
# ==============================================================
set -euo pipefail
# ── Configurazione ──────────────────────────────────────────
BORG_REPO="/mnt/nas-backup/borg-proxmox"
# La passphrase NON sta nello script: vedi /etc/borg-backup.env
source /etc/borg-backup.env
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
DUMP_DIR="/var/lib/vz/dump"
LOG_FILE="/var/log/borg-backup.log"
DATE=$(date +%Y-%m-%d_%H-%M)
# Retention policy
KEEP_DAILY=7
KEEP_WEEKLY=4
KEEP_MONTHLY=6
# Email notification
EMAIL_TO="tuo-indirizzo@example.com"
EMAIL_FROM="mittente@example.com"
# ── Funzioni ────────────────────────────────────────────────
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
check_nas_mount() {
if ! mountpoint -q /mnt/nas-backup; then
log "NAS non montato. Tentativo di mount..."
mount /mnt/nas-backup || { log "ERRORE: Mount fallito."; return 1; }
fi
log "NAS montato correttamente."
}
backup_vms_and_ct() {
log "=== Avvio dump VM e Container ==="
vzdump --all \
--compress zstd \
--mode snapshot \
--dumpdir "$DUMP_DIR" \
--quiet \
2>&1 | tee -a "$LOG_FILE"
log "Dump completato."
}
backup_pve_config() {
log "=== Backup configurazione Proxmox ==="
borg create \
--stats \
--compression zstd,3 \
"${BORG_REPO}::pve-config-${DATE}" \
/etc/pve \
/etc/network/interfaces \
/etc/hosts \
/etc/hostname \
2>&1 | tee -a "$LOG_FILE"
}
backup_dumps_to_borg() {
log "=== Trasferimento dump in Borg ==="
borg create \
--stats \
--compression zstd,3 \
"${BORG_REPO}::proxmox-${DATE}" \
"$DUMP_DIR" \
--exclude "$DUMP_DIR/*.log" \
2>&1 | tee -a "$LOG_FILE"
log "Archivio Borg creato: proxmox-${DATE}"
}
prune_old_backups() {
log "=== Pulizia archivi vecchi ==="
borg prune \
--list \
--glob-archives 'proxmox-*' \
--keep-daily "$KEEP_DAILY" \
--keep-weekly "$KEEP_WEEKLY" \
--keep-monthly "$KEEP_MONTHLY" \
"$BORG_REPO" \
2>&1 | tee -a "$LOG_FILE"
borg prune \
--list \
--glob-archives 'pve-config-*' \
--keep-daily "$KEEP_DAILY" \
--keep-weekly "$KEEP_WEEKLY" \
--keep-monthly "$KEEP_MONTHLY" \
"$BORG_REPO" \
2>&1 | tee -a "$LOG_FILE"
# Con Borg 1.2+ il prune non libera spazio: serve il compact
borg compact "$BORG_REPO" 2>&1 | tee -a "$LOG_FILE"
}
cleanup_dumps() {
log "=== Pulizia dump temporanei ==="
find "$DUMP_DIR" -name "vzdump-*" -mmin +180 -delete
log "Dump temporanei rimossi."
}
# ── Main ─────────────────────────────────────────────────────
BACKUP_OK=true
log "======================================================"
log "Avvio backup Proxmox → Borg | ${DATE}"
log "======================================================"
check_nas_mount || BACKUP_OK=false
$BACKUP_OK && backup_vms_and_ct || BACKUP_OK=false
$BACKUP_OK && backup_dumps_to_borg || BACKUP_OK=false
$BACKUP_OK && backup_pve_config || BACKUP_OK=false
$BACKUP_OK && prune_old_backups || BACKUP_OK=false
# cleanup_dumps gira SEMPRE, anche se un backup precedente è fallito,
# per evitare che i dump locali si accumulino e riempiano lo spazio su /
log "=== Pulizia dump locali (indipendente da esito backup) ==="
cleanup_dumps || log "ATTENZIONE: cleanup_dumps ha riportato un errore"
if $BACKUP_OK; then
log "Backup completato con successo!"
printf "Subject: Backup Proxmox OK - %s\nFrom: %s\nTo: %s\n\nBackup completato con successo il %s.\n\nDettagli nel log:\n%s" \
"$DATE" "$EMAIL_FROM" "$EMAIL_TO" "$DATE" \
"$(tail -20 "$LOG_FILE")" \
| msmtp "$EMAIL_TO"
else
log "ERRORE: il backup non è andato a buon fine."
printf "Subject: Backup Proxmox FALLITO - %s\nFrom: %s\nTo: %s\n\nIl backup del %s è fallito.\n\nUltime righe del log:\n%s" \
"$DATE" "$EMAIL_FROM" "$EMAIL_TO" "$DATE" \
"$(tail -40 "$LOG_FILE")" \
| msmtp "$EMAIL_TO"
exit 1
fi
Due scelte di progetto che contano
cleanup_dumps fuori dalla catena &&. In una prima versione la pulizia dei dump era agganciata con && al resto. Se un backup falliva, la pulizia non girava mai: i dump rimanevano sul disco, riempivano pve-root e il backup successivo falliva a sua volta. Un circolo vizioso. Oggi la pulizia è indipendente dall’esito.
Due prefissi, due retention. Gli archivi si chiamano proxmox-DATA (i dump) e pve-config-DATA (la configurazione). Il prune usa --glob-archives per applicare la retention a ciascun gruppo separatamente: così una serie di backup falliti non cancella per errore gli archivi dell’altro tipo.
2. La passphrase fuori dallo script
Mettere BORG_PASSPHRASE in chiaro nello script è comodo ma pessimo: finisce nei backup, negli screenshot e in qualunque copia dello script. La sposto in un file leggibile solo da root.
# crea il file con permessi restrittivi fin dall'inizio
( umask 077; cat > /etc/borg-backup.env <<'EOF'
export BORG_PASSPHRASE="LA-TUA-PASSPHRASE"
EOF
)
chown root:root /etc/borg-backup.env
chmod 600 /etc/borg-backup.env
Verifica che funzioni:
bash -c 'source /etc/borg-backup.env; borg list --last 1 /mnt/nas-backup/borg-proxmox'
Se la passphrase è finita in chiaro in qualche posto non sicuro, cambiala. Con repokey il cambio è immediato e non riscrive il repository:
source /etc/borg-backup.env
borg key change-passphrase /mnt/nas-backup/borg-proxmox
# poi aggiorna il valore in /etc/borg-backup.env
Esporta anche la chiave del repository e conservala fuori dall’host, insieme alla passphrase, nel password manager. Senza chiave e passphrase il repository non si apre più:
borg key export /mnt/nas-backup/borg-proxmox /root/borg-key-backup.txt
# copiala altrove e poi cancella il file locale
3. Pianificazione con cron
Rendi lo script eseguibile e provalo a mano una volta, osservando il log:
chmod 700 /usr/local/bin/proxmox-borg-backup.sh
bash -n /usr/local/bin/proxmox-borg-backup.sh && echo "sintassi ok"
/usr/local/bin/proxmox-borg-backup.sh
tail -50 /var/log/borg-backup.log
Poi pianificalo con crontab -e come root:
0 8 * * * /usr/local/bin/proxmox-borg-backup.sh
Parte ogni giorno alle 08:00. Scegli un orario in cui l’host e il NAS non siano impegnati in altro. Il NAS, ad esempio, può essere impostato per ibernare i dischi dopo un periodo di inattività: il montaggio all’inizio dello script lo risveglia.
Per controllare cosa è pianificato:
crontab -l
Attenzione al lock. Borg tiene un lock sul repository mentre lavora. Non lanciare lo script a mano mentre sta girando il cron, e non avviare un controllo dalla UI nello stesso momento.
4. Borg-UI come interfaccia di controllo
Borg-UI si installa su Proxmox con lo script della community (community-scripts), che crea un container LXC con Docker già pronto. Io l’ho messo nel CT 106, con 2 core, 4 GB di RAM e IP fisso.
L’obiettivo: far vedere a Borg-UI il repository che sta sul NAS e che l’host scrive ogni mattina. Ecco tutti i passaggi, compresi gli ostacoli incontrati.
4.1 Il primo ostacolo: i permessi sul bind mount
Il container è unprivileged e ha un bind mount verso /mnt/nas-backup. Ho provato a leggere il repository dal container:
pct exec 106 -- ls -ln /mnt/nas-backup/borg-proxmox
# ls: cannot access '/mnt/nas-backup/borg-proxmox': Permission denied
La causa: in un container unprivileged l’utente root dell’host (uid 0) viene mappato su nobody (uid 65534). I file del repository appartengono a root con permessi 0600, quindi dal container non sono leggibili.
Ci sono due strade: rimappare gli uid con lxc.idmap (delicato, perché tocca i permessi di tutto il mount) oppure far eseguire Borg sull’host via SSH. Ho scelto la seconda: Borg gira sull’host come root e il container fa solo da interfaccia.
4.2 Chiave SSH dal container verso l’host
Genero una chiave nel container e la autorizzo sull’host:
# nel container: installa il client e genera la chiave
pct exec 106 -- bash -c 'apt-get install -y openssh-client; \
[ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519'
# sull'host: aggiungi la chiave pubblica del container
pct exec 106 -- cat /root/.ssh/id_ed25519.pub >> /root/.ssh/authorized_keys
Su Proxmox /root/.ssh/authorized_keys è un link simbolico a /etc/pve/priv/authorized_keys: la scrittura funziona normalmente.
4.3 Secondo ostacolo: il root login via SSH è disabilitato
Il test di connessione continuava a rifiutare la chiave. Il log di sshd sull’host ha spiegato il motivo:
sshd-session[...]: ROOT LOGIN REFUSED FROM 192.168.0.25
Nel sshd_config c’era PermitRootLogin no. Non volevo riaprire root a tutta la rete, quindi ho abilitato il login solo con chiave e solo dall’IP del container, con un blocco Match in un file dedicato:
cat > /etc/ssh/sshd_config.d/99-borgui.conf <<'EOF'
Match Address 192.168.0.25
PermitRootLogin prohibit-password
EOF
sshd -t && echo "config ok"
Verifico che la regola valga solo per il container (qualsiasi altro indirizzo resta su no):
sshd -T -C user=root,addr=192.168.0.25,host=x | grep -i permitrootlogin # without-password
sshd -T -C user=root,addr=192.168.0.99,host=x | grep -i permitrootlogin # no
systemctl reload ssh
Il Match Address si basa sull’IP del container, quindi conviene che sia fisso (riserva DHCP sul router o IP statico nel container):
pct set 106 -net0 name=eth0,bridge=vmbr0,ip=192.168.0.25/24,gw=192.168.0.1
pct reboot 106
Ora il test funziona:
pct exec 106 -- ssh -o BatchMode=yes root@192.168.0.10 'borg --version'; echo "exit: $?"
# borg 1.4.0
# exit: 0
4.4 Configurazione in Borg-UI
Importare la chiave SSH. In Macchine Remote → Importa Chiave SSH Esistente si indica il percorso della chiave dentro il container (
/root/.ssh/id_ed25519), senza incollare nulla. Lascio vuoto il percorso della chiave pubblica: lo ricava da solo aggiungendo.pub.Aggiungere la connessione. Con Aggiungi Connessione Manuale inserisco host
192.168.0.10, porta22, utenteroote la chiave importata. Non uso Distribuisci su Server: serve a copiare la chiave tramite password, ma qui il login con password di root è disabilitato e la chiave è già autorizzata.Verificare l’host key. La scheda della connessione mostra Chiave host non verificata. Si controlla confrontando l’impronta con quella dell’host:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pubSe il valore
SHA256:...coincide, si conferma dall’icona dello scudo.Importare il repository esistente. Qui l’errore da evitare: Crea Repository esegue un
borg inite serve per repository nuovi. Per agganciare quello esistente si usa Importa Esistente:- percorso:
/mnt/nas-backup/borg-proxmox - connessione: quella dell’host
- crittografia:
repokey, con la passphrase del repository - compressione:
zstdlivello3, la stessa dello script, così i nuovi archivi sono coerenti
- percorso:
Non creare un piano di backup. Borg-UI propone di creare piani con schedulazione e retention proprie. Finché la pianificazione è affidata al cron, creare un piano che scrive nello stesso repository porterebbe a conflitti di lock e a prune incrociati. Il ruolo di Borg-UI qui è solo il controllo.
4.5 Cosa si vede
Dopo l’importazione la scheda del repository mostra:
- 26 archivi, tra dump (
proxmox-*) e configurazione (pve-config-*); - 517 GB di dimensione totale sul NAS;
- data dell’ultimo backup (quello del mattino, a conferma che il cron funziona);
- crittografia
repokeye compressionezstd,3.
La scheda della connessione remota mostra anche lo spazio del filesystem dell’host. Nel mio caso i ~94 GB corrispondono a pve-root, la partizione che in passato si era riempita di dump rimasti: vederla in percentuale da una UI è un buon sistema di allerta.
Sul repository compaiono anche Ultima compattazione: Mai e Ultimo controllo: Mai. Non significa che lo script non compatti: quei campi si riferiscono solo alle operazioni lanciate da Borg-UI. Il compact è eseguito dallo script alla fine del prune.
5. Cosa ho imparato
- Container unprivileged e bind mount non vanno d’accordo con i file di root. Il problema è strutturale: meglio Borg via SSH che rimappare gli uid.
- Un blocco
Match AddressperPermitRootLoginlimita il rischio: root con sola chiave e solo da un IP. - Dopo il prune serve il compact (Borg 1.2 e successivi), altrimenti il NAS si riempie anche con la retention corretta.
- La pulizia dei dump deve essere indipendente dall’esito del backup, altrimenti un errore innesca una spirale di dischi pieni.
vzdump --compress zstde deduplicazione si ostacolano: i file già compressi cambiano completamente da un giorno all’altro, quindi Borg riesce a deduplicarli poco. Se lo spazio sul NAS è un problema, si può valutare di eseguirevzdumpsenza compressione, ricordando che richiede più spazio temporaneo sull’host.- Cambiare la passphrase se è finita in un posto non sicuro: con
repokeyè questione di un comando.
Nella guida successiva spiego come ripristinare VM, container e configurazione da questi archivi.