Una copia de seguridad solo sirve si se hace siempre, sin depender de que alguien se acuerde. En este tutorial escribirás en Ubuntu 24.04 un script de Bash que empaqueta los directorios importantes, verifica el archivo generado, borra las copias antiguas y envía una copia a otro servidor por rsync. Después lo programarás con cron, evitarás ejecuciones solapadas, guardarás un registro con rotación y configurarás un aviso cuando la copia no llegue a ejecutarse.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. En esta guía se llama
web01. - Un usuario no root con privilegios
sudo. - Un segundo servidor accesible por SSH para guardar la copia externa. En los ejemplos, su IP es
backup_server_ip. - Espacio libre en
web01para al menos dos copias completas de los datos.
Si necesitas cifrado o deduplicación, herramientas como Restic o BorgBackup encajan mejor. Este enfoque con tar y rsync es adecuado para servidores pequeños y te da archivos que se restauran con herramientas estándar.
Paso 1: Preparar el destino remoto
En el servidor de copias, crea un usuario dedicado y el directorio donde recibirá las copias de web01:
sudo adduser --disabled-password --comment '' backup-web01
sudo install -d -m 700 -o backup-web01 -g backup-web01 /srv/backups/web01
En web01, crea una clave SSH para root sin frase de paso, ya que cron la usará sin intervención:
sudo ssh-keygen -t ed25519 -f /root/.ssh/backup_ed25519 -N '' -C 'backup-web01'
sudo cat /root/.ssh/backup_ed25519.pub
En el servidor de copias, añade esa clave al usuario nuevo. La opción from solo acepta la clave si la conexión llega desde la IP de web01 (sustituye your_server_ip), y restrict desactiva el reenvío de puertos, agentes y terminal:
sudo install -d -m 700 -o backup-web01 -g backup-web01 /home/backup-web01/.ssh
sudo nano /home/backup-web01/.ssh/authorized_keys
from="your_server_ip",restrict ssh-ed25519 AAAAC3Nza... backup-web01
sudo chown backup-web01:backup-web01 /home/backup-web01/.ssh/authorized_keys
sudo chmod 600 /home/backup-web01/.ssh/authorized_keys
Vuelve a web01 y prueba la conexión. La primera vez acepta la huella del servidor para que quede guardada en el known_hosts de root:
sudo ssh -i /root/.ssh/backup_ed25519 backup-web01@backup_server_ip 'ls -ld /srv/backups/web01'
drwx------ 2 backup-web01 backup-web01 4096 Sep 25 10:12 /srv/backups/web01
Paso 2: Escribir el script de copia
El script sigue unas reglas que evitan los fallos típicos de los scripts de copia:
set -euo pipefaildetiene el script ante cualquier error, variable no definida o fallo dentro de una tubería, en lugar de seguir y dar una copia rota por buena.- El archivo se escribe primero con extensión
.party solo se renombra cuando está completo. Nunca queda en el directorio un archivo a medias con aspecto de válido. - Todas las variables van entre comillas y la configuración está al principio.
Crea el script:
sudo nano /usr/local/sbin/backup.sh
#!/usr/bin/env bash
# Copia de seguridad diaria: tar local, rotación y copia remota con rsync.
set -euo pipefail
# Configuración
BACKUP_DIR="/var/backups/local"
SOURCES=(etc var/www home root) # rutas relativas a /
EXCLUDES=(--exclude='home/*/.cache' --exclude='root/.cache')
RETENTION_DAYS=14
REMOTE="backup-web01@backup_server_ip:/srv/backups/web01/"
SSH_KEY="/root/.ssh/backup_ed25519"
log() {
printf '%s %s\n' "$(date '+%F %T')" "$*"
}
umask 077
mkdir -p "$BACKUP_DIR"
archive="${BACKUP_DIR}/$(hostname -s)_$(date +%F_%H%M).tar.gz"
trap 'rm -f "${archive}.part"' EXIT
log "Inicio de la copia: ${archive}"
# tar devuelve 1 si algún archivo cambió mientras se leía: es un aviso, no un error.
tar_exit=0
tar --create --gzip --file "${archive}.part" -C / "${EXCLUDES[@]}" "${SOURCES[@]}" || tar_exit=$?
if (( tar_exit > 1 )); then
log "ERROR: tar terminó con código ${tar_exit}"
exit "$tar_exit"
fi
if (( tar_exit == 1 )); then
log "AVISO: algunos archivos cambiaron durante la copia"
fi
gzip --test "${archive}.part"
mv "${archive}.part" "$archive"
log "Archivo verificado: $(du -h "$archive" | cut -f1)"
# Rotación local
find "$BACKUP_DIR" -maxdepth 1 -type f -name '*.tar.gz' -mtime +"$RETENTION_DAYS" -print -delete
# Copia externa
rsync -a --partial -e "ssh -i ${SSH_KEY}" "${BACKUP_DIR}/" "$REMOTE"
log "Copia remota completada"
Qué hace cada parte:
tar -C /cambia a la raíz antes de empaquetar, así las rutas del archivo sonetc/...,var/www/...y no aparece el aviso detarsobre la/inicial.gzip --testcomprueba que el archivo comprimido es íntegro antes de darlo por bueno.find ... -mtime +14 -deleteborra las copias con más de 14 días.-maxdepth 1y-namelimitan el borrado a los archivos del script.rsyncenvía al servidor remoto las copias nuevas. No usa--delete, de modo que un borrado accidental enweb01no se propaga a la copia externa. La rotación del servidor remoto se configura aparte, en el paso 7.
Sustituye backup_server_ip y ajusta SOURCES a tus directorios. Luego hazlo ejecutable solo para root:
sudo chmod 700 /usr/local/sbin/backup.sh
Comprueba que no tiene errores de sintaxis:
sudo bash -n /usr/local/sbin/backup.sh
Si no muestra nada, la sintaxis es correcta.
Consejosi el servidor tiene bases de datos, no basta con copiar sus archivos de datos. Añade al principio del script un volcado con
mysqldumpopg_dumpa un directorio incluido enSOURCES.
Paso 3: Ejecutar el script a mano
Antes de programarlo, ejecútalo una vez y comprueba el resultado:
sudo /usr/local/sbin/backup.sh
2026-09-25 10:31:02 Inicio de la copia: /var/backups/local/web01_2026-09-25_1031.tar.gz
2026-09-25 10:32:40 Archivo verificado: 612M
2026-09-25 10:33:15 Copia remota completada
Comprueba que el archivo contiene lo esperado listando su contenido:
sudo tar --list --file /var/backups/local/web01_2026-09-25_1031.tar.gz | head
etc/
etc/hostname
etc/hosts
...
Y que ha llegado al servidor remoto:
sudo ssh -i /root/.ssh/backup_ed25519 backup-web01@backup_server_ip 'ls -lh /srv/backups/web01'
cron ejecuta los comandos con un entorno mínimo (un PATH reducido, sin tus variables). Para detectar dependencias ocultas del entorno, ejecuta el script también con un entorno vacío:
sudo env -i PATH=/usr/bin:/bin /usr/local/sbin/backup.sh
Si funciona así, funcionará desde cron.
Paso 4: Programar el script con cron
Un trabajo de cron tiene cinco campos de tiempo seguidos del comando:
| Campo | Valores | Ejemplo |
|---|---|---|
| Minuto | 0-59 | 30 |
| Hora | 0-23 | 2 |
| Día del mes | 1-31 | * |
| Mes | 1-12 | * |
| Día de la semana | 0-7 (0 y 7 son domingo) | * |
Algunas combinaciones habituales:
30 2 * * *: cada día a las 02:30.0 3 * * 0: los domingos a las 03:00.0 */6 * * *: cada seis horas, en punto.0 1 1 * *: el día 1 de cada mes a la 01:00.
Para tareas del sistema es preferible un archivo en /etc/cron.d/ que el crontab de root: queda a la vista, se puede gestionar con herramientas de configuración y lleva un campo extra con el usuario que ejecuta el comando. Créalo:
sudo nano /etc/cron.d/backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
# m h dom mon dow usuario comando
30 2 * * * root flock -n /run/backup.lock /usr/local/sbin/backup.sh >> /var/log/backup.log 2>&1
Detalles importantes:
flock -n /run/backup.lockimpide que se ejecuten dos copias a la vez. Si la anterior sigue en marcha, la nueva termina sin hacer nada.>> /var/log/backup.log 2>&1guarda la salida normal y los errores en un registro.MAILTO=""desactiva el envío de la salida por correo. Si tienes un servidor de correo configurado, pon tu dirección en su lugar.- El nombre del archivo en
/etc/cron.d/no puede contener puntos, y el archivo debe terminar con un salto de línea. Si no, cron lo ignora sin avisar.
cron detecta los cambios en /etc/cron.d/ automáticamente. Para comprobar que la programación funciona sin esperar a la noche, cambia temporalmente la hora a dos minutos en el futuro, espera y revisa el registro de cron:
sudo journalctl -u cron --since "10 minutes ago" --no-pager
Sep 25 10:52:01 web01 CRON[48211]: (root) CMD (flock -n /run/backup.lock /usr/local/sbin/backup.sh >> /var/log/backup.log 2>&1)
Después revisa la salida del script y vuelve a dejar la hora en 30 2:
sudo tail -n 5 /var/log/backup.log
Paso 5: Rotar el registro
/var/log/backup.log crece cada día. Configura logrotate para rotarlo:
sudo nano /etc/logrotate.d/backup
/var/log/backup.log {
weekly
rotate 8
compress
missingok
notifempty
}
Comprueba la configuración en modo de prueba:
sudo logrotate --debug /etc/logrotate.d/backup
La salida muestra qué haría logrotate con el archivo sin modificar nada.
Paso 6: Recibir un aviso si la copia no se ejecuta
Un script que falla en silencio es el problema más común de las copias automatizadas. La forma más fiable de detectarlo es un monitor de tipo "dead man's switch": el script avisa a un servicio externo cada vez que termina bien, y el servicio te alerta si el aviso no llega a tiempo. Así detectas también los casos en que cron no llega a ejecutar nada (servidor apagado, archivo de cron roto).
Servicios como Healthchecks.io (que también se puede instalar en tu propio servidor) funcionan así: creas un check con un periodo de 1 día, obtienes una URL única y la llamas al final del script. Añade al final de /usr/local/sbin/backup.sh:
curl -fsS -m 10 --retry 3 -o /dev/null "https://hc-ping.com/your_check_uuid"
Como el script usa set -e, esta línea solo se alcanza si todo lo anterior ha ido bien. Si la copia falla o no se ejecuta, el servicio te enviará una alerta cuando venza el periodo.
Ejecuta el script a mano otra vez y confirma en el panel del servicio que el check ha recibido el aviso.
Paso 7: Rotar las copias en el servidor remoto
Como rsync no borra nada en el destino, el servidor de copias debe hacer su propia rotación. Puedes conservar allí más días que en local. En el servidor de copias, crea un trabajo de cron:
sudo nano /etc/cron.d/backup-rotation
# Conserva 60 días de copias de web01
15 6 * * * backup-web01 find /srv/backups/web01 -maxdepth 1 -type f -name '*.tar.gz' -mtime +60 -delete
Recuerda terminar el archivo con un salto de línea.
Paso 8: Restaurar desde una copia
Para recuperar archivos, extrae en un directorio temporal lo que necesites y cópialo después a su sitio. Por ejemplo, para recuperar la configuración de Nginx:
sudo mkdir -p /tmp/restore
sudo tar --extract --gzip --file /var/backups/local/web01_2026-09-25_1031.tar.gz -C /tmp/restore etc/nginx
sudo diff -r /tmp/restore/etc/nginx /etc/nginx
Si el contenido es el esperado, cópialo con sudo cp -a /tmp/restore/etc/nginx/. /etc/nginx/. Si web01 se ha perdido, descarga el archivo desde el servidor de copias con scp o rsync y extráelo en el servidor nuevo.
Solución de problemas
El trabajo no aparece en el registro de cron. El archivo de /etc/cron.d/ tiene un punto en el nombre, no termina en salto de línea, le falta el campo de usuario o tiene permisos de escritura para otros usuarios. Debe pertenecer a root con permisos 644.
Funciona a mano pero no desde cron. Casi siempre es el entorno: un comando que no está en el PATH de cron o una variable que solo existe en tu sesión. Repite la prueba con env -i del paso 3.
Los comandos con % fallan en cron. En una línea de cron, % significa salto de línea. Escápalo como \% o, mejor, mueve la lógica al script, como en esta guía.
Host key verification failed en el registro. root no tiene la huella del servidor remoto en known_hosts. Repite la conexión de prueba del paso 1 con sudo.
Conclusión
Tienes un script de copia que falla de forma explícita, verifica cada archivo, rota las copias locales y remotas y se ejecuta cada noche con cron sin solaparse, con un registro rotado y un aviso externo cuando algo no funciona. Como siguientes pasos, añade el volcado de tus bases de datos al script, haz una restauración de prueba completa en un servidor limpio y, si los datos crecen, valora pasar a una herramienta incremental como rsnapshot o Restic.
