Backup automático en Linux con cron y rsync: rutina diaria local + offsite

Configura backup automático en servidores Linux usando cron y rsync, con retención, log y copia offsite por SSH. Guía técnica paso a paso.

Backup automatizado en servidor Linux no es un lujo — es la única cosa entre tú y una mala noche cuando el disco falla, el filesystem se corrompe o alguien ejecuta rm -rf en el directorio equivocado. A pesar de eso, muchos sysadmins aún postergan esa decisión para “después” y descubren a la hora del incidente que el último snapshot es de hace seis meses.

Este tutorial monta una rutina diaria de backup local con rsync (usando snapshots por hardlink para ahorrar espacio) y replicación offsite por SSH, agendada con cron. Todo en un script idempotente, con log estructurado y retención configurable. Foco en servidor Linux genérico (Debian/Ubuntu/Rocky/Alma) ejecutándose como VPS — sin dependencias exóticas, sin agente propietario.

Tiempo estimado de ejecución: 30-40 minutos para configurar la primera vez, después se ejecuta solo. La restauración de un archivo individual tarda segundos; la restauración completa depende del volumen de datos.

Requisitos previos

Lo que necesitas antes de empezar

Servidor Linux con acceso root (o sudo completo), rsync instalado, cron activo y un segundo servidor (o storage remoto) accesible por SSH para la copia offsite. Espacio en disco en el destino local de al menos 2x el tamaño de lo que será copiado, para acomodar el historial de snapshots.

SO probado Ubuntu 24.04 / Debian 12
rsync mínimo 3.2.x
Espacio local >= 2x dataset
Destino offsite SSH con clave

Verifica las versiones antes de continuar:

rsync --version | head -n 1
systemctl status cron      # debian/ubuntu
systemctl status crond     # rhel/rocky/alma

Si rsync no está instalado: sudo apt install rsync (Debian/Ubuntu) o sudo dnf install rsync (Rocky/Alma).

Estructura de directorios y estrategia

Antes de escribir el script, define el layout. La receta usa tres niveles:

  • /backup/local/daily/YYYY-MM-DD/ — snapshots diarios (mantenidos 7 días)
  • /backup/local/weekly/YYYY-WW/ — snapshots semanales (mantenidos 4 semanas)
  • /backup/local/monthly/YYYY-MM/ — snapshots mensuales (mantenidos 6 meses)

Cada snapshot diario es una copia completa aparente, pero rsync --link-dest reaprovecha los archivos no modificados como hardlinks del día anterior. Resultado: 30 días de backup ocupan poco más que 1 copia completa + deltas de cambios.

La replicación offsite copia solo el snapshot más reciente (current) a un servidor remoto, creando allí la misma estructura de historial si así se desea (o solo sobrescribiendo la copia más nueva).

Script de backup local

Esta sección arma el script principal que cron va a disparar todos los días. Hace snapshot incremental con hardlinks, rota la retención y genera log.

01

Crea el directorio base y el usuario dedicado (opcional pero recomendado en servidor compartido):

sudo mkdir -p /backup/local/{daily,weekly,monthly}
sudo mkdir -p /var/log/backup
sudo chown -R root:root /backup /var/log/backup
sudo chmod 700 /backup

El permiso 700 garantiza que solo root lee el contenido de los backups — protección básica contra lectura indebida si otra cuenta es comprometida.

02

Crea el script /usr/local/sbin/backup-daily.sh con el contenido a continuación. Es idempotente: ejecutarlo dos veces el mismo día no duplica nada.

#!/bin/bash
set -euo pipefail

# Configuración
SOURCE_DIRS="/etc /home /var/www /var/lib/mysql-dumps"
BACKUP_ROOT="/backup/local"
LOG_FILE="/var/log/backup/backup-$(date +%Y-%m).log"
RETENTION_DAILY=7
RETENTION_WEEKLY=4
RETENTION_MONTHLY=6

