Un sistema de monitorización completo como Prometheus o Zabbix es la opción correcta cuando gestionas muchos servidores, pero para uno o dos servidores a veces basta con un script bien escrito que compruebe lo esencial y te avise cuando algo falla. En este tutorial escribirás en Ubuntu 24.04 un script Bash que vigila el espacio en disco, la memoria disponible, la carga, el estado de servicios de systemd y la respuesta de URLs, envía un aviso a Slack o Discord solo cuando el estado cambia, y se ejecuta cada cinco minutos con un temporizador de systemd.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Una URL de webhook entrante de Slack o de Discord (opcional: sin ella, los avisos solo quedan en el diario de systemd).
- Conocimientos básicos de Bash.
Paso 1: Instalar las dependencias
El script usa curl para comprobar URLs y enviar avisos, y jq para construir el JSON del webhook sin problemas de comillas:
sudo apt update
sudo apt install curl jq
Comprueba que están disponibles:
jq --version
jq-1.7.1
Paso 2: Escribir el script
El script sigue unas reglas que evitan los fallos típicos de los scripts de monitorización:
set -euo pipefailpara que un error inesperado detenga el script en lugar de producir un falso "todo bien".- Todos los umbrales se leen de variables de entorno con un valor por defecto, así no hay que editar el script para ajustarlos.
- Cada problema tiene una clave fija (por ejemplo
disk:/) y un mensaje con el detalle. Solo se avisa cuando cambia el conjunto de claves, no cada vez que el porcentaje sube un punto, así que no recibirás el mismo aviso cada cinco minutos. - No hay bucles infinitos: el script hace una pasada y termina. De repetirlo se encarga systemd.
Crea el fichero:
sudo nano /usr/local/bin/server-check
#!/usr/bin/env bash
# server-check: comprueba disco, memoria, carga, servicios y URLs
# y avisa por webhook cuando cambia el estado.
set -euo pipefail
DISK_MAX="${DISK_MAX:-85}" # % de uso máximo por sistema de ficheros
MEM_MIN_AVAILABLE="${MEM_MIN_AVAILABLE:-10}" # % mínimo de memoria disponible
LOAD_MAX_PER_CPU="${LOAD_MAX_PER_CPU:-2}" # carga máxima (1 min) por vCPU, número entero
SERVICES="${SERVICES:-ssh}" # unidades de systemd separadas por espacios
CHECK_URLS="${CHECK_URLS:-}" # URLs separadas por espacios
WEBHOOK_URL="${WEBHOOK_URL:-}" # vacío = solo escribir en la salida
WEBHOOK_FIELD="${WEBHOOK_FIELD:-text}" # text para Slack, content para Discord
STATE_FILE="${STATE_FILE:-/var/lib/server-check/last_status}"
host="$(hostname)"
keys=()
messages=()
problem() {
keys+=("$1")
messages+=("$2")
}
check_disk() {
local use target
while read -r use target; do
use="${use%\%}"
if (( use >= DISK_MAX )); then
problem "disk:${target}" "Disco ${target} al ${use}% (umbral ${DISK_MAX}%)"
fi
done < <(df --output=pcent,target -x tmpfs -x devtmpfs -x squashfs -x overlay -x efivarfs | tail -n +2)
}
check_memory() {
local available
available="$(awk '/^MemTotal:/ {t=$2} /^MemAvailable:/ {a=$2} END {printf "%d", a * 100 / t}' /proc/meminfo)"
if (( available < MEM_MIN_AVAILABLE )); then
problem "memory" "Memoria disponible ${available}% (mínimo ${MEM_MIN_AVAILABLE}%)"
fi
}
check_load() {
local load1 rest cpus limit
read -r load1 rest < /proc/loadavg
cpus="$(nproc)"
limit=$(( cpus * LOAD_MAX_PER_CPU ))
if awk -v l="$load1" -v m="$limit" 'BEGIN { exit !(l > m) }'; then
problem "load" "Carga ${load1} con ${cpus} vCPU (umbral ${limit})"
fi
}
check_services() {
local -a services
local svc
read -ra services <<< "$SERVICES"
for svc in "${services[@]}"; do
if ! systemctl is-active --quiet "$svc"; then
problem "service:${svc}" "El servicio ${svc} no está activo"
fi
done
}
check_urls() {
local -a urls
local url
read -ra urls <<< "$CHECK_URLS"
for url in "${urls[@]}"; do
if ! curl -fsS -o /dev/null --max-time 10 "$url" 2> /dev/null; then
problem "url:${url}" "${url} no responde correctamente"
fi
done
}
notify() {
local text="$1"
printf '%s\n' "$text"
if [[ -n "$WEBHOOK_URL" ]]; then
if ! jq -n --arg k "$WEBHOOK_FIELD" --arg v "$text" '{($k): $v}' |
curl -fsS --max-time 10 -H 'Content-Type: application/json' -d @- "$WEBHOOK_URL" > /dev/null; then
echo "No se pudo enviar el aviso al webhook" >&2
fi
fi
}
check_disk
check_memory
check_load
check_services
check_urls
if (( ${#keys[@]} > 0 )); then
status="$(printf '%s\n' "${keys[@]}" | sort)"
else
status="OK"
fi
previous="OK"
if [[ -f "$STATE_FILE" ]]; then
previous="$(< "$STATE_FILE")"
fi
if [[ "$status" == "$previous" ]]; then
echo "Sin cambios: ${status//$'\n'/, }"
exit 0
fi
if [[ "$status" == "OK" ]]; then
notify "[${host}] RESUELTO: todas las comprobaciones vuelven a estar bien"
else
notify "[${host}] ALERTA:"$'\n'"$(printf -- '- %s\n' "${messages[@]}")"
fi
mkdir -p "$(dirname "$STATE_FILE")"
printf '%s\n' "$status" > "$STATE_FILE"
Dale permisos de ejecución y revisa que no tenga errores de sintaxis:
sudo chmod 0755 /usr/local/bin/server-check
bash -n /usr/local/bin/server-check && echo "Sintaxis correcta"
Sintaxis correcta
Si quieres un análisis más estricto, instala shellcheck (sudo apt install shellcheck) y ejecuta shellcheck /usr/local/bin/server-check.
Algunas decisiones del script que conviene entender:
- Disco:
df --output=pcent,targetdevuelve solo el porcentaje y el punto de montaje, sin tener que recortar columnas conawk. Se excluyen los sistemas de ficheros virtuales (tmpfs,squashfsde snaps,overlayde contenedores), que darían falsos positivos. - Memoria: se usa
MemAvailablede/proc/meminfo, que es la memoria que el kernel puede dar a nuevos procesos incluyendo la caché liberable. Usar la memoria "libre" daría alarmas constantes, porque Linux aprovecha toda la RAM libre como caché. - Carga: Bash no sabe comparar decimales, así que la comparación se delega en
awken lugar de usarbc. - Servicios:
systemctl is-active --quietdevuelve un código de salida distinto de cero si la unidad no está en marcha, sin tener que analizar texto.
Paso 3: Probar el script manualmente
Ejecútalo tal cual. Con los valores por defecto y un servidor sano no debería avisar de nada:
sudo /usr/local/bin/server-check
Sin cambios: OK
Ahora fuerza una alerta bajando el umbral de disco al 1 % y añadiendo un servicio que no existe. Usa un fichero de estado temporal para no alterar el real:
sudo DISK_MAX=1 SERVICES="ssh servicio-inexistente" STATE_FILE=/tmp/server-check-test /usr/local/bin/server-check
[web01] ALERTA:
- Disco / al 23% (umbral 1%)
- Disco /boot al 11% (umbral 1%)
- El servicio servicio-inexistente no está activo
Si lo ejecutas otra vez con los mismos parámetros, no vuelve a avisar, porque el estado no ha cambiado:
Sin cambios: disk:/, disk:/boot, service:servicio-inexistente
Y si lo ejecutas con los valores normales sobre el mismo fichero de estado, recibes el aviso de resolución:
sudo STATE_FILE=/tmp/server-check-test /usr/local/bin/server-check
[web01] RESUELTO: todas las comprobaciones vuelven a estar bien
Borra el fichero de prueba:
sudo rm /tmp/server-check-test
Paso 4: Configurar los avisos por webhook
Guarda la configuración en un fichero aparte. Así puedes cambiar umbrales sin tocar el script, y la URL del webhook, que funciona como una contraseña, queda protegida:
sudo nano /etc/default/server-check
DISK_MAX=85
MEM_MIN_AVAILABLE=10
LOAD_MAX_PER_CPU=2
SERVICES="ssh nginx"
CHECK_URLS="https://your_domain/"
WEBHOOK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ"
WEBHOOK_FIELD=text
Sustituye your_domain por tu dominio y ajusta SERVICES a lo que ejecutes en el servidor. Para Discord, pega la URL del webhook de tu canal y cambia WEBHOOK_FIELD=content, que es el campo que espera Discord para el texto del mensaje.
Restringe los permisos del fichero:
sudo chmod 0600 /etc/default/server-check
Comprueba que el aviso llega a tu canal con una prueba forzada que lee la configuración:
sudo bash -c 'set -a; . /etc/default/server-check; DISK_MAX=1 STATE_FILE=/tmp/server-check-test /usr/local/bin/server-check; rm -f /tmp/server-check-test'
set -a exporta todas las variables que se cargan del fichero para que el script las vea. Debería aparecer el mensaje de alerta en Slack o Discord.
Paso 5: Ejecutar el script cada 5 minutos con systemd
Un temporizador de systemd es preferible a cron para esto: la salida queda en el diario, puedes ver cuándo fue la última ejecución y si falló, y systemd crea el directorio de estado por ti.
Crea el servicio:
sudo nano /etc/systemd/system/server-check.service
[Unit]
Description=Comprobaciones básicas del servidor
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/default/server-check
ExecStart=/usr/local/bin/server-check
StateDirectory=server-check
StateDirectory=server-check crea /var/lib/server-check, donde el script guarda el último estado. Type=oneshot indica que el servicio hace su trabajo y termina.
Crea el temporizador:
sudo nano /etc/systemd/system/server-check.timer
[Unit]
Description=Ejecuta server-check cada 5 minutos
[Timer]
OnCalendar=*:0/5
AccuracySec=30s
[Install]
WantedBy=timers.target
Recarga systemd y activa el temporizador:
sudo systemctl daemon-reload
sudo systemctl enable --now server-check.timer
Comprueba que está programado:
systemctl list-timers server-check.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-09-25 11:05:00 UTC 3min 12s left - - server-check.timer server-check.service
Lanza una ejecución inmediata y revisa su salida en el diario:
sudo systemctl start server-check.service
journalctl -u server-check.service -n 5 --no-pager
Sep 25 11:01:52 web01 systemd[1]: Starting server-check.service - Comprobaciones básicas del servidor...
Sep 25 11:01:52 web01 server-check[4102]: Sin cambios: OK
Sep 25 11:01:52 web01 systemd[1]: server-check.service: Deactivated successfully.
Sep 25 11:01:52 web01 systemd[1]: Finished server-check.service - Comprobaciones básicas del servidor.
Si el script terminara con un error (por ejemplo, por un fallo inesperado de df), la unidad aparecería en systemctl --failed, así que también te enterarías de que la propia monitorización ha dejado de funcionar.
Paso 6: Añadir una comprobación nueva
La estructura hace que añadir comprobaciones sea sencillo: una función que llama a problem con una clave y un mensaje. Como ejemplo, esta función avisa cuando el certificado TLS de un dominio caduca en menos de 14 días. openssl x509 -checkend devuelve un código de error si el certificado caduca antes de los segundos indicados.
Edita el script:
sudo nano /usr/local/bin/server-check
Añade la función junto a las demás comprobaciones:
check_certs() {
local -a domains
local domain
read -ra domains <<< "${CERT_DOMAINS:-}"
for domain in "${domains[@]}"; do
if ! openssl s_client -connect "${domain}:443" -servername "$domain" < /dev/null 2> /dev/null |
openssl x509 -noout -checkend $(( 14 * 86400 )) > /dev/null 2>&1; then
problem "cert:${domain}" "El certificado de ${domain} caduca en menos de 14 días o no se puede leer"
fi
done
}
Y llámala después de check_urls:
check_certs
Añade la variable a /etc/default/server-check:
CERT_DOMAINS="your_domain"
Comprueba la sintaxis y ejecuta el servicio para verificar que sigue funcionando:
bash -n /usr/local/bin/server-check && sudo systemctl start server-check.service
journalctl -u server-check.service -n 3 --no-pager
Solución de problemas
El script termina sin mensaje y la unidad queda en estado failed. Algún comando ha fallado y set -e ha detenido el script. Ejecútalo con traza para ver dónde: sudo bash -x /usr/local/bin/server-check.
No llegan los avisos al webhook y el diario muestra No se pudo enviar el aviso al webhook. Prueba la URL a mano con curl -fsS -H 'Content-Type: application/json' -d '{"text":"prueba"}' "$WEBHOOK_URL" (usa content en Discord). Un error 400 suele indicar un WEBHOOK_FIELD incorrecto; un 404, una URL revocada.
La alerta de carga aparece y desaparece continuamente. La carga de 1 minuto es muy variable. Sube LOAD_MAX_PER_CPU o cambia en check_load la lectura para usar la media de 5 minutos (read -r _ load1 rest < /proc/loadavg).
Tras cambiar /etc/default/server-check no se aplican los nuevos valores. No hace falta recargar nada: el fichero se lee en cada ejecución. Comprueba que no haya espacios alrededor del =, que el formato de EnvironmentFile no admite.
Conclusión
Tienes un script de monitorización que comprueba disco, memoria, carga, servicios, URLs y certificados, avisa por Slack o Discord solo cuando algo cambia y se ejecuta cada cinco minutos con systemd, con su salida en el diario. Es una buena solución para pocos servidores; cuando necesites histórico, gráficas o vigilar muchas máquinas, da el paso a Prometheus con node_exporter y Alertmanager, o a Netdata. Mientras tanto, complementa este script revisando los logs de /var/log y configurando logrotate para que los logs no llenen el disco.
