Bash sigue siendo la forma más rápida de automatizar tareas de mantenimiento en un servidor Linux: no requiere instalar nada y tiene acceso directo a todas las herramientas del sistema. El problema es que un script mal escrito falla en silencio, borra lo que no debe o deja datos a medias. En este tutorial escribirás en Ubuntu 24.04 una plantilla segura, dos scripts útiles (una alerta de espacio en disco y una copia de seguridad con rotación) y los programarás con temporizadores de systemd.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Soltura básica con la terminal y un editor como nano.

Paso 1: Instalar ShellCheck y preparar el directorio

ShellCheck es un analizador estático que detecta los errores más comunes de Bash: variables sin comillas, comparaciones incorrectas o comandos que no hacen lo que parece. Instálalo desde los repositorios de Ubuntu:

sudo apt update
sudo apt install shellcheck

Comprueba la versión:

shellcheck --version
ShellCheck - shell script analysis tool
version: 0.9.0

Los scripts de administración que ejecuta root se guardan en /usr/local/sbin, que ya está en el PATH de root y no lo tocan los paquetes del sistema. No necesitas crear nada: el directorio existe por defecto.

Paso 2: Crear una plantilla con modo estricto

Por defecto Bash continúa aunque un comando falle y trata las variables no definidas como cadenas vacías. Un rm -rf "$DIR/" con DIR sin definir es el ejemplo clásico de desastre. Estas tres opciones cambian ese comportamiento:

  • set -e: el script termina en cuanto un comando devuelve un código de error.
  • set -u: usar una variable no definida es un error.
  • set -o pipefail: una tubería falla si falla cualquiera de sus comandos, no solo el último.

Esta es la plantilla que usarán los scripts de la guía. Registra los mensajes en el journal del sistema con logger, de modo que puedes consultarlos después con journalctl:

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

SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME

log() {
    # Escribe en stderr y en el journal con la etiqueta del script
    echo "$*" >&2
    logger -t "$SCRIPT_NAME" -- "$*"
}

on_error() {
    log "ERROR: fallo en la línea $1"
}
trap 'on_error $LINENO' ERR

main() {
    log "Inicio"
    # Tu lógica aquí
    log "Fin"
}

main "$@"

La función main al final y "$@" entre comillas hacen que los argumentos lleguen intactos, aunque contengan espacios. El trap ... ERR avisa de en qué línea falló el script antes de que set -e lo termine.

Paso 3: Escribir una alerta de espacio en disco

Un disco lleno detiene bases de datos, colas de correo y logs. Este script revisa cada sistema de archivos real y escribe un aviso en el journal cuando el uso supera un umbral. Crea el archivo:

sudo nano /usr/local/sbin/disk-alert

Pega el contenido:

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

readonly SCRIPT_NAME="disk-alert"
readonly THRESHOLD="${1:-85}"

if ! [[ "$THRESHOLD" =~ ^[0-9]+$ ]] || (( THRESHOLD < 1 || THRESHOLD > 100 )); then
    echo "Uso: $0 [umbral_porcentaje]" >&2
    exit 2
fi

status=0

# Ignora sistemas de archivos virtuales y los montajes de snap
while read -r pcent target; do
    usage="${pcent%\%}"
    if (( usage >= THRESHOLD )); then
        logger -t "$SCRIPT_NAME" -p user.warning -- "$target al ${usage}% (umbral ${THRESHOLD}%)"
        echo "AVISO: $target al ${usage}%" >&2
        status=1
    fi
done < <(df --output=pcent,target -x tmpfs -x devtmpfs -x squashfs -x overlay -x efivarfs | tail -n +2)

exit "$status"

Algunos detalles importantes:

  • ${1:-85} usa 85 como umbral si no se pasa ningún argumento, sin romper set -u.
  • El argumento se valida antes de usarlo en una comparación aritmética.
  • df --output genera columnas limpias, sin necesidad de recortar con awk.
  • El script sale con código 1 cuando hay algún aviso, así systemd marca la ejecución como fallida y es fácil detectarla.

Hazlo ejecutable y pásale ShellCheck:

sudo chmod 755 /usr/local/sbin/disk-alert
shellcheck /usr/local/sbin/disk-alert

Si ShellCheck no muestra nada, no ha encontrado problemas. Para probar el aviso, ejecútalo con un umbral del 1 %, que siempre se superará:

sudo disk-alert 1; echo "código de salida: $?"
AVISO: / al 23%
AVISO: /boot al 12%
código de salida: 1

Comprueba que el mensaje ha llegado al journal:

journalctl -t disk-alert -n 5 --no-pager
sep 25 10:14:02 servidor disk-alert[4121]: / al 23% (umbral 1%)
sep 25 10:14:02 servidor disk-alert[4122]: /boot al 12% (umbral 1%)

Paso 4: Escribir una copia de seguridad con rotación

El siguiente script empaqueta los directorios que indiques en un archivo .tar.gz con fecha y borra las copias con más de 14 días. Crea el directorio de destino con permisos solo para root, ya que las copias de /etc contienen datos sensibles:

sudo install -d -m 700 /var/backups/local

Crea el script:

sudo nano /usr/local/sbin/local-backup
#!/usr/bin/env bash
set -euo pipefail

readonly SCRIPT_NAME="local-backup"
readonly DEST="/var/backups/local"
readonly KEEP_DAYS=14
readonly SOURCES=(/etc /var/www)