TODAY=$(date +%Y-%m-%d)
DEST="$BACKUP_ROOT/daily/$TODAY"
LATEST_LINK="$BACKUP_ROOT/daily/current"

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

log "===== Inicio del backup $TODAY ====="

# Detecta snapshot anterior para --link-dest
LINK_DEST_ARG=""
if [ -d "$LATEST_LINK" ]; then
    LINK_DEST_ARG="--link-dest=$(readlink -f "$LATEST_LINK")"
    log "Usando link-dest: $LINK_DEST_ARG"
fi

mkdir -p "$DEST"

# Sync con rsync
rsync -aHAX --delete \
    --numeric-ids \
    $LINK_DEST_ARG \
    $SOURCE_DIRS \
    "$DEST/" >> "$LOG_FILE" 2>&1

# Actualiza symlink 'current'
ln -snf "$DEST" "$LATEST_LINK"

log "Backup local completado. Tamaño del snapshot:"
du -sh "$DEST" >> "$LOG_FILE" 2>&1

# Retención: elimina snapshots diarios antiguos
find "$BACKUP_ROOT/daily" -maxdepth 1 -type d -name "20*" -mtime +$RETENTION_DAILY -exec rm -rf {} \;
log "Retención diaria aplicada (manteniendo $RETENTION_DAILY días)"

log "===== Fin del backup $TODAY ====="

Guarda y dale permiso de ejecución:

sudo chmod 700 /usr/local/sbin/backup-daily.sh
03

Prueba el script manualmente antes de programarlo:

sudo /usr/local/sbin/backup-daily.sh

Verifica el log generado:

tail -n 50 /var/log/backup/backup-$(date +%Y-%m).log

Confirma que el snapshot existe y tiene contenido:

ls -lah /backup/local/daily/
du -sh /backup/local/daily/current/

Ejecútalo nuevamente en secuencia. La segunda ejecución debería ser mucho más rápida — rsync solo transfiere lo que cambió y usa hardlink para el resto.

Atención a datos en uso

Bases de datos activas (MySQL, Postgres) no deben ser copiadas directamente desde el directorio de datos — corres el riesgo de corromper el snapshot. Genera un dump consistente antes del rsync (ej: mysqldump --single-transaction) a un directorio que el rsync copie. El ejemplo anterior usa /var/lib/mysql-dumps justamente para ese flujo.

Replicación offsite por SSH

El backup local protege contra fallos de aplicación o error humano. No protege contra la pérdida total del servidor (incendio, hipervisor corrompido, acceso comprometido). Para eso, replica a otro servidor — idealmente en una región diferente.

04

En el servidor de origen, genera un par de claves dedicado al backup:

sudo ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N "" -C "backup@$(hostname)"
sudo chmod 600 /root/.ssh/backup_key

Sin passphrase porque cron no puede escribirla. La protección es la restricción vía authorized_keys en el destino, mostrada en el siguiente paso.

05

En el servidor de destino, crea un usuario dedicado (ej: backup-receiver) y agrega la clave pública con restricción de comando:

# En el destino:
sudo useradd -m -s /bin/bash backup-receiver
sudo mkdir -p /home/backup-receiver/.ssh /backup/remote
sudo chown -R backup-receiver: /home/backup-receiver /backup/remote

Edita /home/backup-receiver/.ssh/authorized_keys agregando la clave pública generada (/root/.ssh/backup_key.pub del origen) prefijada con la restricción:

command="rsync --server -logDtprRe.iLsfxC --delete . /backup/remote/",no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAA... backup@servidor-origen

La directiva command="..." fuerza al destino a aceptar solo esa invocación específica del rsync — incluso si la clave se filtra, el atacante no consigue shell. Ajusta --server según las opciones que use tu rsync local (mira lo que exige ejecutando rsync -e "ssh -v" ... en modo verbose para capturar la línea exacta).

06

Crea el script /usr/local/sbin/backup-offsite.sh en el origen:

#!/bin/bash
set -euo pipefail

