Healthchecks: Monitorización de Cron Jobs Self-Hosted

Healthchecks es una herramienta de monitorización de tareas programadas (cron jobs) que funciona como dead man's switch: cada tarea debe hacer ping a una URL única en el tiempo esperado, y si no lo hace, Healthchecks envía alertas a través del canal configurado. Su versión self-hosted proporciona el mismo sistema que el servicio cloud pero bajo tu control total, siendo ideal para monitorizar backups, sincronizaciones y cualquier proceso automatizado crítico. Esta guía cubre la instalación completa y configuración de Healthchecks en Linux.

Requisitos Previos

  • Linux con Docker y Docker Compose instalados
  • 512 MB RAM
  • Dominio con SSL para producción
  • Servidor SMTP para envío de alertas por email

Instalación con Docker

# Crear directorio de trabajo
mkdir -p /opt/healthchecks
cd /opt/healthchecks

# Crear el archivo de configuración de entorno
cat > .env << 'EOF'
# Secreto de Django (mínimo 50 caracteres aleatorios)
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")

# Configuración de la base de datos
DB=postgres
DB_HOST=db
DB_PORT=5432
DB_NAME=hc
DB_USER=hc
DB_PASSWORD=password-seguro-aqui

# URL base (para generar URLs absolutas en emails)
SITE_ROOT=https://cron.tudominio.com
SITE_NAME=Mi Monitorización Cron

# Configuración de email
[email protected]
EMAIL_HOST=smtp.tudominio.com
EMAIL_PORT=587
EMAIL_USE_TLS=true
[email protected]
EMAIL_HOST_PASSWORD=tu-password-smtp

# Superadmin
[email protected]
SUPERUSER_PASSWORD=AdminPassword123!

# Registro de nuevos usuarios (false para servidor privado)
REGISTRATION_OPEN=False
EOF

Docker Compose

# docker-compose.yml
version: '3'

services:
  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: hc
      POSTGRES_USER: hc
      POSTGRES_PASSWORD: password-seguro-aqui
    restart: unless-stopped

  web:
    image: healthchecks/healthchecks:latest
    depends_on:
      - db
    env_file: .env
    ports:
      - "127.0.0.1:8000:8000"
    restart: unless-stopped
    command: gunicorn hc.wsgi:application --bind 0.0.0.0:8000 --workers 4

  worker:
    image: healthchecks/healthchecks:latest
    depends_on:
      - db
    env_file: .env
    restart: unless-stopped
    command: python manage.py sendalerts

volumes:
  postgres_data:
# Iniciar los servicios
docker compose up -d

# Ver los logs durante el inicio
docker compose logs -f

# Crear el superusuario inicial
docker compose exec web python manage.py createsuperuser

# Aplicar las migraciones de base de datos (si no se hicieron automáticamente)
docker compose exec web python manage.py migrate

Proxy inverso con Nginx

server {
    listen 443 ssl http2;
    server_name cron.tudominio.com;

    ssl_certificate /etc/letsencrypt/live/cron.tudominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cron.tudominio.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }
}

Configuración Inicial

Accede a https://cron.tudominio.com con las credenciales del superusuario.

Configurar proyectos y checks

# Crear un proyecto via API
# Primero obtener la API key del usuario en: Profile > API Access
API_KEY="tu-api-key-de-usuario"
HC_URL="https://cron.tudominio.com"

# Crear un proyecto
PROJECT=$(curl -s -X POST "$HC_URL/api/v2/projects/" \
    -H "X-Api-Key: $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name": "Servidores de Produccion"}')

PROJECT_KEY=$(echo $PROJECT | jq -r '.api_key')
echo "API Key del proyecto: $PROJECT_KEY"

Crear y Gestionar Checks

Desde la interfaz web

  1. Ve al proyecto > New Check
  2. Configura:
    • Nombre: "Backup diario PostgreSQL"
    • Schedule: 0 3 * * * (expresión cron)
    • Grace time: 30 minutos (tiempo extra antes de alertar)
    • Tags: backup, produccion

Desde la API

