Healthchecks es una herramienta de monitorización de tareas programadas que funciona como un interruptor de hombre muerto: cada tarea hace una petición HTTP (un ping) a una URL única cuando se ejecuta, y si el ping no llega a tiempo o llega marcando un error, Healthchecks envía una alerta. Así detectas tanto las copias de seguridad que fallan como las que dejan de ejecutarse sin avisar. En este tutorial desplegarás Healthchecks con Docker Compose y PostgreSQL en Ubuntu 24.04, lo publicarás con Nginx y HTTPS, y conectarás tareas de cron y timers de systemd mediante un pequeño script envoltorio.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM y un usuario no root con privilegios
sudo. - Docker Engine y el plugin Docker Compose instalados.
- Nginx instalado y los puertos 80 y 443 abiertos. Si usas UFW:
sudo ufw allow 'Nginx Full'. - Un subdominio (en esta guía,
hc.your_domain) con un registro DNS A que apunte a la IP del servidor. - Una cuenta SMTP para enviar las alertas por correo (servidor, puerto, usuario y contraseña).
Paso 1: Preparar la configuración
Crea el directorio del proyecto:
sudo mkdir -p /opt/healthchecks
sudo chown "$USER": /opt/healthchecks
cd /opt/healthchecks
Healthchecks se configura con variables de entorno. Genera el archivo .env con una clave secreta y una contraseña de base de datos aleatorias. El heredoc sin comillas hace que la shell sustituya las llamadas a openssl por su resultado:
cat > .env <<EOF
SECRET_KEY=$(openssl rand -hex 32)
DEBUG=False
SITE_ROOT=https://hc.your_domain
SITE_NAME=Healthchecks
ALLOWED_HOSTS=hc.your_domain
REGISTRATION_OPEN=False
DB=postgres
DB_HOST=db
DB_NAME=hc
DB_USER=hc
DB_PASSWORD=$(openssl rand -hex 24)
DEFAULT_FROM_EMAIL=healthchecks@your_domain
EMAIL_HOST=smtp.your_domain
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=smtp_user
EMAIL_HOST_PASSWORD=smtp_password
EOF
chmod 600 .env
Abre el archivo y sustituye los datos SMTP (smtp.your_domain, smtp_user, smtp_password) y el dominio por los tuyos:
nano .env
Las variables más importantes:
SITE_ROOTes la URL pública exacta, conhttps://. Healthchecks la usa para generar las URLs de ping y los enlaces de los correos.ALLOWED_HOSTSlimita los nombres de host a los que responde la aplicación.REGISTRATION_OPEN=Falseimpide que cualquiera cree una cuenta en tu instancia.DEBUG=Falsees imprescindible en producción.
Paso 2: Desplegar Healthchecks con Docker Compose
La imagen oficial healthchecks/healthchecks sirve la aplicación en el puerto 8000 y ejecuta en el mismo contenedor el proceso que envía las alertas. Crea el archivo de Compose:
nano docker-compose.yml
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 5s
retries: 10
web:
image: healthchecks/healthchecks:latest
restart: unless-stopped
env_file: .env
ports:
- "127.0.0.1:8000:8000"
depends_on:
db:
condition: service_healthy
volumes:
db-data:
Docker Compose lee .env para sustituir las variables ${DB_*} del servicio de PostgreSQL, y env_file pasa el archivo completo al contenedor de Healthchecks, así que ambos comparten la misma contraseña. La condición service_healthy retrasa el arranque de la aplicación hasta que PostgreSQL acepta conexiones.
Arranca los servicios:
docker compose up -d
Asegúrate de que el esquema de la base de datos está aplicado. Si ya lo estaba, el comando indica que no hay migraciones pendientes:
docker compose exec web /opt/healthchecks/manage.py migrate
...
Running migrations:
No migrations to apply.
Crea la cuenta de administrador. El comando pide un correo y una contraseña:
docker compose exec web /opt/healthchecks/manage.py createsuperuser
Comprueba que la aplicación responde en local:
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: hc.your_domain" http://127.0.0.1:8000/
Un 200 o un 302 indica que funciona. Un 400 suele significar que ALLOWED_HOSTS no coincide con el host de la petición.
Paso 3: Publicar Healthchecks con Nginx y HTTPS
Crea el sitio de Nginx:
sudo nano /etc/nginx/sites-available/healthchecks
server {
listen 80;
listen [::]:80;
server_name hc.your_domain;
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-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Activa el sitio, valida y recarga Nginx:
sudo ln -s /etc/nginx/sites-available/healthchecks /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Obtén el certificado con Certbot:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d hc.your_domain
Abre https://hc.your_domain e inicia sesión con la cuenta creada en el paso anterior. Verás un proyecto vacío.
Paso 4: Crear un check
Un check representa una tarea programada. Para crear uno desde la web:
- Pulsa Add Check.
- Ponle un nombre, por ejemplo
backup-postgresql, y opcionalmente etiquetas comobackup prod. - En la configuración del horario, elige Cron e introduce la misma expresión que usa la tarea, por ejemplo
0 3 * * *, y la zona horaria del servidor. - En Grace Time, indica cuánto tiempo puede retrasarse o durar la tarea antes de alertar, por ejemplo 30 minutos.
- Copia la URL de ping del check, con la forma
https://hc.your_domain/ping/<uuid>.
Haz un ping manual para comprobar que funciona (sustituye your_check_uuid):
curl -fsS -m 10 https://hc.your_domain/ping/your_check_uuid
OK
El check pasa a verde (up) en el panel y el ping aparece en su historial.
Si prefieres crear los checks por código, genera una clave de API de lectura y escritura en Settings del proyecto, en la sección API Access. Este ejemplo crea el mismo check y, gracias a unique, no lo duplica si ejecutas la petición dos veces (sustituye your_api_key):
curl -s -X POST https://hc.your_domain/api/v3/checks/ \
-H "X-Api-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "backup-postgresql",
"tags": "backup prod",
"schedule": "0 3 * * *",
"tz": "Europe/Madrid",
"grace": 1800,
"channels": "*",
"unique": ["name"]
}'
La respuesta es el check en JSON, y el campo ping_url contiene la URL de ping.
Paso 5: Conectar las tareas con un script envoltorio
Healthchecks entiende varias variantes de la URL de ping:
| URL | Significado |
|---|---|
/ping/<uuid>/start | La tarea ha empezado. Permite medir su duración. |
/ping/<uuid> o /ping/<uuid>/0 | La tarea ha terminado bien. |
/ping/<uuid>/fail o /ping/<uuid>/<código> con código de 1 a 255 | La tarea ha fallado. Alerta inmediata. |
En lugar de repetir la lógica en cada línea de cron, crea un script que envíe el ping de inicio, ejecute el comando, adjunte las últimas líneas de su salida y envíe el código de salida real:
sudo nano /usr/local/bin/hc-run
#!/usr/bin/env bash
set -uo pipefail
if [[ $# -lt 2 ]]; then
echo "Uso: hc-run PING_URL COMANDO [ARGUMENTOS...]" >&2
exit 64
fi
url="$1"
shift
curl -fsS -m 10 --retry 5 -o /dev/null "${url}/start" || true
output="$("$@" 2>&1)"
status=$?
printf '%s\n' "$output"
printf '%s\n' "$output" | tail -c 10000 \
| curl -fsS -m 10 --retry 5 -o /dev/null --data-binary @- "${url}/${status}" || true
exit "$status"
sudo chmod 755 /usr/local/bin/hc-run
El script no usa set -e a propósito: debe seguir ejecutándose cuando el comando falla para poder informar del fallo. Los errores de curl se ignoran (|| true) para que una caída de Healthchecks no rompa tus tareas; si el ping no llega, Healthchecks alertará igualmente por falta de ping.
Pruébalo con un comando que falla, usando la URL del check:
hc-run https://hc.your_domain/ping/your_check_uuid false
echo "Código de salida: $?"
Código de salida: 1
En el panel, el check pasa a rojo (down) y recibes una alerta por los canales configurados. Ejecuta después hc-run https://hc.your_domain/ping/your_check_uuid true para devolverlo a verde.
Con cron
Añade la tarea en un archivo de /etc/cron.d:
sudo nano /etc/cron.d/backup-postgresql
0 3 * * * root /usr/local/bin/hc-run https://hc.your_domain/ping/your_check_uuid /usr/local/bin/backup-postgresql.sh
Sustituye /usr/local/bin/backup-postgresql.sh por tu script real. El horario de cron y el del check deben coincidir.
Con un timer de systemd
Crea la unidad de servicio:
sudo nano /etc/systemd/system/backup-postgresql.service
[Unit]
Description=Copia de seguridad de PostgreSQL
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hc-run https://hc.your_domain/ping/your_check_uuid /usr/local/bin/backup-postgresql.sh
Crea el timer que la lanza cada día a las 03:00:
sudo nano /etc/systemd/system/backup-postgresql.timer
[Unit]
Description=Copia de seguridad diaria de PostgreSQL
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
Activa el timer y lanza el servicio una vez a mano para comprobarlo:
sudo systemctl daemon-reload
sudo systemctl enable --now backup-postgresql.timer
sudo systemctl start backup-postgresql.service
systemctl list-timers backup-postgresql.timer
El ping aparece en el historial del check con la duración de la ejecución y la salida del script.
Paso 6: Configurar los canales de alerta
Comprueba primero que el envío de correo funciona:
docker compose exec web /opt/healthchecks/manage.py sendtestemail your_email@your_domain
Si el correo llega, ve a Integrations en el proyecto. El correo de tu cuenta suele aparecer ya como integración; si no, añádelo con Email y confirma la dirección desde el mensaje que recibirás.
Healthchecks incluye integraciones nativas con servicios como ntfy, Slack, Telegram o webhooks genéricos. Para ntfy, por ejemplo, elige ntfy en Integrations, indica la URL del servidor y el topic, y guarda. Cada integración tiene un botón para enviar una notificación de prueba.
Revisa en la configuración de cada check qué integraciones tiene activadas: los checks creados con "channels": "*" usan todas las del proyecto.
Solución de problemas
Error CSRF al iniciar sesión. SITE_ROOT no coincide con la URL con la que accedes (por ejemplo, falta https:// o el subdominio es distinto). Corrígelo en .env y recrea el contenedor con docker compose up -d --force-recreate web.
Los checks aparecen en verde aunque la tarea falle. El comando no devuelve un código de salida distinto de cero al fallar, o la línea de cron envía el ping con ; sin tener en cuenta el resultado. Usa hc-run, que envía el código real, y comprueba el código de tu script con tu_script; echo $?.
No llegan las alertas. Revisa los logs del contenedor con docker compose logs web --tail=50 y vuelve a probar el correo con sendtestemail. Si usas el puerto 587, EMAIL_USE_TLS debe ser True.
Conclusión
Tienes Healthchecks funcionando con PostgreSQL detrás de Nginx con HTTPS, checks con horario cron y un script que informa del inicio, la duración, la salida y el código de salida de cada tarea. Como siguientes pasos, puedes crear un check por cada tarea crítica (copias de seguridad, renovaciones, sincronizaciones), añadir una segunda integración como ntfy para no depender solo del correo e incluir el volumen de PostgreSQL en tus copias de seguridad.
