Ir al contenido

Automatizar backups incrementales de volúmenes Docker a almacenamiento remoto con rsync y verificación de integridad

Rogelio Guerra Riverón
Autor
Rogelio Guerra Riverón
Construyendo mi propia infraestructura web desde cero. Aquí documento cada paso: servidores, redes, contenedores y lo que vaya surgiendo.

El problema
#

Tenía varios contenedores Docker corriendo en mi servidor doméstico con datos que no podía perder. Los backups manuales no escalan, así que decidí automatizar todo con rsync y cron. Aquí documento lo que funcionó.

La arquitectura
#

Mi setup:

  • Servidor local: Host Docker con volúmenes en /var/lib/docker/volumes/
  • Almacenamiento remoto: NAS en red con SSH habilitado
  • Herramientas: rsync para sincronización incremental, cron para programación, sha256sum para verificación

Preparación
#

Primero, configura acceso SSH sin contraseña desde el host Docker al NAS:

ssh-keygen -t ed25519 -f ~/.ssh/backup_key -N ""
ssh-copy-id -i ~/.ssh/backup_key usuario@nas.local

Verifica que funciona:

ssh -i ~/.ssh/backup_key usuario@nas.local "ls -la"

Script de backup incremental
#

Crea /usr/local/bin/docker-backup.sh:

#!/bin/bash

BACKUP_USER="usuario"
BACKUP_HOST="nas.local"
BACKUP_PATH="/mnt/backups/docker"
SSH_KEY="/root/.ssh/backup_key"
LOG_FILE="/var/log/docker-backup.log"
MANIFEST="/var/log/docker-backup-manifest.txt"

# Volúmenes a respaldar (ajusta según tus necesidades)
VOLUMES=("postgres_data" "nextcloud_data" "app_config")

# Función para logging
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

log "=== Iniciando backup incremental ==="

# Crear directorio remoto si no existe
ssh -i "$SSH_KEY" "$BACKUP_USER@$BACKUP_HOST" "mkdir -p $BACKUP_PATH"

# Función para respaldar volumen
backup_volume() {
    local volume=$1
    local volume_path="/var/lib/docker/volumes/${volume}/_data"
    local remote_path="$BACKUP_USER@$BACKUP_HOST:$BACKUP_PATH/$volume"
    
    if [ ! -d "$volume_path" ]; then
        log "ERROR: Volumen $volume no encontrado en $volume_path"
        return 1
    fi
    
    log "Respaldando volumen: $volume"
    
    # rsync incremental
    rsync -av --delete \
        -e "ssh -i $SSH_KEY" \
        "$volume_path/" \
        "$remote_path/" >> "$LOG_FILE" 2>&1
    
    if [ $? -eq 0 ]; then
        log "✓ Backup completado: $volume"
        return 0
    else
        log "✗ Error en backup: $volume"
        return 1
    fi
}

# Respaldar todos los volúmenes
FAILED=0
for volume in "${VOLUMES[@]}"; do
    backup_volume "$volume" || ((FAILED++))
done

# Generar manifiesto con checksums
log "Generando manifiesto de integridad..."

{
    echo "Manifiesto de Backup - $(date '+%Y-%m-%d %H:%M:%S')"
    echo "=========================================="
    for volume in "${VOLUMES[@]}"; do
        echo "Volumen: $volume"
        find "/var/lib/docker/volumes/${volume}/_data" -type f -exec sha256sum {} \; 2>/dev/null
        echo ""
    done
} > "$MANIFEST"

# Respaldar el manifiesto también
scp -i "$SSH_KEY" "$MANIFEST" "$BACKUP_USER@$BACKUP_HOST:$BACKUP_PATH/manifest-$(date +%Y%m%d).txt"

if [ $FAILED -eq 0 ]; then
    log "=== Backup completado exitosamente ==="
    exit 0
else
    log "=== Backup completado con $FAILED errores ==="
    exit 1
fi

Hazlo ejecutable:

chmod +x /usr/local/bin/docker-backup.sh

Verificación de integridad
#

Crea /usr/local/bin/docker-backup-verify.sh:

#!/bin/bash

BACKUP_USER="usuario"
BACKUP_HOST="nas.local"
BACKUP_PATH="/mnt/backups/docker"
SSH_KEY="/root/.ssh/backup_key"
LOG_FILE="/var/log/docker-backup-verify.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

log "=== Iniciando verificación de integridad ==="

# Descargar manifiesto más reciente
LATEST_MANIFEST=$(ssh -i "$SSH_KEY" "$BACKUP_USER@$BACKUP_HOST" \
    "ls -t $BACKUP_PATH/manifest-*.txt | head -1")

if [ -z "$LATEST_MANIFEST" ]; then
    log "ERROR: No se encontró manifiesto remoto"
    exit 1
fi

scp -i "$SSH_KEY" "$BACKUP_USER@$BACKUP_HOST:$LATEST_MANIFEST" /tmp/manifest-verify.txt

# Extraer y verificar checksums
while IFS= read -r line; do
    if [[ $line =~ ^[a-f0-9]{64} ]]; then
        hash=$(echo "$line" | awk '{print $1}')
        file=$(echo "$line" | awk '{print $2}')
        
        if [ -f "$file" ]; then
            local_hash=$(sha256sum "$file" | awk '{print $1}')
            if [ "$hash" != "$local_hash" ]; then
                log "ALERTA: Checksum inconsistente en $file"
            fi
        fi
    fi
done < /tmp/manifest-verify.txt

log "=== Verificación completada ==="

Programación con cron
#

Edita el crontab:

crontab -e

Añade estas líneas:

# Backup diario a las 2:00 AM
0 2 * * * /usr/local/bin/docker-backup.sh

# Verificación semanal los domingos a las 3:00 AM
0 3 * * 0 /usr/local/bin/docker-backup-verify.sh

# Enviar log por correo si hay errores
0 4 * * * [ -f /var/log/docker-backup.log ] && tail -20 /var/log/docker-backup.log | mail -s "Backup Docker - Resumen" admin@example.com

Monitoreo
#

Revisa los logs regularmente:

tail -f /var/log/docker-backup.log

Para investigar problemas de rsync específicos:

rsync -av --dry-run /var/lib/docker/volumes/postgres_data/_data usuario@nas.local:/mnt/backups/docker/postgres_data/

Resultado
#

Ahora tengo backups incrementales automáticos cada noche. rsync solo transfiere cambios, lo que ahorra ancho de banda. La verificación de integridad me alerta si algo se corrompe. Es simple, func