Tutoriales 9 min de lectura

Respaldos incrementales con rsync y scripts Bash

Guía paso a paso para crear un sistema de backups incrementales con rsync y Bash que respalda archivos eficientemente, conserva versiones históricas y se automatiza con cron.

Terminal mostrando ejecución de rsync transfiriendo archivos incrementales con estadísticas de transferencia
Terminal mostrando ejecución de rsync transfiriendo archivos incrementales con estadísticas de transferencia

El backup con Python que construimos antes es perfecto para bases de datos y archivos que necesitan lógica de procesamiento. Pero para respaldos de archivos puros — directorios de datos, uploads de usuarios, configuraciones de servidor — rsync es más eficiente: solo transfiere los bytes que cambiaron, maneja permisos y ownership correctamente, y con hard links puedes mantener versiones diarias que ocupan una fracción del espacio total.

¿Por qué rsync?

rsync compara los archivos entre origen y destino y solo transfiere las diferencias. Cuando combinas rsync con hard links, obtienes lo mejor de los dos mundos — backups rápidos (solo se copian los cambios) con restauración simple (cada backup es una carpeta completa que puedes navegar y copiar directamente).

MétodoPrimer backup 100 GBBackup diario (500 MB cambios)30 días de backups
cp -r100 GB, 30 min100 GB, 30 min3 TB
tar.gz100 GB, 45 min100 GB, 45 min3 TB (comprimido ~1 TB)
rsync incremental100 GB, 30 min500 MB, 30 seg~115 GB

Requisitos previos

rsync viene preinstalado en Ubuntu. Verifica:

rsync --version

Si respaldar a un servidor remoto, necesitas acceso SSH con llaves (sin contraseña).

Paso 1: rsync básico

Antes de automatizar, entiende el comando base:

rsync -avz --delete /origen/ /destino/
FlagFunción
-aArchive — preserva permisos, ownership, timestamps, symlinks
-vVerbose — muestra los archivos que transfiere
-zCompress — comprime durante la transferencia (útil en remoto)
--deleteElimina archivos en destino que ya no existen en origen

La barra final importa

/origen/ (con barra) copia el contenido del directorio. /origen (sin barra) copia el directorio mismo. La diferencia: /datos/ → los archivos de datos van directo al destino. /datos → se crea una carpeta datos dentro del destino. Para backups, generalmente quieres la barra.

Ejemplo: backup local

# Respaldar /var/www a /backup
rsync -avz --delete /var/www/ /backup/www/

Ejemplo: backup a servidor remoto

# Respaldar /var/www al servidor de backup
rsync -avz --delete -e "ssh -p 2222" /var/www/ backup@10.0.1.50:/backups/web/

La técnica de hard links es la joya de rsync para backups. Cada backup diario es una carpeta completa (puedes navegar y restaurar cualquier archivo directamente), pero los archivos que no cambiaron son hard links al backup anterior — no ocupan espacio adicional.

rsync -avz --delete \
  --link-dest=/backup/latest \
  /datos/ \
  /backup/2026-01-08/

--link-dest le dice a rsync: "antes de copiar un archivo, verifica si es idéntico al que está en /backup/latest. Si es idéntico, crea un hard link en vez de copiar." El resultado: solo los archivos que cambiaron se copian realmente.

/backup/
├── 2026-01-06/      ← Backup del 6 (completo)
│   ├── archivo1.txt   (inode 12345)
│   ├── archivo2.txt   (inode 12346)
│   └── archivo3.txt   (inode 12347)
├── 2026-01-07/      ← Backup del 7 (solo archivo2 cambió)
│   ├── archivo1.txt   (inode 12345) ← Hard link, 0 bytes extra
│   ├── archivo2.txt   (inode 12400) ← Copia nueva, cambió
│   └── archivo3.txt   (inode 12347) ← Hard link, 0 bytes extra
├── 2026-01-08/      ← Backup del 8 (solo archivo1 cambió)
│   ├── archivo1.txt   (inode 12500) ← Copia nueva, cambió
│   ├── archivo2.txt   (inode 12400) ← Hard link al del 7
│   └── archivo3.txt   (inode 12347) ← Hard link al del 6
└── latest -> 2026-01-08/  ← Symlink al más reciente

Cada carpeta parece un backup completo, pero solo los archivos modificados ocupan espacio real.

Paso 3: Script de backup completo

#!/bin/bash
# backup-rsync.sh — Backup incremental con rsync y hard links
# Uso: sudo ./backup-rsync.sh

set -euo pipefail

