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 romperset -u.- El argumento se valida antes de usarlo en una comparación aritmética.
df --outputgenera columnas limpias, sin necesidad de recortar conawk.- 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.
Importanteuna copia guardada en el mismo disco no te protege si el servidor se pierde. Cópiala después a otro sistema, por ejemplo con
rsyncsobre SSH o a un almacenamiento de objetos.
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
rsyncsobre 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.
