--- title: "Monitorizar un servidor con un script Bash y systemd" description: "Escribe un script Bash que vigila disco, memoria, carga, servicios y URLs, avisa por webhook a Slack o Discord solo cuando algo cambia y se ejecuta con systemd." category: "Monitoring Logging" language: es source: https://cubepath.com/es/docs/monitoring-logging/monitoreo-con-scripts-bash-personalizados updated: 2026-09-25 --- # Monitorizar un servidor con un script Bash y systemd 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: ```bash sudo apt update sudo apt install curl jq ``` Comprueba que están disponibles: ```bash jq --version ``` ```output 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 pipefail` para 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: ```bash sudo nano /usr/local/bin/server-check ``` ```bash #!/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: ```bash sudo chmod 0755 /usr/local/bin/server-check bash -n /usr/local/bin/server-check && echo "Sintaxis correcta" ``` ```output 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,target` devuelve solo el porcentaje y el punto de montaje, sin tener que recortar columnas con `awk`. Se excluyen los sistemas de ficheros virtuales (`tmpfs`, `squashfs` de snaps, `overlay` de contenedores), que darían falsos positivos. - **Memoria**: se usa `MemAvailable` de `/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 `awk` en lugar de usar `bc`. - **Servicios**: `systemctl is-active --quiet` devuelve 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: ```bash sudo /usr/local/bin/server-check ``` ```output 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: ```bash sudo DISK_MAX=1 SERVICES="ssh servicio-inexistente" STATE_FILE=/tmp/server-check-test /usr/local/bin/server-check ``` ```output [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: ```output 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: ```bash sudo STATE_FILE=/tmp/server-check-test /usr/local/bin/server-check ``` ```output [web01] RESUELTO: todas las comprobaciones vuelven a estar bien ``` Borra el fichero de prueba: ```bash 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: ```bash sudo nano /etc/default/server-check ``` ```ini 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: ```bash sudo chmod 0600 /etc/default/server-check ``` Comprueba que el aviso llega a tu canal con una prueba forzada que lee la configuración: ```bash 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: ```bash sudo nano /etc/systemd/system/server-check.service ``` ```ini [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: ```bash sudo nano /etc/systemd/system/server-check.timer ``` ```ini [Unit] Description=Ejecuta server-check cada 5 minutos [Timer] OnCalendar=*:0/5 AccuracySec=30s [Install] WantedBy=timers.target ``` Recarga systemd y activa el temporizador: ```bash sudo systemctl daemon-reload sudo systemctl enable --now server-check.timer ``` Comprueba que está programado: ```bash systemctl list-timers server-check.timer ``` ```output 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: ```bash sudo systemctl start server-check.service journalctl -u server-check.service -n 5 --no-pager ``` ```output 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: ```bash sudo nano /usr/local/bin/server-check ``` Añade la función junto a las demás comprobaciones: ```bash 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`: ```bash check_certs ``` Añade la variable a `/etc/default/server-check`: ```ini CERT_DOMAINS="your_domain" ``` Comprueba la sintaxis y ejecuta el servicio para verificar que sigue funcionando: ```bash 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.