← Blog

Auto-hospedar Healthchecks.io para monitorear tareas cron

Railyard

19 de julio de 2026 · 11 min de lectura

Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta

Una tarea cron que deja de ejecutarse en silencio no puede avisarte que se detuvo. El script simplemente no se ejecuta una noche, nada falla, nada registra un error, y tres semanas después descubres de la peor manera que los respaldos no se han ejecutado desde que se actualizó el kernel del servidor y el demonio cron nunca volvió a arrancar. Healthchecks.io es un dead man’s switch diseñado exactamente para esto: cada una de tus tareas programadas envía un ping a una URL única cuando termina, y si ese ping no llega según el horario que le indicaste a Healthchecks, te avisa por correo electrónico, Slack, un webhook, o cualquiera de sus otras integraciones. Esta guía configura una instancia auto-hospedada en tu propio servidor con Postgres, HTTPS mediante Caddy y correo de alerta real, y luego muestra el atajo de un clic si prefieres saltarte la configuración manual.

Lo que necesitas

  • Un servidor. Un Hetzner CX23 (2 vCPU, 4 GB de memoria, 40 GB NVMe) a 5,99 €/mes sin IVA es suficiente para Healthchecks, su propio Postgres y un proxy inverso funcionando juntos en una sola máquina, para uso personal o el volumen de tareas cron de un equipo pequeño.
  • Un dominio (o un subdominio de uno que ya tengas), con acceso a su configuración DNS.
  • Docker y el plugin de Docker Compose instalados en el servidor (cubierto en el paso 2).
  • Una cuenta SMTP o un relay por el que puedas enviar correo, ya que los correos de alerta son todo el sentido de ejecutar esto.
  • Una terminal y acceso SSH al servidor.

Paso 1: crear el servidor y apuntar un dominio hacia él

Levanta un servidor Ubuntu 24.04 en el plan CX23 de Hetzner (2 vCPU, 4 GB de memoria, 40 GB NVMe, 5,99 €/mes sin IVA). Anota su dirección IP pública, y agrega tu clave SSH al crearlo o inmediatamente después, para no depender de una contraseña.

En la configuración DNS de tu dominio, agrega un registro A que apunte un subdominio, por ejemplo checks.tudominio.com, a esa IP:

Type: A
Name: checks
Value: <la IP de tu servidor>
TTL: 300

Confirma que resuelve antes de continuar:

dig checks.tudominio.com +short

Eso debería mostrar la IP de tu servidor, no una anterior y no nada. Mientras tanto, asegura el firewall para dejar solo lo que esta pila necesita:

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Paso 2: instalar Docker

Conéctate por SSH al servidor e instala Docker Engine junto con el plugin de Compose:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Cierra sesión y vuelve a entrar para que el cambio de grupo tenga efecto, luego confirma que ambas piezas están presentes:

docker --version
docker compose version

Paso 3: organizar el proyecto y sus secretos

Crea un directorio para la pila:

mkdir -p ~/healthchecks && cd ~/healthchecks

Genera una clave secreta aleatoria y una contraseña de base de datos. Healthchecks usa SECRET_KEY para firmar las sesiones, así que un valor débil o reutilizado es un problema real, no una formalidad:

openssl rand -base64 32
openssl rand -base64 24

Crea .env con las claves de configuración propias del proyecto, completadas para tu dominio:

# .env
DOMAIN=checks.tudominio.com
DB=postgres
DB_HOST=db
DB_PORT=5432
DB_NAME=healthchecks
DB_USER=postgres
DB_PASSWORD=<la contraseña que generaste>
ADMINS=tu@tudominio.com
DEFAULT_FROM_EMAIL=healthchecks@tudominio.com
SITE_ROOT=https://checks.tudominio.com
SITE_NAME=Healthchecks
PING_ENDPOINT=https://checks.tudominio.com/ping/
REGISTRATION_OPEN=False
SECRET_KEY=<la clave que generaste>
GUNICORN_WORKERS=4
GUNICORN_PORT=8000

