Why monitor what must run, not just what responds
An uptime monitor checks that a service responds. It cannot see what happens between two requests: the 3 a.m. backup, invoice sending, a certificate renewal, the export to your accountant. These jobs fail silently — a full disk, an expired password, a server rebooted without its timer — and you notice on the day you need them.
Cron's MAILTO variable barely helps: it only sends a message if the job runs and writes to its output, and says nothing when the job never starts. Healthchecks starts from absence: every job has an expected schedule, and silence beyond the grace time triggers the alert.
What Healthchecks brings
- Ping-based monitoring — each job calls a unique URL; a missing ping triggers the alert.
- Cron or period schedules — a cron expression with time zone, or a simple interval, plus a grace time.
- Run duration —
/startand/failpings measure duration and report explicit failures. - Exit codes and logs — the exit code and the tail of the command output, viewable in the dashboard.
- More than twenty integrations — email, webhook, Slack, Discord, Matrix, ntfy, Telegram, Gotify, PagerDuty, Opsgenie…
- Team and API — separate projects, invited members, status badges and a REST API to create checks from your scripts.
Healthchecks compared with common approaches
Scroll the table
| Healthchecks | Cron MAILTO | Uptime monitor | |
|---|---|---|---|
| Alert if the job never starts | Yes | No | No |
| Alert if the job fails | Yes, exit code or /fail | Only if it writes output | No |
| Run duration | Measured | No | No |
| Machines with no open port | Yes, a simple outbound HTTPS call | Yes | No, the target must be reachable |
| Alert channels | More than twenty integrations | Local email | Depends on the tool |
| Central history | Yes, per check | No | Yes, for services |
Prerequisites
An Ubuntu 24.04 VPS with 1 GB of RAM is enough: Healthchecks runs as a single container and stores its data in an SQLite database. You need Docker with the Compose plugin, and a domain name pointing to the VPS, with nginx and an HTTPS certificate: ping URLs and alert links are built on that address, and the application rejects requests sent to another name.
Ideally, install Healthchecks on a machine separate from the servers it watches: if it goes down with them, nobody will warn you.
Install Healthchecks with Docker Compose
Prepare the .env file
Create the application directory and a
.envfile holding the secret key and the public address. Replacehc.example.comwith your domain.mkdir -p /opt/healthchecks && cd /opt/healthchecks cat > .env <<EOF SECRET_KEY=$(openssl rand -hex 24) SITE_ROOT=https://hc.example.com EOFCreate the Compose file
Put this
docker-compose.ymlin the same directory. The port is published on the loopback only, public sign-up is closed and the SQLite database lives in a volume.services: healthchecks: image: healthchecks/healthchecks:v5.0.1 restart: unless-stopped ports: - "127.0.0.1:8102:8000" env_file: .env environment: DEBUG: "False" DB_NAME: /data/hc.sqlite REGISTRATION_OPEN: "False" GUNICORN_WORKERS: "2" volumes: - healthchecks-data:/data volumes: healthchecks-data: {}Start and create the administrator account
On start, the container runs the migrations, then launches the web server and the alert-sending processes. Then create your account: the command asks for an email address and a password.
docker compose up -d docker compose exec healthchecks ./manage.py createsuperuserPublish over HTTPS behind nginx
Add a
serverblock for your domain, with a Let's Encrypt certificate, and forward the origin headers: withoutX-Forwarded-Proto, login fails with a CSRF error.location / { proxy_pass http://127.0.0.1:8102; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
Keep sign-up closed
By default, Healthchecks accepts public sign-ups. REGISTRATION_OPEN: "False" closes them: only the accounts you create or invite into a project can log in. On a server exposed to the Internet, set this from the very first start.
Wire up your cron jobs
In the dashboard, create a check, enter the schedule — the job's cron expression and its time zone — then a grace time covering its normal duration. Copy the ping URL and append it to the job.
# crontab: backup at 3 a.m., ping only on success
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://hc.example.com/ping/<uuid>
# variant: signal the start, then send the exit code
URL=https://hc.example.com/ping/<uuid>
curl -fsS -m 10 --retry 5 -o /dev/null "$URL/start"
/usr/local/bin/backup.sh > /tmp/backup.log 2>&1
curl -fsS -m 10 --retry 5 -o /dev/null --data-binary @/tmp/backup.log "$URL/$?"The first form pings only on success: a failure shows up as a missing ping, hence an alert after the grace time. The second is more telling: /start measures the duration, the exit code sent in place of the ping reports failure immediately, and the output sent in the request body appears in the check's history. The -m 10 --retry 5 options keep a flaky network from blocking the job or faking an outage.
Receive the alerts
Under Integrations, add at least one channel you actually read, then attach it to your checks. Webhook, Slack (incoming webhook), Mattermost, ntfy and Gotify work without any server-side configuration. Other integrations need credentials declared in .env: a bot token for Telegram, an account and token for Matrix, an OAuth app for Discord.
Email alerts go through an SMTP server: add EMAIL_HOST, EMAIL_PORT, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD and DEFAULT_FROM_EMAIL to .env, then restart with docker compose up -d. The same settings serve team invitations and periodic reports.
Troubleshooting
The page returns a 400 error. The request does not arrive with the right host name: check that SITE_ROOT exactly matches the domain served by nginx, and that the location block forwards the Host header.
Login fails with a CSRF error. nginx does not forward X-Forwarded-Proto: the application thinks it receives HTTP while the browser speaks HTTPS. Add the header and reload nginx.
A check goes into alert although the job ran. The ping did not leave: run the curl command by hand from the machine concerned, check outbound HTTPS access, and lengthen the grace time if the job lasts longer than expected.
No email arrives. Without EMAIL_HOST, Healthchecks sends nothing and says so in its logs at start-up: check docker compose logs healthchecks.
One-click install from the ServOrbit Marketplace
The ServOrbit Marketplace installs Healthchecks on your VPS without a command line: it deploys the container with sign-up closed, creates the administrator account with a generated password, and publishes the application over HTTPS on a subdomain of your domain or on the VPS's free subdomain. The login and password appear in your client area; all that is left is to create your checks.