Copias de seguridad con Bash: tar con fecha, comprobación y rotación de copias

Cómo hacer copias de seguridad con un script de Bash: comprimir una carpeta con tar y la fecha en el nombre, dejar fuera lo que sobra, comprobar que la copia se puede restaurar, borrar las copias viejas sin quedarte nunca sin ninguna y avisar si algo falla, con el script completo al final.

En los ejemplos trabajamos con una carpeta documentos con este contenido (el archivo .tmp es de los que no merece la pena copiar):

terminal$ find documentos -type f | sort
documentos/borrador.tmp
documentos/clientes.csv
documentos/fotos/portada.txt
documentos/notas.txt

Una copia con tar

tar junta una carpeta entera en un solo archivo y, con la opción z, lo comprime con gzip. Las letras se leen así: c crear, z comprimir, f el nombre del archivo que sigue. Para ver lo que hay dentro sin extraerlo, t (listar):

terminal$ tar -czf copia.tar.gz documentos
$ tar -tzf copia.tar.gz | sort
documentos/
documentos/borrador.tmp
documentos/clientes.csv
documentos/fotos/
documentos/fotos/portada.txt
documentos/notas.txt

Fíjate en que la copia guarda la ruta documentos/… tal como la escribiste. Si das una ruta absoluta, tar quita la barra del principio y avisa, para que al extraer no machaque los archivos originales:

terminal$ tar -czf copia.tar.gz /home/ana/proyecto/documentos
tar: Removing leading `/' from member names
$ tar -tzf copia.tar.gz | head -n 2
home/ana/proyecto/documentos/
home/ana/proyecto/documentos/borrador.tmp

Para que la copia guarde solo documentos/… aunque estés en otra carpeta, usa -C, que hace que tar cambie de carpeta antes de empezar: tar -czf copia.tar.gz -C /home/ana/proyecto documentos.

Con la fecha en el nombre y sin lo que sobra

Con la fecha en formato año-mes-día en el nombre, cada día tendrás un archivo distinto y se ordenarán solos. --exclude deja fuera lo que no quieras copiar:

#!/usr/bin/env bash
set -euo pipefail

fecha=2026-10-30            # en un script real: fecha=$(date +%F)
mkdir -p copias
archivo="copias/documentos-$fecha.tar.gz"

tar --exclude='*.tmp' -czf "$archivo" documentos
echo "Copia creada: $archivo"
tar -tzf "$archivo" | sort
salidaCopia creada: copias/documentos-2026-10-30.tar.gz
documentos/
documentos/clientes.csv
documentos/fotos/
documentos/fotos/portada.txt
documentos/notas.txt

Comprobar que la copia sirve

Una copia que no se puede restaurar no es una copia. Hay tres comprobaciones baratas: que tar pueda leerla entera, guardar su suma SHA-256 para detectar si se corrompe más adelante, y, de vez en cuando, restaurarla en una carpeta aparte y compararla con el original:

#!/usr/bin/env bash
set -euo pipefail

archivo=copias/documentos-2026-10-30.tar.gz
mkdir -p copias
tar --exclude='*.tmp' -czf "$archivo" documentos

# 1. tar puede leerla de principio a fin
if tar -tzf "$archivo" > /dev/null; then
    echo "1. La copia se lee entera"
fi

# 2. Suma de comprobación, para verificarla más adelante
sha256sum "$archivo" > "$archivo.sha256"
printf '2. '
sha256sum -c "$archivo.sha256"

# 3. Restaurar en una carpeta temporal y comparar con el original
prueba=$(mktemp -d)
tar -xzf "$archivo" -C "$prueba"
if diff -r --exclude='*.tmp' documentos "$prueba/documentos"; then
    echo "3. La restauración es idéntica al original"
fi
rm -r -- "$prueba"
salida1. La copia se lee entera
2. copias/documentos-2026-10-30.tar.gz: OK
3. La restauración es idéntica al original

Borrar las copias viejas

Si copias cada día, en un año tendrás 365 archivos. Lo normal es guardar solo las últimas N. Como el nombre lleva la fecha en formato año-mes-día, basta con ordenarlas por nombre y borrar todas menos las últimas. En la carpeta copias hay ya cinco copias de días anteriores:

#!/usr/bin/env bash
set -euo pipefail
guardar=3

mapfile -t copias < <(find copias -maxdepth 1 -name 'documentos-*.tar.gz' | sort)
echo "Hay ${#copias[@]} copias; se guardan las $guardar más recientes"

sobran=$(( ${#copias[@]} - guardar ))
for (( i = 0; i < sobran; i++ )); do
    rm -- "${copias[i]}"
    echo "Borrada: ${copias[i]}"
done

ls copias
salidaHay 5 copias; se guardan las 3 más recientes
Borrada: copias/documentos-2026-10-25.tar.gz
Borrada: copias/documentos-2026-10-26.tar.gz
documentos-2026-10-27.tar.gz
documentos-2026-10-28.tar.gz
documentos-2026-10-29.tar.gz

La otra forma habitual es borrar por antigüedad: find copias -name 'documentos-*.tar.gz' -mtime +30 -delete borra las que se modificaron hace más de 30 días. Es más sencilla, pero si las copias dejan de hacerse durante un mes, acabarás borrándolas todas; contar las que quedan, como arriba, nunca te deja sin ninguna.

Cuidado con los borrados en lote. Prueba siempre primero cambiando rm por echo rm, y no uses nunca ls | xargs rm ni rm $(ls …): un nombre con espacios borraría lo que no debe. Encontrarás más casos en la seguridad de los scripts de Bash.

El script completo

Este es el script entero, listo para programarlo con cron: recibe la carpeta de origen, la de destino y cuántas copias guardar; escribe primero en un archivo .parcial y solo le pone el nombre definitivo si tar terminó bien, de modo que una copia a medias nunca pasa por buena; comprueba la copia, borra las viejas y deja un registro. Si algo falla, termina con un código de salida distinto de 0, que es lo que cron o cualquier sistema de avisos necesitan para enterarse:

#!/usr/bin/env bash
# respaldo.sh ORIGEN DESTINO [COPIAS_A_GUARDAR]
set -euo pipefail

uso="Uso: ${0##*/} ORIGEN DESTINO [COPIAS_A_GUARDAR]"
origen=${1:?$uso}
destino=${2:?$uso}
guardar=${3:-7}
fecha=${FECHA:-$(date +%F)}     # FECHA permite fijar la fecha en las pruebas

registro() { printf '[respaldo %s] %s\n' "$fecha" "$*" >&2; }
trap 'registro "ERROR: falló la orden de la línea $LINENO"' ERR

if [[ ! -d $origen ]]; then
    registro "no existe la carpeta de origen: $origen"
    exit 2
fi
[[ $guardar =~ ^[1-9][0-9]*$ ]] || { registro "COPIAS_A_GUARDAR debe ser un número mayor que 0"; exit 2; }

mkdir -p -- "$destino"
nombre=$(basename -- "$origen")
archivo="$destino/$nombre-$fecha.tar.gz"

# 1. Copia en un archivo .parcial, que solo se renombra si todo fue bien
tar --exclude='*.tmp' -czf "$archivo.parcial" -C "$(dirname -- "$origen")" "$nombre"
tar -tzf "$archivo.parcial" > /dev/null
mv -- "$archivo.parcial" "$archivo"
sha256sum -- "$archivo" > "$archivo.sha256"
registro "copia creada: $archivo ($(tar -tzf "$archivo" | wc -l) entradas)"

# 2. Rotación: se guardan las $guardar más recientes
mapfile -t copias < <(find "$destino" -maxdepth 1 -name "$nombre-*.tar.gz" | sort)
for (( i = 0; i < ${#copias[@]} - guardar; i++ )); do
    rm -f -- "${copias[i]}" "${copias[i]}.sha256"
    registro "borrada la copia vieja ${copias[i]}"
done
registro "terminado: quedan $(find "$destino" -maxdepth 1 -name "$nombre-*.tar.gz" | wc -l) copias"
salida[respaldo 2026-10-30] copia creada: copias/documentos-2026-10-30.tar.gz (5 entradas)
[respaldo 2026-10-30] borrada la copia vieja copias/documentos-2026-10-25.tar.gz
[respaldo 2026-10-30] borrada la copia vieja copias/documentos-2026-10-26.tar.gz
[respaldo 2026-10-30] borrada la copia vieja copias/documentos-2026-10-27.tar.gz
[respaldo 2026-10-30] terminado: quedan 3 copias

Y si la carpeta de origen no existe, acaba con código 2 y lo dice en el registro:

./respaldo.sh documentoss copias[respaldo 2026-10-30] no existe la carpeta de origen: documentoss
código de salida: 2

Para que se haga sola cada noche a las 2:30, una línea en el crontab basta (lo explicamos en programar tareas con cron):

30 2 * * * /home/ana/bin/respaldo.sh /home/ana/documentos /mnt/copias 14 >> /home/ana/respaldo.log 2>&1

Una copia en el mismo disco no es suficiente

Si el disco se rompe, te roban el portátil o un ransomware cifra todo, la copia que está en el mismo equipo desaparece con lo demás. La regla clásica es la del 3-2-1: tres copias de los datos, en dos soportes distintos y una de ellas fuera de casa o de la oficina. Con este mismo script, el destino puede ser un disco externo o una carpeta de red montada, y para enviar las copias a otro servidor la herramienta habitual es rsync sobre SSH.

Todos los scripts de esta página se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red, y pasaron ShellCheck 0.11 sin avisos antes de publicarla; las salidas están copiadas de esa ejecución.