Algunos de estos valores importan más de lo que parece:

  • SITE_ROOT es la URL HTTPS completa de tu instancia, no solo el dominio desnudo. Healthchecks lo usa para construir cada enlace que te muestra o te envía por correo, incluidas las propias URL de ping.
  • PING_ENDPOINT debería coincidir con SITE_ROOT con /ping/ agregado al final. Es un parámetro separado porque algunas instalaciones sirven el tráfico de ping desde una ruta distinta y más ligera que la interfaz principal; en esta configuración de un solo contenedor, ambos comparten el mismo dominio.
  • REGISTRATION_OPEN=False cierra el registro público en tu instancia. Viene abierto por defecto, lo cual es el único parámetro de este archivo que no deberías olvidar, ya que una página de registro abierta en un dominio público es una invitación para que extraños creen cuentas en tu servidor.
  • DOMAIN es una segunda forma, en dominio desnudo, del mismo valor, usada por el Caddyfile en el paso 5. Healthchecks en sí nunca lee DOMAIN; lee SITE_ROOT.

También necesitarás parámetros SMTP para que Healthchecks pueda realmente enviarte correos de alerta, ya que una herramienta de monitoreo que no puede enviarte un correo cuando una tarea se queda callada no está haciendo su trabajo. Agrega EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_PORT y EMAIL_USE_TLS al mismo archivo .env, apuntando al proveedor SMTP o relay que ya uses. La documentación de configuración auto-hospedada del propio proyecto cubre cada variable relacionada con correo en detalle si tu proveedor necesita algo más allá de lo básico.

Fija los permisos para que solo tu usuario pueda leer el archivo:

chmod 600 .env

Paso 4: escribir el archivo de Docker Compose

El propio docker/docker-compose.yml de Healthchecks, en el repositorio de GitHub del proyecto, está escrito para construir la imagen localmente desde docker/Dockerfile y publicar el puerto 8000 directamente al host, lo cual es la forma correcta para ejecutarlo en tu propia máquina pero no para un servidor expuesto a internet. Esta configuración lo adapta de tres maneras: usa la imagen preconstruida healthchecks/healthchecks en lugar de construir desde el código fuente, fija una etiqueta de versión exacta en lugar de latest para que un reinicio desatendido no pueda moverte silenciosamente a una nueva versión, y no publica el puerto 8000 al host en absoluto, ya que Caddy lo atiende en su lugar.

Crea docker-compose.yml:

services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - healthchecks-net

  web:
    image: healthchecks/healthchecks:v5.0.1
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    networks:
      - healthchecks-net

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - DOMAIN=${DOMAIN}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - web
    networks:
      - healthchecks-net

networks:
  healthchecks-net:

volumes:
  db-data:
  caddy_data:
  caddy_config:

postgres:17-alpine reemplaza el postgres:16 desnudo del ejemplo del propio proyecto; ambos funcionan bien, 17 es simplemente la versión más actual al momento de escribir esto. POSTGRES_USER se deja sin definir a propósito, lo que hace que Postgres use por defecto un usuario postgres, coincidiendo con DB_USER=postgres en .env sin repetirlo. Solo Caddy publica puertos hacia el host; db y web solo son alcanzables en la red interna de Docker.

Paso 5: escribir el Caddyfile y arrancar todo

Crea Caddyfile en el mismo directorio:

{$DOMAIN} {
	reverse_proxy web:8000
}

Arranca la pila:

docker compose up -d

Observa cómo Caddy obtiene un certificado:

docker compose logs -f caddy

Deberías ver una línea mencionando un certificado obtenido para tu dominio, y luego https://checks.tudominio.com debería cargar con un candado válido, sin advertencias.

Paso 6: configuración del primer arranque y cierre del registro

Visitar tu dominio por primera vez muestra la propia página de registro de Healthchecks. No hay un paso de CLI separado para crear una cuenta de administrador aquí; la primera cuenta que registres es la que usarás para gestionar verificaciones, proyectos e integraciones de ahí en adelante. Regístrala ahora, inicia sesión y crea tu primera verificación para confirmar que los pings la alcanzan.

