Yunohost - strategia di backup e recovery: appunti pratici
Gestire un server personale significa accettare una responsabilità molto semplice: prima o poi qualcosa si romperà.
Disco che muore, aggiornamento andato storto, errore umano, oppure la necessità di migrare su una macchina nuova.
Questo post raccoglie la procedura che utilizzo sul mio server YunoHost per avere:
- backup automatici e verificati;
- una copia esterna e fisica dei dati;
- una procedura di ripristino.
È scritto prima di tutto come promemoria personale.
Strategia di backup
-
Backup ufficiale YunoHost
- uso
yunohost backup create(system + apps); - mantengo solo gli ultimi backup automatici.
- uso
-
Snapshot giornalieri su disco esterno
- disco dedicato, montato solo durante il backup;
- snapshot incrementali con
rsync --link-dest; - rotazione automatica (7 giorni).
-
Verifica dell’integrità
- controllo dei file
.tarsia internamente sia sul disco esterno.
- controllo dei file
-
Recovery esplicito
- script separato;
- scelta manuale dello snapshot;
- conferma prima del restore.
Il disco esterno è offline, si monta automaticamente per il backup e poi viene, sempre automaticamente smontato.
Struttura dei backup
Sul disco esterno:
/mnt/disco-backup/
└── yunohost/
└── giornaliero/
├── 2025-12-07-2302/
│ ├── archives/
│ ├── etc/
│ └── home/
├── 2025-12-08-0300/
└── ...
Ogni directory rappresenta uno snapshot coerente del sistema.
Preparazione del disco esterno
Identificazione del disco
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT
Supponiamo che il disco esterno sia /dev/sda.
Inizializzazione
sudo wipefs -a /dev/sda
sudo fdisk /dev/sda
In fdisk:
g→ nuova tabella GPTn→ nuova partizionew→ salva
Risultato atteso: /dev/sda1.
Creazione filesystem
sudo mkfs.ext4 -L disco-backup /dev/sda1
lsblk -f
Test del mount
sudo mkdir -p /mnt/disco-backup
sudo mount /dev/disk/by-label/disco-backup /mnt/disco-backup
df -h | grep disco-backup
sudo umount /mnt/disco-backup
Struttura directory sul disco
sudo mkdir -p /mnt/disco-backup/yunohost/giornaliero
sudo ls -R /mnt/disco-backup
sudo umount /mnt/disco-backup
Script di backup giornaliero
Questo script:
- crea un backup completo YunoHost (
--system --apps); - verifica l’integrità dell’archivio interno (
tar -tf); - monta il disco esterno;
- crea uno snapshot datato e sincronizza:
/home/yunohost.backup/archives/etc/home(escludendo la copia ridondante degli archivi);
- verifica l’integrità dello snapshot sul disco esterno;
- mantiene solo gli ultimi 7 snapshot giornalieri sul disco esterno;
- mantiene solo gli ultimi 3 backup interni
auto-*; - smonta il disco.
Creazione file
sudo nano /usr/local/sbin/backup_yunohost_giornaliero.sh
Contenuto script (copia/incolla)
#!/usr/bin/env bash
set -euo pipefail
# ==========================
# CONFIGURAZIONE
# ==========================
# Etichetta del disco esterno
DISK_LABEL="disco-backup"
# Mount point
MOUNT_POINT="/mnt/disco-backup"
# Directory base per i backup giornalieri sul disco esterno
BASE_DIR="$MOUNT_POINT/yunohost/giornaliero"
# Quanti snapshot giornalieri tenere sul disco esterno
KEEP_DAILY=7
# Dove YunoHost salva gli archivi di backup
YH_BACKUP_DIR="/home/yunohost.backup/archives"
# Quanti backup interni auto-* tenere
KEEP_INTERNAL_AUTO=3
# Cosa sincronizzare sul disco esterno
SOURCES=(
"$YH_BACKUP_DIR" # archivi ufficiali YunoHost
"/etc" # configurazione sistema
"/home" # dati utenti, app, ecc.
)
# ==========================
# FUNZIONI DI UTILITÀ
# ==========================
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
error() {
log "ERRORE: $*"
exit 1
}
mount_disk() {
log "Verifica presenza disco con etichetta '$DISK_LABEL'..."
if [ ! -e "/dev/disk/by-label/${DISK_LABEL}" ]; then
error "Disco con etichetta '${DISK_LABEL}' non trovato. È collegato?"
fi
mkdir -p "$MOUNT_POINT"
if mountpoint -q "$MOUNT_POINT"; then
log "Il disco risulta già montato su $MOUNT_POINT."
else
log "Monto il disco su $MOUNT_POINT..."
mount "/dev/disk/by-label/${DISK_LABEL}" "$MOUNT_POINT"
fi
if ! mountpoint -q "$MOUNT_POINT"; then
error "Impossibile montare il disco su $MOUNT_POINT."
fi
log "Disco montato correttamente."
}
umount_disk() {
if mountpoint -q "$MOUNT_POINT"; then
log "Smonto il disco da $MOUNT_POINT..."
umount "$MOUNT_POINT" || log "ATTENZIONE: impossibile smontare $MOUNT_POINT (forse in uso)."
fi
}
cleanup() {
umount_disk
}
trap cleanup EXIT
check_internal_backup_integrity() {
local backup_name="$1"
local tar_path="$YH_BACKUP_DIR/${backup_name}.tar"
log "Verifico integrità archivio interno YunoHost: $tar_path"
if [ ! -f "$tar_path" ]; then
error "Archivio YunoHost non trovato: $tar_path"
fi
# Controllo base: tar deve riuscire a leggerlo
if ! tar -tf "$tar_path" > /dev/null 2>&1; then
error "Archivio YunoHost corrotto o illeggibile: $tar_path"
fi
log "Archivio interno YunoHost OK."
}
check_snapshot_integrity() {
local snap_dir="$1"
local backup_name="$2"
log "Verifica integrità snapshot su disco esterno: $snap_dir"
# Controllo struttura minima
[[ -d "$snap_dir/etc" ]] || error "Manca la directory etc nello snapshot: $snap_dir"
[[ -d "$snap_dir/home" ]] || error "Manca la directory home nello snapshot: $snap_dir"
[[ -d "$snap_dir/archives" ]] || error "Manca la directory archives nello snapshot: $snap_dir"
local snap_tar="$snap_dir/archives/${backup_name}.tar"
if [ ! -f "$snap_tar" ]; then
error "Archivio YunoHost non trovato nello snapshot: $snap_tar"
fi
log "Verifico integrità archivio YunoHost nello snapshot: $snap_tar"
if ! tar -tf "$snap_tar" > /dev/null 2>&1; then
error "Archivio YunoHost corrotto nello snapshot: $snap_tar"
fi
log "Snapshot su disco esterno OK."
}
cleanup_internal_backups() {
log "Pulizia backup interni auto-* (mantengo ultimi $KEEP_INTERNAL_AUTO)..."
cd "$YH_BACKUP_DIR" || {
log "ATTENZIONE: impossibile entrare in $YH_BACKUP_DIR, salto pulizia interna."
return
}
# Lista dei backup auto-*.tar (senza estensione), ordinati
mapfile -t INTERNAL < <(ls -1 auto-*.tar 2>/dev/null | sed 's/\.tar$//' | sort || true)
local num=${#INTERNAL[@]}
if (( num <= KEEP_INTERNAL_AUTO )); then
log "Nessuna eliminazione necessaria: backup interni auto-* = $num (<= $KEEP_INTERNAL_AUTO)."
return
fi
local to_delete=$((num - KEEP_INTERNAL_AUTO))
log "Trovati $num backup interni auto-*, ne elimino $to_delete più vecchi."
for (( i=0; i<to_delete; i++ )); do
local name="${INTERNAL[$i]}"
log "Elimino backup interno vecchio: $name.(tar|info.json)"
rm -f "${name}.tar" "${name}.info.json"
done
}
# ==========================
# INIZIO SCRIPT
# ==========================
log "===== INIZIO BACKUP GIORNALIERO YUNOHOST ====="
# 1) CREA UN BACKUP COMPLETO YUNOHOST
BACKUP_NAME="auto-$(date +'%Y-%m-%d-%H%M')"
log "Creo backup YunoHost: $BACKUP_NAME"
yunohost backup create --system --apps --name "$BACKUP_NAME"
log "Backup YunoHost completato. Archivi in: $YH_BACKUP_DIR"
# 1b) CONTROLLO DI INTEGRITÀ ARCHIVIO INTERNO
check_internal_backup_integrity "$BACKUP_NAME"
# 2) MONTA DISCO ESTERNO
mount_disk
# Assicuriamoci che la directory base esista
mkdir -p "$BASE_DIR"
# Nome dello snapshot corrente: es. 2025-12-07-2302
SNAP_NAME="$(date +'%Y-%m-%d-%H%M')"
SNAP_DIR="$BASE_DIR/$SNAP_NAME"
log "Creo snapshot corrente sul disco esterno: $SNAP_DIR"
mkdir -p "$SNAP_DIR"
# Trova l'ultimo snapshot (se esiste)
LAST_SNAP="$(ls -1d "$BASE_DIR"/20* 2>/dev/null | sort | tail -n 1 || true)"
if [ -n "$LAST_SNAP" ]; then
log "Ultimo snapshot precedente trovato: $LAST_SNAP"
else
log "Nessun snapshot precedente trovato (primo backup sul disco esterno)."
fi
# Opzioni rsync
RSYNC_OPTS="-aHAX --numeric-ids --delete --info=progress2"
# 3) SINCRONIZZA LE SORGENTI SUL DISCO ESTERNO
for SRC in "${SOURCES[@]}"; do
if [ ! -d "$SRC" ]; then
log "ATTENZIONE: sorgente $SRC non trovata, salto..."
continue
fi
SRC_NAME="$(basename "$SRC")"
DEST_DIR="$SNAP_DIR/$SRC_NAME"
mkdir -p "$DEST_DIR"
log "Backup di $SRC -> $DEST_DIR"
# Opzioni extra per alcune sorgenti
EXTRA_OPTS=()
if [ "$SRC" = "/home" ]; then
# Evita di duplicare gli archivi (li abbiamo già in $YH_BACKUP_DIR)
EXTRA_OPTS+=(--exclude='yunohost.backup/archives')
fi
# Se esiste snapshot precedente con stessa struttura, usa --link-dest
if [ -n "$LAST_SNAP" ] && [ -d "$LAST_SNAP/$SRC_NAME" ]; then
LINK_DEST="--link-dest=$LAST_SNAP/$SRC_NAME"
log "Uso rsync incrementale con hard-link (--link-dest=$LAST_SNAP/$SRC_NAME)."
else
LINK_DEST=""
log "Nessun --link-dest disponibile per $SRC (farà una copia completa)."
fi
rsync $RSYNC_OPTS "${EXTRA_OPTS[@]}" $LINK_DEST "$SRC"/ "$DEST_DIR"/
done
# 3b) CONTROLLO DI INTEGRITÀ DELLO SNAPSHOT SU DISCO ESTERNO
check_snapshot_integrity "$SNAP_DIR" "$BACKUP_NAME"
# 4) ROTAZIONE SNAPSHOT SUL DISCO ESTERNO
log "Gestione rotazione snapshot (mantengo ultimi $KEEP_DAILY)..."
mapfile -t BACKUPS < <(ls -1d "$BASE_DIR"/20* 2>/dev/null | sort || true)
NUM=${#BACKUPS[@]}
if (( NUM > KEEP_DAILY )); then
TO_DELETE=$((NUM - KEEP_DAILY))
log "Trovati $NUM snapshot, ne elimino $TO_DELETE più vecchi."
for (( i=0; i<TO_DELETE; i++ )); do
OLD="${BACKUPS[$i]}"
log "Elimino snapshot vecchio: $OLD"
rm -rf --one-file-system "$OLD"
done
else
log "Nessuna eliminazione necessaria: snapshot presenti = $NUM (<= $KEEP_DAILY)."
fi
# 5) PULIZIA BACKUP INTERNI AUTO-*
cleanup_internal_backups
log "===== FINE BACKUP GIORNALIERO YUNOHOST ====="
Rendere eseguibile lo script
sudo chmod +x /usr/local/sbin/backup_yunohost_giornaliero.sh
Test manuale
sudo /usr/local/sbin/backup_yunohost_giornaliero.sh
Recovery da disco esterno
Questa sezione descrive come ripristinare un server YunoHost da un backup
salvato sul disco esterno disco-backup.
Lo script:
- monta il disco esterno;
- sceglie lo snapshot e l’archivio di backup;
- copia l’archivio in
/home/yunohost.backup/archives; - verifica l’integrità del
.tar; - chiede conferma;
- esegue
yunohost backup restore.
Creazione script
sudo nano /usr/local/sbin/yunohost_recovery_from_disk.sh
Contenuto script
#!/usr/bin/env bash
set -euo pipefail
# ==========================
# CONFIGURAZIONE
# ==========================
# Etichetta del disco esterno con i backup
DISK_LABEL="disco-backup"
# Punto di mount del disco esterno
MOUNT_POINT="/mnt/disco-backup"
# Directory base degli snapshot giornalieri sul disco esterno
BASE_DIR="$MOUNT_POINT/yunohost/giornaliero"
# Directory interna dove YunoHost si aspetta gli archivi di backup
YH_BACKUP_DIR="/home/yunohost.backup/archives"
# ==========================
# FUNZIONI DI UTILITÀ
# ==========================
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
error() {
log "ERRORE: $*"
exit 1
}
mount_disk() {
log "Verifico presenza disco con etichetta '$DISK_LABEL'..."
if [ ! -e "/dev/disk/by-label/${DISK_LABEL}" ]; then
error "Disco con etichetta '${DISK_LABEL}' non trovato. È collegato?"
fi
mkdir -p "$MOUNT_POINT"
if mountpoint -q "$MOUNT_POINT"; then
log "Il disco risulta già montato su $MOUNT_POINT."
else
log "Monto il disco su $MOUNT_POINT..."
mount "/dev/disk/by-label/${DISK_LABEL}" "$MOUNT_POINT"
fi
if ! mountpoint -q "$MOUNT_POINT"; then
error "Impossibile montare il disco su $MOUNT_POINT."
fi
log "Disco montato correttamente."
}
umount_disk() {
if mountpoint -q "$MOUNT_POINT"; then
log "Smonto il disco da $MOUNT_POINT..."
umount "$MOUNT_POINT" || log "ATTENZIONE: impossibile smontare $MOUNT_POINT (forse in uso). Lascio montato."
fi
}
cleanup() {
umount_disk
}
trap cleanup EXIT
choose_snapshot() {
local snapshot_arg="$1"
if [ -n "$snapshot_arg" ]; then
# Può essere un nome tipo 2025-12-07-2302 o un path completo
if [[ "$snapshot_arg" == /* ]]; then
SNAP_DIR="$snapshot_arg"
else
SNAP_DIR="$BASE_DIR/$snapshot_arg"
fi
else
log "Nessuno snapshot specificato, scelgo l'ultimo disponibile..."
SNAP_DIR="$(ls -1d "$BASE_DIR"/20* 2>/dev/null | sort | tail -n 1 || true)"
fi
if [ -z "${SNAP_DIR:-}" ] || [ ! -d "$SNAP_DIR" ]; then
error "Snapshot non trovato. Controlla che ci siano directory tipo $BASE_DIR/2025-12-07-2302"
fi
log "Snapshot selezionato: $SNAP_DIR"
}
choose_backup_name() {
local backup_arg="$1"
local archives_dir="$SNAP_DIR/archives"
if [ ! -d "$archives_dir" ]; then
error "La directory archives non esiste nello snapshot: $archives_dir"
fi
if [ -n "$backup_arg" ]; then
BACKUP_NAME="$backup_arg"
else
log "Nessun nome di backup specificato, scelgo l'ultimo backup auto-* nello snapshot..."
local latest_tar
latest_tar="$(ls -1 "$archives_dir"/auto-*.tar 2>/dev/null | sort | tail -n 1 || true)"
if [ -z "$latest_tar" ]; then
error "Nessun archivio auto-*.tar trovato in $archives_dir"
fi
BACKUP_NAME="$(basename "$latest_tar" .tar)"
fi
log "Backup selezionato: $BACKUP_NAME (snapshot: $SNAP_DIR)"
}
copy_backup_to_local() {
local src_tar="$SNAP_DIR/archives/${BACKUP_NAME}.tar"
local src_info="$SNAP_DIR/archives/${BACKUP_NAME}.info.json"
if [ ! -f "$src_tar" ]; then
error "Archivio .tar non trovato nello snapshot: $src_tar"
fi
mkdir -p "$YH_BACKUP_DIR"
log "Copio l'archivio di backup su $YH_BACKUP_DIR..."
cp -a "$src_tar" "$YH_BACKUP_DIR/"
if [ -f "$src_info" ]; then
log "Copio anche il file info.json..."
cp -a "$src_info" "$YH_BACKUP_DIR/"
else
log "ATTENZIONE: file .info.json non trovato per $BACKUP_NAME, procedo solo con .tar."
fi
LOCAL_TAR="$YH_BACKUP_DIR/${BACKUP_NAME}.tar"
}
check_local_backup_integrity() {
log "Verifico integrità dell'archivio locale: $LOCAL_TAR"
if ! tar -tf "$LOCAL_TAR" > /dev/null 2>&1; then
error "Archivio locale corrotto o illeggibile: $LOCAL_TAR"
fi
log "Archivio locale OK."
}
run_restore() {
log "Pronto a eseguire: yunohost backup restore $BACKUP_NAME"
echo
echo "ATTENZIONE:"
echo " - Questo ripristino sovrascriverà la configurazione e i dati del server YunoHost corrente."
echo " - Assicurati di eseguirlo su una reinstallazione pulita o di sapere cosa stai facendo."
echo
read -r -p "Vuoi procedere con il ripristino? [yes/N]: " answer
case "$answer" in
yes|y|Y)
log "Eseguo il ripristino..."
yunohost backup restore "$BACKUP_NAME"
log "Ripristino completato (controlla l'output di yunohost per eventuali messaggi)."
;;
*)
log "Ripristino annullato dall'utente."
exit 1
;;
esac
}
# ==========================
# INIZIO SCRIPT
# ==========================
# Argomenti:
# $1 = (opzionale) nome snapshot (es. 2025-12-07-2302) o path completo
# $2 = (opzionale) nome backup (es. auto-2025-12-07-2301)
SNAPSHOT_ARG="${1:-}"
BACKUP_ARG="${2:-}"
log "===== SCRIPT DI RECOVERY YUNOHOST DA DISCO ESTERNO ====="
mount_disk
choose_snapshot "$SNAPSHOT_ARG"
choose_backup_name "$BACKUP_ARG"
copy_backup_to_local
check_local_backup_integrity
run_restore
log "===== FINE SCRIPT DI RECOVERY ====="
Rendere eseguibile lo script
sudo chmod +x /usr/local/sbin/yunohost_recovery_from_disk.sh
Utilizzo
Ultimo snapshot e ultimo backup auto-*:
sudo /usr/local/sbin/yunohost_recovery_from_disk.sh
Snapshot specifico:
sudo /usr/local/sbin/yunohost_recovery_from_disk.sh 2025-12-07-2302
Snapshot e nome backup preciso:
sudo /usr/local/sbin/yunohost_recovery_from_disk.sh 2025-12-07-2302 auto-2025-12-07-2301