PROJECT_API_KEY="api-key-del-proyecto"
HC_URL="https://cron.tudominio.com"

# Crear un check con schedule cron
CHECK=$(curl -s -X POST "$HC_URL/api/v2/checks/" \
    -H "X-Api-Key: $PROJECT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "name": "Backup diario PostgreSQL",
        "tags": "backup produccion",
        "desc": "Backup completo de todas las bases de datos",
        "schedule": "0 3 * * *",
        "tz": "Europe/Madrid",
        "grace": 1800,
        "channels": "*"
    }')

PING_URL=$(echo $CHECK | jq -r '.ping_url')
echo "URL de ping: $PING_URL"

# Crear check con periodo simple (cada N segundos/minutos)
curl -s -X POST "$HC_URL/api/v2/checks/" \
    -H "X-Api-Key: $PROJECT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "name": "Sincronización de archivos",
        "timeout": 3600,
        "grace": 600
    }'

# Listar todos los checks del proyecto
curl "$HC_URL/api/v2/checks/" \
    -H "X-Api-Key: $PROJECT_API_KEY" | jq '.checks[] | {name, status, last_ping}'

# Ver un check específico
CHECK_UUID="uuid-del-check"
curl "$HC_URL/api/v2/checks/$CHECK_UUID" \
    -H "X-Api-Key: $PROJECT_API_KEY" | jq '.'

Protocolos de Ping

Healthchecks acepta pings por HTTP(S). Las URL siguen el formato: https://cron.tudominio.com/ping/UUID

Tipos de ping

PING_URL="https://cron.tudominio.com/ping/UUID_DEL_CHECK"

# Ping de éxito (tarea completada correctamente)
curl -fsS --retry 3 $PING_URL

# Ping de inicio (tarea comenzó)
curl -fsS --retry 3 $PING_URL/start

# Ping de fallo (tarea falló - alerta inmediata)
curl -fsS --retry 3 $PING_URL/fail

# Ping con log (incluir salida del comando)
curl -fsS --retry 3 --data-binary @- $PING_URL << 'EOF'
Backup completado: 15,234 archivos respaldados
Tamaño total: 2.3 GB
Tiempo de ejecución: 4m 32s
EOF

# Enviar el código de salida del comando
EXIT_CODE=0
curl -fsS --retry 3 "$PING_URL/$EXIT_CODE"

Integración con Cron y Systemd

Wrapper para cron jobs

# Patrón recomendado: medir el tiempo y enviar logs
PING_URL="https://cron.tudominio.com/ping/UUID_DEL_CHECK"

# Notificar inicio y ejecutar el backup
curl -fsS --retry 3 -m 10 "$PING_URL/start" > /dev/null 2>&1

# Ejecutar el backup y capturar la salida
if OUTPUT=$(mi-script-de-backup.sh 2>&1); then
    # Éxito: enviar ping con log
    echo "$OUTPUT" | curl -fsS --retry 3 -m 10 --data-binary @- "$PING_URL"
else
    # Fallo: enviar ping de fallo con el error
    echo "$OUTPUT" | curl -fsS --retry 3 -m 10 --data-binary @- "$PING_URL/fail"
fi

Crontab con Healthchecks

# /etc/cron.d/mis-backups

# Backup de base de datos con monitorización
0 3 * * * root curl -fsS -m 10 https://cron.tudominio.com/ping/UUID/start; \
    /usr/local/bin/pg-backup.sh 2>&1 | \
    curl -fsS -m 30 --data-binary @- https://cron.tudominio.com/ping/UUID

# Sincronización de archivos con timeout
*/15 * * * * user curl -fsS -m 5 https://cron.tudominio.com/ping/UUID-SYNC/start; \
    rsync -avz /datos/ servidor-backup:/backups/datos/ && \
    curl -fsS -m 5 https://cron.tudominio.com/ping/UUID-SYNC || \
    curl -fsS -m 5 https://cron.tudominio.com/ping/UUID-SYNC/fail

Integración con systemd timers

# Para systemd timers, agregar el ping al ExecStart del servicio
cat > /etc/systemd/system/backup-db.service << 'EOF'
[Unit]
Description=Backup de base de datos
After=network.target