Una vez que esa cuenta existe, asegúrate de que REGISTRATION_OPEN=False esté definido en .env (ya lo está, desde el paso 3) y vuelve a desplegar el servicio web para que el parámetro surta efecto si cambiaste algo:

docker compose up -d web

A partir de este momento, visitar la página de registro en tu instancia muestra un mensaje de registro cerrado en lugar de un formulario funcional, así que nadie más puede crear una cuenta en tu servidor.

Paso 7: respaldar la base de datos y la clave secreta

La base de datos contiene cada proyecto, verificación, integración e historial de ping que tienes. Un volcado de Postgres diario, guardado fuera del servidor:

docker compose exec -T db pg_dump -U postgres healthchecks > "healthchecks-backup-$(date +%F).sql"

Pon eso en una tarea cron (irónicamente, una que podrías hacer monitorear por el propio Healthchecks) y copia el resultado a otro lugar que no sea este servidor, ya que un respaldo que solo vive en el servidor que respalda no sobrevive al modo de falla que realmente destruye un servidor.

Respalda también .env, guardado por separado y con acceso controlado. Su SECRET_KEY firma cada sesión que Healthchecks emite; perderla solo significa que todos quedan desconectados y deben iniciar sesión de nuevo, lo cual es una molestia menor, pero una copia filtrada de esa clave le permite a alguien más forjar una sesión, así que trátala con el mismo cuidado que la contraseña de la base de datos.

Paso 8: actualizar sin sorpresas

Revisa las notas de la versión en el repositorio de GitHub del proyecto entre la etiqueta que tienes corriendo y la que vas a usar, luego cambia la versión en docker-compose.yml:

image: healthchecks/healthchecks:<new-version>

Descarga y reinicia solo ese servicio:

docker compose pull web
docker compose up -d web

Revisa los registros para confirmar un arranque limpio y un paso de migración exitoso:

docker compose logs web

Se espera que la imagen preconstruida ejecute automáticamente cualquier migración de base de datos pendiente como parte de su propio arranque, así que unos registros limpios sin error de migración es lo que buscas; si algo falla, tu respaldo de Postgres del paso 7 es el camino de regreso a la versión anterior.

El atajo: hazlo con Railyard

Todo lo anterior, una instancia de Healthchecks respaldada por Postgres con una clave secreta generada y HTTPS ya configurado, también existe como plantilla de un clic:

https://app.railyard.run/deploy?template=healthchecks

Railyard la construye directamente desde el docker/Dockerfile del propio repositorio del proyecto, provisiona automáticamente una base de datos Postgres administrada para ella, y ejecuta el propio comando de migración del proyecto como paso de release antes de cada despliegue, el mismo python manage.py migrate que ejecutarías manualmente de otra forma. También define ALLOWED_HOSTS=* y genera una SECRET_KEY real por ti, así que no hay ningún secreto que inventar y pegar tú mismo.

Si prefieres partir de Railyard en general antes de decidirte por Healthchecks específicamente, la guía de inicio rápido cubre lo básico, botón de despliegue explica el formato de enlace ?template= si quieres incrustar uno en otro lugar, y bases de datos y respaldos cubre cómo funcionan los respaldos en las plantillas en general, no solo en esta. El plan Free de Railyard incluye un servidor más un servidor de base de datos y tres aplicaciones sin tarjeta requerida, Pro cuesta $19/mes por equipo, y Team cuesta $49/mes por equipo.

Solución de problemas

Los correos de alerta nunca llegan. Esto casi siempre es por parámetros SMTP faltantes o incorrectos, no un error de Healthchecks. Verifica que EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_PORT y EMAIL_USE_TLS estén todos definidos en .env y que la cuenta que describen realmente pueda enviar correo; muchos proveedores bloquean el primer envío desde un servidor o IP nuevos hasta verificarlo. Una herramienta de monitoreo cuyo propio camino de alerta está roto fallará en silencio justo en la única tarea para la que existe, así que envíate una falla de prueba y confirma que el correo realmente llega antes de confiar en esto para algo real.