log() {
    echo "$*" >&2
    logger -t "$SCRIPT_NAME" -- "$*"
}

trap 'log "ERROR: la copia ha fallado en la línea $LINENO"' ERR

main() {
    local stamp archive tmp
    stamp="$(date +%Y%m%d-%H%M%S)"
    archive="$DEST/backup-$stamp.tar.gz"
    tmp="$archive.part"

    # Incluye solo los orígenes que existen
    local existing=()
    for src in "${SOURCES[@]}"; do
        if [[ -e "$src" ]]; then
            existing+=("$src")
        fi
    done

    log "Creando $archive"
    tar --create --gzip --file "$tmp" --directory / "${existing[@]#/}"
    mv "$tmp" "$archive"

    log "Borrando copias con más de $KEEP_DAYS días"
    find "$DEST" -maxdepth 1 -name 'backup-*.tar.gz' -mtime +"$KEEP_DAYS" -print -delete

    log "Copia completada: $(du -h "$archive" | cut -f1)"
}

main "$@"

El archivo se escribe primero con la extensión .part y solo se renombra cuando tar termina bien. Así una copia interrumpida nunca parece válida, y la rotación con find solo toca archivos con el patrón backup-*.tar.gz dentro de DEST. Ajusta SOURCES a los directorios que quieras guardar.

Dale permisos, revísalo y ejecútalo:

sudo chmod 755 /usr/local/sbin/local-backup
shellcheck /usr/local/sbin/local-backup
sudo local-backup
Creando /var/backups/local/backup-20260925-101830.tar.gz
Borrando copias con más de 14 días
Copia completada: 1,2M

Comprueba el contenido del archivo:

sudo tar --list --file /var/backups/local/backup-*.tar.gz | head -n 5
etc/
etc/hostname
etc/fstab
etc/hosts
etc/passwd

Las rutas se guardan sin la barra inicial (--directory / y ${existing[@]#/}), así que al restaurar con tar --extract los archivos se descomprimen en el directorio actual y no sobrescriben el sistema por accidente.

Paso 5: Programar los scripts con temporizadores de systemd

Los temporizadores de systemd tienen ventajas sobre cron para tareas de administración: registran la salida en el journal automáticamente, evitan ejecuciones solapadas y, con Persistent=true, recuperan una ejecución perdida si el servidor estaba apagado.

Crea la unidad de servicio de la copia de seguridad:

sudo nano /etc/systemd/system/local-backup.service
[Unit]
Description=Copia de seguridad local

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/local-backup
Nice=10
IOSchedulingClass=idle

Crea el temporizador, que la lanzará cada día a las 03:00:

sudo nano /etc/systemd/system/local-backup.timer
[Unit]
Description=Copia de seguridad local diaria

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target

Repite el patrón para la alerta de disco, cada 15 minutos. Primero el servicio:

sudo nano /etc/systemd/system/disk-alert.service
[Unit]
Description=Alerta de espacio en disco

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/disk-alert 85

Y después el temporizador:

sudo nano /etc/systemd/system/disk-alert.timer
[Unit]
Description=Comprobar el espacio en disco cada 15 minutos

[Timer]
OnCalendar=*:0/15

[Install]
WantedBy=timers.target

Recarga systemd y activa los dos temporizadores:

sudo systemctl daemon-reload
sudo systemctl enable --now local-backup.timer disk-alert.timer

Comprueba cuándo se ejecutarán:

systemctl list-timers local-backup.timer disk-alert.timer
NEXT                        LEFT       LAST PASSED UNIT               ACTIVATES
Thu 2026-09-25 10:30:00 UTC 9min left  -    -      disk-alert.timer   disk-alert.service
Fri 2026-09-26 03:04:12 UTC 16h left   -    -      local-backup.timer local-backup.service

2 timers listed.

Puedes lanzar una ejecución manual en cualquier momento y revisar su resultado:

sudo systemctl start local-backup.service
journalctl -u local-backup.service -n 10 --no-pager

Solución de problemas

unbound variable: con set -u, leer $1 sin argumentos o una variable no definida es un error. Usa un valor por defecto, como ${1:-} o ${VAR:-valor}.

/usr/bin/env: 'bash\r': No such file or directory: el script se editó en Windows y tiene finales de línea CRLF. Conviértelos con sudo sed -i 's/\r$//' /usr/local/sbin/disk-alert.

Permission denied al ejecutar el script: falta el bit de ejecución. Revísalo con ls -l y aplica sudo chmod 755 al archivo.

El servicio aparece como failed: consulta el motivo con systemctl status disk-alert.service y journalctl -u disk-alert.service. En el caso de disk-alert, un estado failed significa que algún disco supera el umbral, que es justo lo que quieres detectar.

Conclusión

Has creado una plantilla de Bash con modo estricto y registro en el journal, una alerta de espacio en disco y una copia de seguridad con rotación segura, y las has programado con temporizadores de systemd. Pasar ShellCheck a cada script antes de ponerlo en producción evita la mayoría de los errores habituales.

Como siguientes pasos puedes:

  • Enviar las copias a otro servidor con rsync sobre SSH y una clave dedicada.
  • Recibir los avisos por correo o en un canal de chat añadiendo OnFailure= a la unidad del servicio.
  • Pasar a Ansible cuando tengas que aplicar los mismos scripts en varios servidores.