# =====================
# CONFIGURACIÓN
# =====================
BACKUP_ROOT="/backup"
SOURCE_DIRS=(
    "/var/www"
    "/etc/nginx"
    "/etc/postgresql"
    "/opt/apps"
    "/home"
)
RETENTION_DAYS=30
LOG_DIR="${BACKUP_ROOT}/logs"
DATE=$(date +%Y-%m-%d_%H%M%S)
BACKUP_DIR="${BACKUP_ROOT}/${DATE}"
LATEST_LINK="${BACKUP_ROOT}/latest"
LOG_FILE="${LOG_DIR}/backup-${DATE}.log"

# Servidor remoto (dejar vacío para backup local)
REMOTE_HOST=""
REMOTE_DIR=""
# REMOTE_HOST="backup@10.0.1.50"
# REMOTE_DIR="/backups/servidor-web"

# Notificación Slack (opcional)
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"

# =====================
# FUNCIONES
# =====================
mkdir -p "$LOG_DIR"

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

notify_slack() {
    local message="$1"
    local color="$2"
    if [[ -n "$SLACK_WEBHOOK" ]]; then
        curl -s -X POST "$SLACK_WEBHOOK" \
            -H "Content-Type: application/json" \
            -d "{\"attachments\":[{\"color\":\"${color}\",\"text\":\"${message}\"}]}" \
            > /dev/null 2>&1 || true
    fi
}

cleanup_old_backups() {
    log "Limpiando backups con más de ${RETENTION_DAYS} días..."
    local count=0
    local target_dir="${1:-$BACKUP_ROOT}"

    find "$target_dir" -maxdepth 1 -type d -name "20*" -mtime +${RETENTION_DAYS} | sort | while read -r dir; do
        log "  Eliminando: $(basename "$dir")"
        rm -rf "$dir"
        ((count++)) || true
    done

    log "  Backups eliminados: ${count}"
}

# =====================
# EJECUCIÓN
# =====================
log "=========================================="
log "BACKUP INICIADO — ${DATE}"
log "=========================================="

START_TIME=$(date +%s)
TOTAL_SIZE=0
ERRORS=0

# Crear directorio de backup
mkdir -p "$BACKUP_DIR"

# Construir --link-dest
LINK_DEST_FLAG=""
if [[ -L "$LATEST_LINK" ]] && [[ -d "$LATEST_LINK" ]]; then
    LINK_DEST_FLAG="--link-dest=${LATEST_LINK}"
    log "Usando link-dest: $(readlink -f "$LATEST_LINK")"
else
    log "Sin link-dest (primer backup completo)"
fi

# Respaldar cada directorio
for SRC in "${SOURCE_DIRS[@]}"; do
    if [[ ! -d "$SRC" ]]; then
        log "${SRC} no existe, saltando..."
        continue
    fi

    DEST_NAME=$(echo "$SRC" | tr '/' '_' | sed 's/^_//')
    DEST="${BACKUP_DIR}/${DEST_NAME}"
    mkdir -p "$DEST"

    log "Respaldando: ${SRC}${DEST_NAME}"

    if rsync -a --delete $LINK_DEST_FLAG \
        --exclude='.cache' \
        --exclude='node_modules' \
        --exclude='__pycache__' \
        --exclude='*.log' \
        --exclude='.git' \
        "$SRC/" "$DEST/" >> "$LOG_FILE" 2>&1; then

        DIR_SIZE=$(du -sh "$DEST" | cut -f1)
        log "${SRC} completado (${DIR_SIZE})"
    else
        log "  ✗ ERROR en ${SRC}"
        ((ERRORS++)) || true
    fi
done

# Actualizar symlink latest
rm -f "$LATEST_LINK"
ln -s "$BACKUP_DIR" "$LATEST_LINK"

# Backup remoto (opcional)
if [[ -n "$REMOTE_HOST" ]] && [[ -n "$REMOTE_DIR" ]]; then
    log "Sincronizando a servidor remoto: ${REMOTE_HOST}:${REMOTE_DIR}"

    if rsync -az --delete \
        -e "ssh -o StrictHostKeyChecking=no" \
        "${BACKUP_DIR}/" \
        "${REMOTE_HOST}:${REMOTE_DIR}/${DATE}/" >> "$LOG_FILE" 2>&1; then
        log "  ✓ Sincronización remota completada"
    else
        log "  ✗ ERROR en sincronización remota"
        ((ERRORS++)) || true
    fi
fi

# Limpieza
cleanup_old_backups "$BACKUP_ROOT"

# Resumen
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
TOTAL_SIZE=$(du -sh "$BACKUP_DIR" | cut -f1)
DISK_FREE=$(df -h "$BACKUP_ROOT" | tail -1 | awk '{print $4}')

log "=========================================="
log "BACKUP COMPLETADO"
log "  Duración: ${DURATION}s"
log "  Tamaño: ${TOTAL_SIZE}"
log "  Errores: ${ERRORS}"
log "  Espacio libre: ${DISK_FREE}"
log "=========================================="