REMOTE_USER="backup-receiver"
REMOTE_HOST="backup.ejemplo.com"
LOCAL_CURRENT="/backup/local/daily/current/"
LOG_FILE="/var/log/backup/offsite-$(date +%Y-%m).log"
SSH_KEY="/root/.ssh/backup_key"

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

log "===== Inicio replicación offsite ====="

rsync -aHAX --delete \
    --numeric-ids \
    -e "ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new" \
    "$LOCAL_CURRENT" \
    "$REMOTE_USER@$REMOTE_HOST:/backup/remote/" >> "$LOG_FILE" 2>&1

log "===== Fin replicación offsite ====="

Permiso y prueba:

sudo chmod 700 /usr/local/sbin/backup-offsite.sh
sudo /usr/local/sbin/backup-offsite.sh
Ancho de banda en la replicación inicial

La primera ejecución copia todo y puede tardar horas en datasets grandes. Considera hacer la carga inicial fuera del horario pico o vía medio físico si el destino está en la misma sala. Las ejecuciones siguientes transfieren solo el delta — generalmente segundos a minutos.

Agendamiento con cron

Con los scripts validados, programa la ejecución automática diaria.

07

Edita el crontab de root:

sudo crontab -e

Agrega (ajusta el horario según la ventana de menor tráfico de tu servidor):

# Garantiza PATH consistente — cron usa PATH mínimo por defecto
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]

# Backup local todos los días a las 03:00
0 3 * * * /usr/local/sbin/backup-daily.sh

# Replicación offsite todos los días a las 04:30 (después del local)
30 4 * * * /usr/local/sbin/backup-offsite.sh

Guarda y sal. El cron lee el cambio automáticamente — sin necesidad de reiniciar nada.

Verificación

Después de 24-48 horas, confirma que todo está ejecutándose:

# Snapshots diarios acumulando
ls -lah /backup/local/daily/

# Log de la última ejecución
tail -n 30 /var/log/backup/backup-$(date +%Y-%m).log

# Log del offsite
tail -n 30 /var/log/backup/offsite-$(date +%Y-%m).log

# En el servidor de destino
ls -lah /backup/remote/

Debes ver directorios fechados (2026-05-28, 2026-05-29, etc.), con du -sh mostrando un tamaño aparente igual al dataset pero un espacio real consumido bastante menor gracias a los hardlinks. Confirma con:

du -sh /backup/local/daily/2026-05-29        # tamaño aparente
du -sh --total /backup/local/daily/          # tamaño real total

La diferencia entre los dos números es la ganancia de los hardlinks.

Resolución de problemas

El script funciona manualmente pero falla en cron

Casi siempre es el PATH. Cron se ejecuta con PATH=/usr/bin:/bin por defecto. Solución: o usar rutas absolutas en el script (/usr/bin/rsync, /usr/bin/ssh), o definir PATH= al inicio del crontab como se mostró antes. Verifica también si MAILTO está configurado para recibir stderr — sin eso, los fallos se vuelven silencio absoluto.

rsync retorna código 23 o 24

El código 23 indica “algunos archivos no fueron transferidos” (generalmente archivos abiertos o con permiso restringido); el 24 indica “algunos archivos desaparecieron durante la transferencia” (típico en /var/log o /tmp). Estos códigos no son necesariamente error fatal — ajusta el set -e del script para tolerarlos con rsync ... || [ $? -eq 23 ] || [ $? -eq 24 ] si son esperados en tu escenario.

Permission denied en el destino offsite

La directiva command= en authorized_keys es literal — cualquier cambio en los flags del rsync local la rompe. Solución: ejecuta backup-offsite.sh manual con -e "ssh -v" en el rsync, captura la línea exacta rsync --server ... que envía, y actualiza el command= en el destino con esa cadena.

El espacio en disco crece descontroladamente

