--- title: "Automatizar copias de seguridad con Bash y cron en Ubuntu" description: "Escribe un script de copias en Bash con rotación, verificación y copia remota por rsync, prográmalo con cron en Ubuntu 24.04 y recibe avisos si falla." category: "Recuperación de copias" language: es source: https://cubepath.com/es/docs/recuperación-de-copias/automatizacion-de-respaldos-con-scripts-y-cron updated: 2026-09-25 --- # Automatizar copias de seguridad con Bash y cron en Ubuntu 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 `web01` para 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`: ```bash 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: ```bash 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: ```bash sudo install -d -m 700 -o backup-web01 -g backup-web01 /home/backup-web01/.ssh sudo nano /home/backup-web01/.ssh/authorized_keys ``` ```text from="your_server_ip",restrict ssh-ed25519 AAAAC3Nza... backup-web01 ``` ```bash 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: ```bash sudo ssh -i /root/.ssh/backup_ed25519 backup-web01@backup_server_ip 'ls -ld /srv/backups/web01' ``` ```output 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 pipefail` detiene 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 `.part` y 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: ```bash sudo nano /usr/local/sbin/backup.sh ``` ```bash #!/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 son `etc/...`, `var/www/...` y no aparece el aviso de `tar` sobre la `/` inicial. - `gzip --test` comprueba que el archivo comprimido es íntegro antes de darlo por bueno. - `find ... -mtime +14 -delete` borra las copias con más de 14 días. `-maxdepth 1` y `-name` limitan el borrado a los archivos del script. - `rsync` envía al servidor remoto las copias nuevas. No usa `--delete`, de modo que un borrado accidental en `web01` no 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: ```bash sudo chmod 700 /usr/local/sbin/backup.sh ``` Comprueba que no tiene errores de sintaxis: ```bash sudo bash -n /usr/local/sbin/backup.sh ``` Si no muestra nada, la sintaxis es correcta. > Consejo: si el servidor tiene bases de datos, no basta con copiar sus archivos de datos. Añade al principio del script un volcado con `mysqldump` o `pg_dump` a un directorio incluido en `SOURCES`. ## Paso 3: Ejecutar el script a mano Antes de programarlo, ejecútalo una vez y comprueba el resultado: ```bash sudo /usr/local/sbin/backup.sh ``` ```output 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: ```bash sudo tar --list --file /var/backups/local/web01_2026-09-25_1031.tar.gz | head ``` ```output etc/ etc/hostname etc/hosts ... ``` Y que ha llegado al servidor remoto: ```bash 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: ```bash 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: ```bash sudo nano /etc/cron.d/backup ``` ```text 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.lock` impide 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>&1` guarda 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: ```bash sudo journalctl -u cron --since "10 minutes ago" --no-pager ``` ```output 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`: ```bash 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: ```bash sudo nano /etc/logrotate.d/backup ``` ```text /var/log/backup.log { weekly rotate 8 compress missingok notifempty } ``` Comprueba la configuración en modo de prueba: ```bash 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`: ```bash 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: ```bash sudo nano /etc/cron.d/backup-rotation ``` ```text # 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: ```bash 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.