[Service]
Type=oneshot
# Notificar inicio
ExecStartPre=curl -fsS --retry 3 -m 10 https://cron.tudominio.com/ping/UUID/start
# Ejecutar el backup
ExecStart=/usr/local/bin/backup-postgres.sh
# Notificar éxito al terminar
ExecStartPost=curl -fsS --retry 3 -m 10 https://cron.tudominio.com/ping/UUID
# En caso de fallo, el servicio marca error y el ping de éxito no se envía
OnFailure=curl -fsS --retry 3 -m 10 https://cron.tudominio.com/ping/UUID/fail
EOF

# Timer asociado al servicio
cat > /etc/systemd/system/backup-db.timer << 'EOF'
[Unit]
Description=Backup de base de datos - diario a las 3 AM
After=network.target

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=300  # Retraso aleatorio de hasta 5 minutos
Persistent=true

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now backup-db.timer

# Ver los timers activos
systemctl list-timers

Canales de Notificación

Healthchecks soporta múltiples canales de alerta:

Email

La configuración de SMTP en el .env habilita las alertas por email automáticamente:

  • Ve a Settings > Integrations > Email
  • Agrega las direcciones de email a notificar

Webhook (ntfy, Slack, etc.)

# En la interfaz web: Project > Add Integration > Webhook
# URL: https://ntfy.tudominio.com/alertas-cron
# Method: POST
# Headers:
#   Content-Type: application/json
#   Authorization: Bearer TU_NTFY_TOKEN
# Request body (template):
{
    "title": "Alerta cron: $NAME",
    "message": "Estado: $STATUS\nÚltimo ping: $LAST_PING",
    "priority": "high"
}

Telegram

# Crear bot en Telegram: https://t.me/BotFather
# Obtener el Token y el Chat ID

# En Healthchecks: Add Integration > Telegram
# Token: el token del bot de BotFather
# Chat ID: ID del chat donde enviar alertas

# Puedes obtener el Chat ID con:
curl "https://api.telegram.org/botTU_TOKEN/getUpdates"
# Envía un mensaje al bot primero para que aparezca en getUpdates

PagerDuty y OpsGenie

# Para alertas on-call con escalado:
# Add Integration > PagerDuty o OpsGenie
# Ingresar la Integration Key correspondiente

Solución de Problemas

Los pings no se reciben

# Verificar que el servidor de Healthchecks está accesible
curl -I https://cron.tudominio.com

# Probar el ping manualmente
curl -v https://cron.tudominio.com/ping/UUID_DEL_CHECK

# Ver los logs de Healthchecks
cd /opt/healthchecks
docker compose logs web --tail=50

Las alertas de email no llegan

# Verificar la configuración de SMTP
docker compose exec web python manage.py sendtestemail [email protected]

# Ver el log del worker de alertas
docker compose logs worker --tail=50

# Verificar la conectividad con el servidor SMTP
docker compose exec web python -c "
import smtplib
server = smtplib.SMTP('smtp.tudominio.com', 587)
server.starttls()
server.login('user', 'pass')
print('SMTP OK')
server.quit()
"

Los checks aparecen siempre como "up" aunque fallen

# Verificar que el script envía el ping de fallo correctamente
# El patrón correcto es:
#   comando && curl PING_URL || curl PING_URL/fail

# Si el comando siempre devuelve 0 (exit code 0), nunca enviará /fail
# Verificar el exit code:
mi-comando; echo "Exit code: $?"

Conclusión

Healthchecks self-hosted convierte la monitorización de tareas programadas en una práctica robusta y centralizada: en lugar de confiar en que los cron jobs se ejecutan sin errores, obtienes visibilidad activa de cada ejecución con historial, métricas de tiempo de ejecución y alertas configurables por múltiples canales. Su modelo de dead man's switch garantiza que si una tarea deja de ejecutarse (por cualquier razón, incluyendo fallos del servidor), recibirás una alerta, lo que lo hace indispensable en sistemas de producción donde los backups y procesos automáticos son críticos.