# Notificación
if [[ $ERRORS -eq 0 ]]; then
    notify_slack "✅ Backup completado | ${TOTAL_SIZE} | ${DURATION}s | $(hostname)" "#36a64f"
else
    notify_slack "🚨 Backup con ${ERRORS} error(es) | $(hostname) | Ver log: ${LOG_FILE}" "#ff0000"
fi

exit $ERRORS

Paso 4: Configurar permisos y probar

# Hacer ejecutable
chmod +x backup-rsync.sh

# Probar manualmente
sudo ./backup-rsync.sh

Verifica la estructura de backups:

ls -la /backup/
# Deberías ver el directorio con fecha y el symlink latest

# Verificar que latest apunta al backup reciente
readlink -f /backup/latest

# Ver tamaño real vs aparente (hard links)
du -sh /backup/2026-01-08/       # Tamaño aparente (completo)
du -sh --apparent-size /backup/   # Tamaño real en disco

Paso 5: Automatizar con cron

sudo crontab -e
# Backup diario a las 2:00 AM
0 2 * * * /opt/scripts/backup-rsync.sh >> /var/log/backup-rsync-cron.log 2>&1

Paso 6: Restaurar archivos

La ventaja de rsync con hard links es que restaurar es trivial — cada backup es una carpeta navegable:

# Restaurar un archivo específico del backup de ayer
cp /backup/2026-01-07/var_www/uploads/documento.pdf /var/www/uploads/

# Restaurar un directorio completo
rsync -av /backup/2026-01-07/var_www/ /var/www/

# Restaurar desde el backup más reciente
rsync -av /backup/latest/var_www/ /var/www/

No necesitas herramientas especiales ni procesos de extracción — solo cp o rsync.

Paso 7: Backup a servidor remoto

Para cumplir con la regla 3-2-1, envía los backups a otro servidor. Modifica las variables del script:

REMOTE_HOST="backup@10.0.1.50"
REMOTE_DIR="/backups/servidor-web"

O ejecuta rsync manualmente para enviar el último backup:

rsync -avz -e "ssh -p 2222" /backup/latest/ backup@10.0.1.50:/backups/servidor-web/latest/

Ancho de banda

rsync con -z comprime los datos durante la transferencia. Para backups iniciales grandes sobre internet, considera ejecutarlo con --bwlimit=10000 (10 MB/s) para no saturar tu enlace. Los incrementales diarios suelen ser pequeños y no necesitan limitación.

Monitoreo de backups

Verificar que el backup se ejecutó

# Verificar la fecha del último backup
stat -c %y /backup/latest

# Verificar con Zabbix o Prometheus
# Item: antigüedad del último backup en minutos
find /backup -maxdepth 1 -type d -name "20*" -mmin -1500 | wc -l
# Si el resultado es 0, no hubo backup en las últimas 25 horas → alerta

Verificar integridad

# Comparar el backup con el origen (dry-run, no copia nada)
rsync -avn --delete /var/www/ /backup/latest/var_www/
# Si la salida está vacía, el backup es idéntico al origen

rsync vs herramientas especializadas

HerramientaMejor paraLimitación
rsyncArchivos, directorios, configuracionesNo deduplica entre archivos diferentes
BorgBackupDeduplicación avanzada, cifradoMás complejo de configurar
resticBackup cifrado a cloud (S3, B2)Restauración más lenta que rsync
VeeamVMs completas (Proxmox, VMware)Licencia comercial
Python scriptBases de datos + lógica customMás código que mantener

Para archivos de servidor y directorios de datos, rsync con hard links es difícil de superar en simplicidad y eficiencia.

Siguientes pasos

Con backups incrementales funcionando:

  • BorgBackup — deduplicación y cifrado para backups que van a destinos no confiables
  • Backup de PostgreSQL — combina este script de rsync para archivos con el de Python para bases de datos
  • RAID — protección del disco de backups contra fallo de hardware
  • Monitoreo — alertas si el backup no se ejecutó o si el disco de backups está lleno
  • Infraestructura de backup profesional — estrategia 3-2-1 completa con Veeam, Proxmox Backup Server y pruebas de restauración

Respaldos empresariales

¿Tu estrategia de backup está completa y probada?

Diseñamos tu plan de backup con rsync, Veeam o Proxmox Backup Server, con retención, cifrado y pruebas de restauración periódicas.

Solicitar evaluación

Preguntas frecuentes

Temas relacionados

#backup#rsync#bash#linux#servidores#tutorial

¿Te fue útil? Compártelo

Artículos relacionados

Ver todos

Consultoría gratuita

¿Necesitas una estrategia de backup profesional?

Diseñamos e implementamos tu plan de backup con rsync, Veeam o Proxmox Backup Server, con pruebas de restauración periódicas.

Solicitar evaluación