Backup incremental automático con rsync y systemd timer
El problema
Todo sysadmin ha pasado por esto: tienes backups que se ejecutan… o eso crees. Hasta que pasa algo y descubres que el cronjob dejó de funcionar hace tres meses y nadie se enteró.
La solución canónica es rsync + cron. Pero cron tiene limitaciones: no sabes si falló, los logs son opacos, y si el servidor estaba apagado a la hora programada, simplemente no se ejecuta.
Aquí entra systemd timer: más robusto, con logs integrados en journalctl, soporte para eventos perdidos (Persistent=true) y aleatorización (RandomizedDelaySec) para no saturar discos cuando tienes muchas máquinas.
El script
Guarda esto como /usr/local/bin/backup-incremental.sh:
#!/usr/bin/env bash
set -euo pipefail
# === CONFIGURACIÓN ===
SOURCE_DIR="${SOURCE_DIR:-/srv/data}"
BACKUP_ROOT="${BACKUP_ROOT:-/backups/data}"
RETENTION_DAYS="${RETENTION_DAYS:-30}"
# =====================
TIMESTAMP=$(date +%Y-%m-%d_%H%M%S)
LATEST_LINK="${BACKUP_ROOT}/latest"
DEST_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
mkdir -p "${BACKUP_ROOT}"
rsync -avh --delete \
--link-dest="${LATEST_LINK}" \
"${SOURCE_DIR}/" \
"${DEST_DIR}/"
# Actualizar el enlace simbólico 'latest'
rm -f "${LATEST_LINK}"
ln -s "${DEST_DIR}" "${LATEST_LINK}"
# Rotar backups antiguos
find "${BACKUP_ROOT}" -maxdepth 1 -type d -name '20*' -mtime "+${RETENTION_DAYS}" \
-exec rm -rf {} \; 2>/dev/null || true
echo "Backup completado: ${DEST_DIR}"
¿Qué hace cada parte?
| Línea | Explicación |
|---|---|
set -euo pipefail | El script muere ante cualquier error. Sin esto, rsync puede fallar y el script reportaría éxito. |
SOURCE_DIR / BACKUP_ROOT | Configurables por variable de entorno. Si no se pasan, usan valores por defecto. |
--link-dest | La magia de rsync: crea hard links a archivos que no cambiaron. Backup 30 ocupa casi lo mismo que backup 1 si los datos no cambiaron. |
rm -f "${LATEST_LINK}" | Borramos el enlace anterior antes de crear el nuevo. ln -sf no es atómico — mejor explícito. |
find ... -mtime | Borra backups más viejos que RETENTION_DAYS. Usa -mtime (fecha de modificación), no -ctime. |
El timer de systemd
Crea dos archivos. Primero el servicio (/etc/systemd/system/backup-incremental.service):
[Unit]
Description=Backup incremental con rsync
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-incremental.sh
Environment="SOURCE_DIR=/srv/produccion"
Environment="BACKUP_ROOT=/backups/produccion"
Environment="RETENTION_DAYS=30"
User=root
Nice=19
IOSchedulingClass=idle
Y luego el timer (/etc/systemd/system/backup-incremental.timer):
[Unit]
Description=Ejecuta backup incremental diario a las 03:00
Requires=backup-incremental.service
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1800
[Install]
WantedBy=timers.target
Puntos importantes
Type=oneshot: El servicio se considera “activo” mientras se ejecuta. systemd sabe exactamente cuándo terminó.Nice=19+IOSchedulingClass=idle: El backup no compite con procesos de producción.Persistent=true: Si la máquina estuvo apagada a las 03:00, el timer se dispara inmediatamente al boot. Esto es lo quecronNO puede hacer.RandomizedDelaySec=1800: Añade hasta 30 minutos aleatorios. Útil si tienes 50 servidores haciendo backup a la misma hora contra el mismo NAS.
Actívalo:
systemctl daemon-reload
systemctl enable --now backup-incremental.timer
Verificar que funciona
# ¿El timer está activo?
systemctl status backup-incremental.timer
# ¿Cuándo se disparó la última vez?
systemctl list-timers backup-incremental.timer
# ¿Falló algo?
journalctl -u backup-incremental.service --since "1 day ago"
Resumen
| Aspecto | cron | systemd timer |
|---|---|---|
| Logs | Hay que redirigir manualmente | journalctl automático |
| Máquina apagada | Se pierde la ejecución | Persistent=true la recupera |
| Dependencias | Ninguna | After=, Wants= |
| Aleatorización | Manual con sleep $RANDOM | RandomizedDelaySec nativo |
| Monitoreo | Script externo | systemctl status + métricas exportables |
Este es el tipo de contenido que rankea: responde una pregunta específica, da código funcional, y resuelve un dolor real de un sysadmin.
¿Quieres probar el script en tu máquina antes de publicarlo?