Cron jobs fail quietly. That's the annoying part. A backup script that's run every night at 2 AM for the past year can stop running for weeks before anyone notices — usually right when you actually need that backup and discover the last successful one is from six weeks ago.

Healthchecks flips the usual monitoring model around. Instead of a dashboard checking whether something is up, your script pings Healthchecks when it finishes. Miss the scheduled ping, and Healthchecks tells you. No agent to install on every machine, nothing to poll, no dashboard you have to remember to glance at.

This guide walks through installing a self-hosted Healthchecks instance inside an LXC container on Proxmox VE, using Docker Compose to run the app alongside a PostgreSQL database. If you've already got a Proxmox VE box doing homelab duty, this is a light addition — the whole stack idles at well under 300 MB of RAM once it's running.

What You Will Learn

By the end of this tutorial you'll have a working Healthchecks instance running in its own LXC container, with an admin account, your first monitored check, and a real cron job wired up to report in. Specifically, you'll learn how to:

  • Create a Debian 13 LXC container sized for a small Docker workload
  • Enable nesting, the setting almost everyone forgets before running Docker inside LXC
  • Install Docker and the Compose plugin inside the container
  • Clone the official Healthchecks repository and configure its environment file
  • Bring the stack up and create your first admin account from the command line
  • Set up a check and point a real cron job at it, including the retry flags that keep flaky networks from causing false alarms

What Is This Feature?

Healthchecks is an open-source monitoring tool built around a simple idea: your job pings a URL when it finishes, and Healthchecks watches the clock. Miss a scheduled ping, and it fires off an alert through email, Slack, Discord, a phone call, or one of a dozen other channels it supports. In monitoring circles this pattern has a name — a dead man's switch, borrowed from railway equipment that trips an alarm if the operator lets go of a lever and stops responding.

That's a different problem than the one tools like Uptime Kuma solve. Uptime Kuma reaches out and asks "are you up?" on a schedule. Healthchecks waits for your job to reach out and say "I finished." You want the second kind of monitoring for anything that runs on a schedule and produces no obvious symptom when it silently stops working — nightly backups, TLS certificate renewals, database exports, a ZFS scrub job, or that Python script quietly syncing files at 3 AM that nobody's looked at since they wrote it.

We're running Healthchecks inside an LXC container, which is Proxmox VE's lightweight alternative to a full virtual machine. An LXC container shares the host's Linux kernel instead of emulating its own hardware, so it starts in a second or two and uses a fraction of the RAM a comparable VM would need. For a small app like this, that's the right trade — you don't need the isolation of a full VM, just a clean, separate place to run it.

Inside that container we'll use Docker to run the actual application. Docker packages software together with everything it needs — the right Python version, system libraries, dependencies — into a container image, so you're not manually chasing down package versions on the host. Docker Compose is the tool that starts multiple related containers (here, the Healthchecks app and its PostgreSQL database) together with one command, using a single configuration file instead of a string of separate docker run commands.

Why Would You Use It?

Most homelab monitoring setups watch whether services are reachable. That covers Plex crashing or your reverse proxy falling over, but it does nothing for a backup script that exits early because a disk filled up, or a certificate renewal cron job that started throwing a Python traceback three weeks ago and has been failing silently ever since. Nothing crashed. Nothing looks down. The job just... stopped doing its job.

Healthchecks catches exactly that category of failure. You wire a single curl command into the end of any script, and if that script stops running — or starts failing before it reaches the ping — you get an alert instead of a nasty surprise during a restore.

There's also a practical reason to self-host rather than use the hosted healthchecks.io service: the free tier there caps you at 20 checks, and once you start monitoring every backup job, every cron task, and every certificate renewal across a homelab, that limit disappears fast. Self-hosting removes the cap entirely and keeps your job names, schedules, and failure history off a third-party server.

Honestly, if you already run backups and don't actively check whether they succeeded, this is one of the highest-value tools you can add to a homelab in a weekend. It's not glamorous. It just quietly tells you when something you depend on stops working.

Prerequisites

Before you start, make sure you have the following in place:

  • A working Proxmox VE 8.x or 9.x host with roughly 6 GB of free storage and 1 GB of RAM you're not already using
  • A Debian 13 (Trixie) LXC template downloaded — grab it from Datacenter → your node → local (Storage) → CT Templates → Templates if it isn't there already
  • Basic comfort using the Proxmox VE web interface and a terminal inside a container shell
  • SMTP credentials if you want email alerts (a free tier from something like Brevo or Mailgun works fine) — you can skip this and add it later, since Healthchecks also supports webhook and Slack-style notifications