Las URL de ping mostradas en la interfaz apuntan al dominio equivocado. SITE_ROOT no coincide con la dirección HTTPS real desde la que sirves la aplicación. Corrígelo en .env y vuelve a desplegar web; cada URL de ping que Healthchecks ya te mostró o puso en un correo fue construida con el valor anterior y necesita volver a copiarse desde la interfaz después de la corrección.

Bad Request (400) al cargar el sitio. ALLOWED_HOSTS no incluye el dominio que estás visitando. La configuración aquí lo fija en * mediante el manejo por defecto del propio proyecto, así que esto normalmente solo aparece si has cambiado ese comportamiento; verifica que coincida con tu dominio real si lo hiciste.

Conexión a Postgres rechazada, o el contenedor web se reinicia constantemente. Revisa primero docker compose logs db para ver el error real. Una contraseña no coincidente entre DB_PASSWORD en .env y aquella con la que Postgres fue inicializado es la causa más común, generalmente por editar .env después de que el volumen db-data ya existe; Postgres solo lee POSTGRES_PASSWORD en el primer arranque con un directorio de datos vacío, así que un cambio posterior necesita un volumen nuevo o un cambio de contraseña directamente en la base de datos para que coincidan.

El HTTPS nunca se emite, o el navegador muestra una advertencia de certificado. Ejecuta dig checks.tudominio.com +short y confirma que retorna la IP de este servidor. Let’s Encrypt también necesita que el puerto 80 sea alcanzable desde internet para su paso de validación, así que verifica que ufw status muestre los puertos 80 y 443 abiertos, no solo el 443.

Preguntas frecuentes

¿Necesito Postgres, o SQLite funciona? Healthchecks admite SQLite para instalaciones pequeñas y de bajo tráfico, pero Postgres es la mejor opción por defecto en cuanto más de una o dos personas dependen de que esta instancia se mantenga disponible, ya que maneja escrituras concurrentes e historiales de ping más grandes con mayor soltura. Esta guía usa Postgres desde el inicio por esa razón.

¿Cómo apunto realmente una tarea cron hacia esto una vez que está corriendo? Agrega una llamada a curl al final de la propia tarea, después de que el comando que estás monitoreando tenga éxito:

tu-script-de-respaldo.sh && curl -fsS -m 10 --retry 5 https://checks.tudominio.com/ping/<uuid>

Cada verificación en la interfaz te da su propia URL de ping única para copiar en la tarea. Si el script falla antes de llegar a esa línea, no llega ningún ping, y esa es exactamente la señal que Healthchecks está esperando.

¿Cuál es la diferencia real con el healthchecks.io hospedado? Funcionalmente, muy poca; es la misma aplicación de código abierto en ambos casos. La versión hospedada se mantiene por ti y se factura mensualmente una vez superado su nivel gratuito; una instancia auto-hospedada cuesta solo el servidor en el que corre, sin límite en el número de verificaciones, pero eres tú quien la mantiene parchada y respaldada.

¿De verdad conviene cerrar REGISTRATION_OPEN? Sí, una vez que tu propia cuenta existe. No hay razón para que una instancia de monitoreo auto-hospedada en tu propio dominio acepte registros de cualquiera que se tope con la URL, y dejarla abierta es el único parámetro de toda esta guía que sí sería un error saltarse.

¿Cuánto cuesta realmente ejecutar esto? El servidor cuesta 5,99 €/mes sin IVA. Healthchecks en sí es software libre y de código abierto, sin cargo por verificación ni por integración; pagas por la máquina en la que corre, no por lo que corre en ella.

Una tarea cron que falla ruidosamente es fácil de notar. La que falla en silencio, el respaldo que dejó de correr hace tres semanas, la renovación de certificado que dejó de dispararse discretamente, es para eso que esto existe en realidad, y ahora tienes tu propia instancia vigilando justo eso.

Por qué eligen Railyard para esto

  • Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
  • Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
  • Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
  • Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
  • Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.

Sáltate la configuración del servidor

Despliega la plantilla verificada de Healthchecks en Railyard con un clic y obtén una instancia funcionando con Postgres y HTTPS ya configurados.

Plan gratis, sin tarjeta, y 14 días de Pro para cada equipo nuevo. Un precio fijo por equipo.