Probablemente --link-dest no está tomando el snapshot anterior — algún cambio rompió el symlink current o los snapshots están en filesystems diferentes (los hardlinks no cruzan filesystem). Verifica con stat -c '%i' archivo en dos fechas: si el inode es el mismo, hardlink activo; si es diferente, está duplicando.

Próximos pasos

Esta rutina cubre el básico de producción. Para evolucionar:

  • Agrega promoción automática semanal/mensual: copia (no hardlink) del snapshot diario a weekly/ todos los lunes y a monthly/ el primer día del mes.
  • Integra alerta activa: si el último log tiene más de 25 horas, dispara una notificación (Discord, e-mail, Healthchecks.io). Backup silenciosamente detenido es el peor escenario.
  • Prueba restauración mensual automatizada: restaura un archivo aleatorio del backup más antiguo, compara checksum con producción, registra el resultado.
  • Evalúa cifrado en tránsito y en reposo para el destino offsite (ya en tránsito vía SSH; en reposo considera LUKS en el volumen del destino).
  • Si estás ejecutando esto en producción, una VPS Hostini (/vps) tiene volúmenes NVMe rápidos para acomodar el historial de snapshots y además libera ancho de banda generoso para la replicación offsite — el backup deja de ser factor limitante en la elección del plan.

Preguntas frecuentes

¿Cuál es la diferencia entre rsync con --link-dest y tar incremental?

rsync con --link-dest crea snapshots completos usando hardlinks para archivos no modificados — cada directorio de backup parece una copia íntegra pero ocupa solo el delta en el disco. tar incremental genera archivos diferentes en cada ejecución y exige restaurar la base más todos los incrementales en orden. Para restauración rápida y gestión simple, --link-dest suele ser preferible.

¿Por qué mi cron no está ejecutando el script de backup?

Causas comunes: PATH no definido en el crontab (cron usa PATH mínimo, por lo que comandos como rsync o ssh pueden no ser encontrados — usa rutas absolutas), falta de permiso de ejecución en el script, o redirección de stderr ausente ocultando el error real. Agrega MAILTO al inicio del crontab o redirige con >> /var/log/backup.log 2>&1 para capturar la salida.

¿Backup offsite por SSH requiere contraseña en cada ejecución?

No — y no debe. Genera un par de claves dedicado al backup con ssh-keygen -t ed25519 -f /root/.ssh/backup_key sin passphrase, agrega la clave pública al authorized_keys del destino restringiendo el comando permitido (command="rsync --server ...",no-pty) y usa ssh -i /root/.ssh/backup_key en el rsync. Contraseña interactiva en cron simplemente bloquea el job.

¿Cuánto tiempo de retención es razonable para backup diario?

Estándar saludable: 7 días de diarios, 4 semanas de semanales, 6 meses de mensuales — varía según el tamaño del dataset y el costo de almacenamiento. Con --link-dest el costo marginal de mantener 30 snapshots diarios es bajo (solo el delta de cambios), así que no temas retener más de lo que sugiere la intuición inicial.

¿Cómo verificar que un backup realmente funciona sin restaurar todo?

Restaurar es la única prueba real, así que automatízala: una vez al mes, restaura aleatoriamente un archivo del backup más antiguo (rsync inverso o cp directo desde el snapshot) a un directorio temporal y compara checksum con producción mediante sha256sum. Registra el resultado y alerta ante divergencias. Backup no probado es esperanza, no estrategia.

¿rsync o borg/restic para backup en servidor Linux?

rsync es simple, transparente e ideal para copias completas con hardlinks (--link-dest) o sincronizaciones entre máquinas. borg y restic ofrecen deduplicación por bloque, cifrado integrado y compresión — ventajas reales para datasets grandes (TB) o backup offsite por internet con ancho de banda limitado. Para servidores pequeños/medianos, rsync cubre el 90% de los casos sin dependencias extra.

Temas:
Próximos pasos Cloud Ryzen con NVMe y protección DDoS siempre activa.Pon en producción en un VPS Hostini →
¿Te resultó útil este tutorial?
Hablar por WhatsApp