You don't need a domain name or a TLS certificate to follow along. We'll access Healthchecks over plain HTTP on your local network first. Putting it behind a reverse proxy with HTTPS is worth doing before you rely on it for anything, but that's outside the scope of this guide.

Step-by-Step Tutorial

Step 1: Create the LXC Container

In the Proxmox VE web interface, click Create CT in the top-right corner and work through the wizard with these settings:

  • Hostname: something like healthchecks
  • Unprivileged container: leave this checked
  • Template: Debian 13 (Trixie)
  • Disk size: 6 GB is plenty to start
  • CPU cores: 1 is enough; 2 gives you headroom
  • Memory: 1024 MB, plus 512 MB of swap
  • Network: attach it to your usual bridge (vmbr0 in most setups) with DHCP or a static IP

Don't start the container yet. There's one setting almost everyone trying Docker inside LXC for the first time forgets, and it's worth doing before boot.

Step 2: Enable Nesting

Docker needs to create its own network namespaces and cgroups, which an LXC container doesn't allow by default for security reasons. Select the container, go to Options → Features, click Edit, and check the box labeled Nesting. Click OK.

Skip this and the Docker daemon will either refuse to start or fail partway through pulling images with a permission error that doesn't obviously point back to this setting. It's a two-second checkbox that saves twenty confused minutes later.

Now start the container and open its console, or SSH in once networking comes up.

Step 3: Update the Container and Install Docker

Update the package list first:

apt update && apt upgrade -y

Install the packages Docker's repository setup needs, then add the official Docker apt repository:

apt install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null

Then install Docker itself, along with the Compose plugin and Git, which you'll need in the next step:

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin git

Step 4: Clone the Healthchecks Repository

Healthchecks doesn't ship a single prebuilt Docker image on its own. Instead, you clone the repository and Compose builds the image locally the first time you bring the stack up:

git clone https://github.com/healthchecks/healthchecks.git
cd healthchecks

Step 5: Configure the Environment File

Copy the example environment file and open it for editing:

cp docker/.env.example docker/.env
nano docker/.env

At minimum, set these values:

  • ALLOWED_HOSTS — the hostname or IP address you'll use to reach the app, for example 192.168.1.50 or hc.yourdomain.com
  • SITE_ROOT — the base URL, matching whatever you put in ALLOWED_HOSTS, such as http://192.168.1.50:8000
  • SECRET_KEY — a random string used to secure sessions; generate one with openssl rand -hex 32 and paste the result in

If you want email alerts, also fill in DEFAULT_FROM_EMAIL, EMAIL_HOST, EMAIL_HOST_USER, and EMAIL_HOST_PASSWORD with your SMTP provider's details. Skip these for now if you'd rather test with a different notification channel first — you can always come back and add email later without losing any check history.

Step 6: Start the Stack

Move into the docker directory and bring the containers up in the background:

cd docker
docker compose up -d

The first run takes a few minutes, since Compose has to build the Healthchecks image and pull the PostgreSQL image before anything starts. Once it's done, check that both containers are running:

docker compose ps

You should see two containers listed, both showing as running: one for the web app, one for the database.

Step 7: Create Your Admin Account

Healthchecks doesn't come with a default login. Create one from inside the running web container:

docker compose run web /opt/healthchecks/manage.py createsuperuser

It'll ask for an email address and password interactively. That email address becomes your login for the web dashboard, so use one you'll actually check for the eventual test alert.

Step 8: Log In and Create Your First Check

Open a browser and go to the address you set as SITE_ROOT — something like http://192.168.1.50:8000. Log in with the account you just created, and click Add Check.

Give the check a name that means something later, like nightly-backup, and set a Period and Grace Time that match how often the job actually runs. A daily backup that finishes around 2:15 AM might use a 24-hour period with a 30-minute grace window, so a slightly slow run doesn't trigger a false alarm.

Once you save the check, Healthchecks gives you a unique ping URL. That URL is what your cron job will hit when it finishes.

Step 9: Wire Up a Real Cron Job

On whichever machine actually runs the job you're monitoring — this doesn't have to be the same container — add a line like this to its crontab:

0 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 5 http://192.168.1.50:8000/ping/your-uuid-here > /dev/null

The && matters here. It means the ping only fires if backup.sh actually exits successfully. If the backup script fails, the ping never fires, the check goes overdue, and you get an alert — which is the entire point.

Commands Explained

