Vaya servidor…
Justo me voy de Semana Santa, una semana entera, Dejo los servidores bien puestos, con un reinicio fresco, y todo funciona perfecto. Ese mismo día por la noche me llega este correo:

Y todo deja de estar disponible. Empiezo a investigar y todo el acceso remoto lo he perdido. La VPN y los túneles de Cloudflared estaban ahí. Y no hay manera de que pueda acceder a casa para saber cómo están las cosas.
Mi padre tiene el nuevo NAS UGREEN DH2300, pero no puedo conseguir una shell remota. Lo que sí veo es que puedo instalar Docker, pero sigo sin poder conseguir una shell remota.

Con docker instalado, puedo descargar una imagen de ubuntu y iniciar el contenedor.

Puedo iniciar una shell para ver si puedo hacer ping al portátil. No responde, pero es verdad que, por defecto, Proxmox rechaza los paquetes ICMP. Así que lo comprobamos con ARP y vemos que sí obtenemos una respuesta. Es decir, Proxmox está funcionando, pero como todos los contenedores están apagados, no podemos acceder a él.

Tampoco podemos acceder por SSH, así que se ha quedado totalmente bloqueado. Se ha quedado en proceso de apagado y está conectado y encendido, pero con todos los servicios apagados.
El otro nodo sí que vemos que está activo y puedo acceder por SSH a él. En este punto, descargo una plantilla de Ubuntu 25.05 para instalar Cloudflared y ver si podemos acceder a la GUI de Proxmox. Y lo conseguimos.
Sin embargo, el portátil está totalmente inutilizable. Solo queda esperar a volver a casa para ver qué ha pasado: si se ha quemado el SSD y por eso se ha quedado bloqueado, o si ha ocurrido otra cosa.
Una semana después, llego a casa y, según me voy acercando, empieza a sonar un ruido raro. Algo que arranca lo intenta y se para. Al principio pienso que es el disco duro secundario, que se haya estropeado la cabeza lectora y por eso suena ese clic.
Fuerzo el apagado con el botón, dejo que se enfríe porque estaba muy, muy caliente y lo vuelvo a encender. Empiezo a pensar que es posible que sea el ventilador. Eso explicaría la temperatura excesiva del ordenador, así como el correo de alerta crítica de temperatura en el disco.
Y así era: una vez enciendo el ordenador, a pesar de que suene, Proxmox arranca. Obtengo la shell y veo que ambos almacenamientos están funcionando. Cuando abro la tapa, el ventilador empieza a girar, roza y se para. Los muelles del ventilador parece que se han descolgado y por eso no funciona.
Tiene todo el sentido que haya pasado esto. El ordenador lleva encendido 24/7, los 365 días, durante 2 años seguidos. Es normal que un ventilador de portátil deje de funcionar.
Toca pedir un recambio de ventiladores y, ya que estamos, cambiamos la pasta térmica.
Mientras tanto, mientras hago la migración de servicios importantes, vamos a poner una base refrigerada con ventiladores y vamos a montarnos un script para que lea los datos de los sensores con el comando sensors:
root@proxmox:~# sensors
pch_cannonlake-virtual-0
Adapter: Virtual device
temp1: +60.0°C
nvme-pci-0600
Adapter: PCI adapter
Composite: +48.9°C (low = -0.1°C, high = +80.8°C)
(crit = +81.8°C)
Sensor 1: +48.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +48.9°C (low = -273.1°C, high = +65261.8°C)
iwlwifi_1-virtual-0
Adapter: Virtual device
temp1: N/A
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +55.0°C (high = +100.0°C, crit = +100.0°C)
Core 0: +54.0°C (high = +100.0°C, crit = +100.0°C)
Core 1: +53.0°C (high = +100.0°C, crit = +100.0°C)
Core 2: +53.0°C (high = +100.0°C, crit = +100.0°C)
Core 3: +52.0°C (high = +100.0°C, crit = +100.0°C)
Core 4: +54.0°C (high = +100.0°C, crit = +100.0°C)
Core 5: +52.0°C (high = +100.0°C, crit = +100.0°C)
BAT0-acpi-0
Adapter: ACPI interface
in0: 12.72 V
Vamos a montar un script que lea los datos actuales de temperatura y, si supera un umbral, me notifique por un webhook de Discord.
#!/bin/bash
# ============================================================
# monitor_temps.sh — Monitorización de temperatura en Proxmox
# Envía alertas a Discord cuando se superan umbrales de aviso
# ============================================================
DISCORD_WEBHOOK="https://discord.com/api/webhooks/1490637687038808095/gwrQx_enYLyanPX5FHeEl91B3qc9LOfJDRlpSWsQmgsu2kzwZmbT0wLVxBfv6bJSJHVt"
SSH_HOST="proxmox"
INTERVAL="${INTERVAL:-60}" # segundos entre comprobaciones
# --- Umbrales de aviso (°C) ---
# Se avisa bastante antes de los límites críticos del hardware
THRESH_CPU=70 # CPU cores: crítico en 100°C
THRESH_NVME=60 # NVMe SSD: crítico en 81.8°C
THRESH_PCH=65 # PCH: sin límite declarado, conservador
# --- Cooldown entre alertas del mismo sensor (segundos) ---
COOLDOWN=300 # 5 minutos para no spamear
# Directorio temporal para guardar estado de cooldown
STATE_DIR="${TMPDIR:-/tmp}/proxmox_monitor"
mkdir -p "$STATE_DIR"
# ============================================================
usage() {
echo "Uso: DISCORD_WEBHOOK='https://discord.com/api/webhooks/...' $0 [--once]"
echo ""
echo " --once Comprueba una sola vez y sale (útil para cron)"
echo ""
echo "Variables de entorno:"
echo " DISCORD_WEBHOOK URL del webhook de Discord (obligatorio)"
echo " INTERVAL Segundos entre comprobaciones (default: 60)"
exit 1
}
# ============================================================
send_discord_alert() {
local sensor="$1"
local temp="$2"
local threshold="$3"
local level="$4" # WARNING o CRITICAL
local color
local emoji
if [[ "$level" == "CRITICAL" ]]; then
color=15158332 # rojo
emoji="🔴"
else
color=16776960 # amarillo
emoji="🟡"
fi
local hostname
hostname=$(ssh -o ConnectTimeout=10 "$SSH_HOST" hostname 2>/dev/null || echo "$SSH_HOST")
local payload
payload=$(cat <<EOF
{
"embeds": [{
"title": "${emoji} Alerta de temperatura — ${hostname}",
"color": ${color},
"fields": [
{"name": "Sensor", "value": "\`${sensor}\`", "inline": true},
{"name": "Temperatura", "value": "\`${temp}°C\`", "inline": true},
{"name": "Umbral", "value": "\`${threshold}°C\`", "inline": true},
{"name": "Nivel", "value": "${level}", "inline": true},
{"name": "Posible causa", "value": "Ventilador de GPU desconectado — revisar físicamente el portátil", "inline": false}
],
"footer": {"text": "monitor_temps.sh • $(date '+%Y-%m-%d %H:%M:%S')"}
}]
}
EOF
)
curl -s -o /dev/null -w "%{http_code}" \
-H "Content-Type: application/json" \
-d "$payload" \
"$DISCORD_WEBHOOK" > /dev/null
}
# ============================================================
check_cooldown() {
local sensor_key="$1"
local state_file="$STATE_DIR/${sensor_key//[^a-zA-Z0-9]/_}"
local now
now=$(date +%s)
if [[ -f "$state_file" ]]; then
local last
last=$(cat "$state_file")
if (( now - last < COOLDOWN )); then
return 1 # en cooldown, no alertar
fi
fi
echo "$now" > "$state_file"
return 0
}
# ============================================================
parse_and_check() {
local sensors_output="$1"
local alerts=()
# Extraer temperaturas con regex
# Formato: "Label: +XX.X°C"
while IFS= read -r line; do
# CPU cores y Package
if [[ "$line" =~ ^(Package\ id\ 0|Core\ [0-9]+):[[:space:]]+\+([0-9]+\.[0-9]+)°C ]]; then
local label="${BASH_REMATCH[1]}"
local temp="${BASH_REMATCH[2]}"
local temp_int="${temp%.*}"
if (( temp_int >= THRESH_CPU )); then
local level="WARNING"
(( temp_int >= 85 )) && level="CRITICAL"
if check_cooldown "cpu_${label// /_}"; then
alerts+=("${label}|${temp}|${THRESH_CPU}|${level}")
fi
fi
# NVMe Composite
elif [[ "$line" =~ ^Composite:[[:space:]]+\+([0-9]+\.[0-9]+)°C ]]; then
local temp="${BASH_REMATCH[1]}"
local temp_int="${temp%.*}"
if (( temp_int >= THRESH_NVME )); then
local level="WARNING"
(( temp_int >= 75 )) && level="CRITICAL"
if check_cooldown "nvme_composite"; then
alerts+=("NVMe Composite|${temp}|${THRESH_NVME}|${level}")
fi
fi
# PCH
elif [[ "$line" =~ ^temp1:.*\+([0-9]+\.[0-9]+)°C ]]; then
local temp="${BASH_REMATCH[1]}"
local temp_int="${temp%.*}"
# Solo el PCH (pch_cannonlake), no iwlwifi que puede ser N/A
if (( temp_int >= THRESH_PCH )); then
local level="WARNING"
(( temp_int >= 80 )) && level="CRITICAL"
if check_cooldown "pch_temp1"; then
alerts+=("PCH temp1|${temp}|${THRESH_PCH}|${level}")
fi
fi
fi
done <<< "$sensors_output"
# Enviar alertas
for alert in "${alerts[@]}"; do
IFS='|' read -r sensor temp threshold level <<< "$alert"
echo "[$(date '+%H:%M:%S')] ALERTA ${level}: ${sensor} = ${temp}°C (umbral: ${threshold}°C)"
if [[ -n "$DISCORD_WEBHOOK" ]]; then
send_discord_alert "$sensor" "$temp" "$threshold" "$level"
else
echo " (DISCORD_WEBHOOK no configurado, alerta no enviada)"
fi
done
if [[ ${#alerts[@]} -eq 0 ]]; then
echo "[$(date '+%H:%M:%S')] OK — todas las temperaturas dentro del rango"
fi
}
# ============================================================
run_check() {
local sensors_output
sensors_output=$(ssh -o ConnectTimeout=10 -o BatchMode=yes "$SSH_HOST" sensors 2>&1)
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "[$(date '+%H:%M:%S')] ERROR: no se pudo conectar a '${SSH_HOST}' (exit ${exit_code})"
if [[ -n "$DISCORD_WEBHOOK" ]] && check_cooldown "ssh_error"; then
local payload
payload=$(cat <<EOF
{
"embeds": [{
"title": "🔴 Error de conexión SSH — ${SSH_HOST}",
"color": 15158332,
"description": "No se puede conectar al servidor para leer temperaturas.\n\`\`\`${sensors_output}\`\`\`",
"footer": {"text": "monitor_temps.sh • $(date '+%Y-%m-%d %H:%M:%S')"}
}]
}
EOF
)
curl -s -o /dev/null -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK"
fi
return 1
fi
parse_and_check "$sensors_output"
}
# ============================================================
# Main
# ============================================================
ONCE=false
for arg in "$@"; do
case "$arg" in
--once) ONCE=true ;;
--help|-h) usage ;;
*) echo "Argumento desconocido: $arg"; usage ;;
esac
done
if [[ -z "$DISCORD_WEBHOOK" ]]; then
echo "AVISO: DISCORD_WEBHOOK no está definido. Las alertas solo se mostrarán por consola."
echo " Exporta la variable o pásala así:"
echo " DISCORD_WEBHOOK='https://discord.com/api/webhooks/...' $0"
echo ""
fi
echo "=== Monitor de temperatura Proxmox ==="
echo "Host SSH : $SSH_HOST"
echo "Umbral CPU : ${THRESH_CPU}°C"
echo "Umbral NVMe: ${THRESH_NVME}°C"
echo "Umbral PCH : ${THRESH_PCH}°C"
echo "Cooldown : ${COOLDOWN}s"
[[ "$ONCE" == false ]] && echo "Intervalo : ${INTERVAL}s"
echo ""
if $ONCE; then
run_check
else
while true; do
run_check
sleep "$INTERVAL"
done
fi
Sin embargo, después de un tiempo veo que me envía notificaciones diciendo que toda la CPU está a 100 °C, y me parece imposible porque cuando ejecuto sensors manualmente, la temperatura no llega a 50 °C.
Efectivamente, la expresión regular no lo estaba capturando bien. Así que la cambiamos para que, esta vez sí, solo mire la primera temperatura que aparece:
[...]
if [[ "$line" =~ ^(Package\ id\ 0|Core\ [0-9]+):[[:space:]]+\+([0-9]+\.[0-9]+)°C ]]; then
[...]
elif [[ "$line" =~ ^Composite:[[:space:]]+\+([0-9]+\.[0-9]+)°C ]]; then
[...]
Ahora sí nos notifica de manera correcta y vemos que las temperaturas están controladas.