CommandWhat it does
docker compose up -dBuilds (if needed) and starts every service defined in the compose file, running them in the background instead of tying up your terminal session
docker compose psLists the containers Compose is managing for this project and their current status
docker compose logs -f webStreams the live log output of the web container, useful when something isn't behaving and you need to see what Django is actually complaining about
docker compose run web /opt/healthchecks/manage.py createsuperuserRuns a one-off command inside a fresh instance of the web container to create an admin login
curl -fsS -m 10 --retry 5 URLSends a silent, fail-on-error HTTP request with a 10-second timeout, retrying up to five times — the retry flags matter on flaky home networks, where a single dropped packet shouldn't count as a missed check-in

Common Errors

Docker daemon won't start, or fails partway through pulling images. This is almost always the nesting checkbox from Step 2. Go back, confirm it's checked under Options → Features, and reboot the container.

"Bad Request (400)" in the browser after logging in. Django refuses to serve requests for a hostname it doesn't recognize. Double-check that ALLOWED_HOSTS in docker/.env exactly matches the address you're typing into the browser, then restart the stack with docker compose restart web.

Checks stay "Late" even though the cron job ran fine. Test the ping URL manually with curl -v http://192.168.1.50:8000/ping/your-uuid-here from the machine running the cron job. If that fails, it's a network or firewall issue between that machine and the container, not a Healthchecks problem.

Email alerts never arrive. Most often this is a wrong SMTP password or a provider blocking the connection because the "from" address isn't verified. Check docker compose logs web right after triggering a test alert — SMTP errors show up there in plain text.

Troubleshooting

When something isn't working, logs are almost always faster than guessing. Start with:

docker compose logs -f web

and watch what happens the moment you trigger the behavior you're troubleshooting — logging in, sending a test ping, saving a check. Most errors in Healthchecks show up as a readable Django traceback rather than a cryptic code.

If the container itself seems unresponsive, confirm it actually has network access from inside Proxmox VE:

ping -c 3 1.1.1.1

A container with no internet access will fail silently at the docker compose up step, usually while trying to pull the PostgreSQL image, and the error message can be easy to misread as a Docker installation problem rather than a networking one.

If checks are created but never receive pings, walk the chain backward: does curl from the job's machine reach the container at all? Is the container's firewall (if you've set one up) blocking inbound port 8000? Is the cron job even running — check with grep CRON /var/log/syslog on Debian-based systems, or journalctl -u cron on some others.

Best Practices

Set grace periods with some real slack in them. A check with a one-minute grace window on a job that occasionally takes an extra ninety seconds will page you constantly for no reason, and you'll start ignoring the alerts — which defeats the purpose.

Back up the PostgreSQL data volume, not just the application. Your check history and configuration live in that database, and Compose stores it in a named volume by default. Include it in whatever backup routine you already use for the rest of your homelab.

Don't expose port 8000 straight to the internet. If you want to check in on jobs from outside your home network, put Healthchecks behind a reverse proxy with a real TLS certificate instead of forwarding the raw port.

Use separate projects inside Healthchecks for unrelated groups of checks — homelab infrastructure versus a personal side project, say. It keeps the dashboard readable once you're past a dozen checks, and it's much easier to set up early than to reorganize later.

Frequently Asked Questions

Do I need a domain name to use this?
No. A local IP address and port work fine for anything staying inside your network. A domain only matters if you want to reach it from outside or need a real TLS certificate.

Is self-hosted Healthchecks the same as the hosted healthchecks.io service?
It's the same open-source project behind both. Self-hosting removes the free tier's 20-check limit and keeps your data local, at the cost of you being responsible for uptime and backups.

Can it monitor more than cron jobs?
Yes. Anything that can send an HTTP request on a schedule qualifies — systemd timers, application health routines, even a script running on a completely different network, as long as it can reach the container.

Does this replace Uptime Kuma or Zabbix?
No, and it isn't trying to. Those tools monitor whether services are reachable. Healthchecks monitors whether scheduled jobs actually ran. Most homelabs benefit from having both.

What happens if the Healthchecks container itself goes down?
You won't get alerted about missed pings while it's offline, since nothing is watching. Some people mitigate this by also using a free hosted healthchecks.io check to monitor the self-hosted instance's own uptime.

Conclusion

None of this is complicated once it's running, and that's kind of the appeal. You spend twenty minutes setting up a container most people never think to build, and in exchange you stop finding out about broken backups the hard way — during a restore, at the worst possible moment.

Start small. Wire up the one cron job you'd actually be upset to lose, watch a real check go from green to overdue at least once so you know what the alert looks like, and add the rest of your scheduled jobs from there.