A single bash script that runs twelve daily health checks against a remote VPS over SSH, aggregates the result into one notification push, and exits 0 / 1 / 2 so cron mails on regression. Designed for self-hosted operators with one or two VPSes, where running a full observability stack (Prometheus + Alertmanager + Grafana) is overkill.
Born out of running sovgrid.org on a no-KYC VPS and wanting morning-coffee confidence that nothing rotted overnight. The reference deployment is documented in the engineering log at sovgrid.org/projects and sovgrid.org/stack.
| # | Check | Threshold |
|---|---|---|
| 1 | SSH reachability + public-URL 200 OK | binary |
| 2 | /var/run/reboot-required flag |
warn |
| 3 | Pending Debian-Security updates | warn ≥1, crit ≥5 |
| 4 | Failed systemd units | any → crit |
| 5 | UFW active | inactive → crit |
| 6 | Fail2ban currently-banned (sshd jail) | informational |
| 7 | Disk usage / and /boot/efi |
warn ≥85%, crit ≥95% |
| 8 | Memory available | warn <200 MiB |
| 9 | OOM kills in last 24h via journalctl -k |
any → crit |
| 10 | Let's Encrypt cert expiry per domain | warn <14d, crit <3d |
| 11 | Expected docker containers all Up |
missing → crit |
| 12 | Optional freshness file (e.g. nightly job output) | stale ≥26h → warn |
Everything is bundled into a single SSH session for efficiency — the script issues exactly one outbound ssh, parses the structured response, and pushes one notification on the way out. No retries, no auto-fix. Read the script, it fits on one screen with comments.
# Clone
git clone https://github.com/cipherfoxie/vps-healthcheck.git
cd vps-healthcheck
# Configure (option A: env vars)
export HC_SSH_HOST=my-vps
export HC_REACH_URL=https://my-domain.example/
export HC_CERT_DOMAINS="my-domain.example www.my-domain.example"
export HC_EXPECTED_CONTAINERS="caddy app"
# (option B: persistent config file alongside the script)
cp vps-healthcheck.conf.example vps-healthcheck.conf
$EDITOR vps-healthcheck.conf
# Test
bash vps-healthcheck.sh --dry-run
# Cron (daily 07:30 local)
crontab -e
# 30 7 * * * /usr/bin/bash /path/to/vps-healthcheck.sh --quiet >> /var/log/vps-healthcheck.log 2>&1Exit codes: 0 = clean, 1 = warnings, 2 = critical. Cron mails on non-zero by default. The --quiet flag suppresses notification pushes when there's nothing to report.
The script calls "$HC_NOTIFY" <title> <body> if HC_NOTIFY points to an executable. Use any one-shot notify helper:
- A Matrix bridge (curl POST to a homeserver)
- ntfy.sh (
curl -d "$2" -H "Title: $1" https://ntfy.sh/your-topic) - A Telegram bot
- Signal-cli
- Plain
notify-sendif you run cron on a workstation with a display
If HC_NOTIFY is unset, the script prints to stdout only. Combined with cron's "MAILTO=you@host", that's enough for a one-VPS setup.
| Tool | Best at | Why it's overkill for one VPS |
|---|---|---|
| Prometheus + Alertmanager + Grafana | Fleet observability with PromQL, multi-day trends, complex alert rules | Three services to run, learn, maintain. Itself a new fleet to monitor. |
| Netdata | Real-time per-host dashboards with subsecond resolution | Agent must run on the monitored host with broad permissions. Web UI surface adds attack area. |
| Monit / Munin | Long-history graphs + service-level restarts | Either daemonized on the host (Monit) or pull-based with a server (Munin). |
| Uptime Kuma / Healthchecks.io | Pull/push uptime monitoring, status pages | Excellent for external uptime but doesn't see disk, memory, OOM, or cert expiry from the inside. |
Where vps-healthcheck fits: you already trust your SSH access to the box, you don't want a daemon, and you want the morning glance — "is anything red, is anything yellow, otherwise leave me alone."
| Property | vps-healthcheck | Prometheus stack | Netdata | Uptime Kuma |
|---|---|---|---|---|
| Lines of code (operator-readable) | ~250 | ~50k+ | ~100k+ | ~10k+ |
| Daemons to run | 0 (cron only) | 3+ | 1 | 1 |
| Attack surface on the VPS | none added | exporter port | agent + web UI | none |
| Per-host trend data | none | yes | yes | partial |
| Cert-expiry, security-update, OOM, fail2ban built-in | yes | requires custom exporters / queries | partial | URL only |
| Single-SSH bundling | yes | n/a | n/a | n/a |
| Setup time | 5 min | hours | 30 min | 30 min |
This is not a replacement for any of the above when you actually need trends, real-time data, or multi-host fleet management. It's the right amount of monitoring for a one-or-two-VPS sovereign-stack operator who does not want to monitor the monitoring.
In the reference deployment, vps-healthcheck.sh runs on the operator's workstation, audits a remote no-KYC VPS, and pushes results to a private Matrix room via a one-line notify-matrix.sh helper. The full story (including the JSON sidecar that feeds the live /insights/ dashboard at sovgrid.org) lives at:
- sovgrid.org/projects/ — full project context
- sovgrid.org/stack/ — what the operator stack looks like end-to-end
- sovgrid.org/blog/setup-floki-vps-setup/ — the VPS bring-up walkthrough that informed the checks
On your workstation: bash, ssh, curl, optional jq (for the JSON sidecar).
On the VPS: bash, sudo-able access to apt-get, ufw, fail2ban-client, journalctl, docker (only if container checks are enabled), openssl (only if cert checks are enabled).
If you self-host on a Bitcoin-aligned no-KYC stack and you don't want a full observability tower, you should be able to copy-paste this and have a daily audit running in 5 minutes. The script is small enough to read before you trust it. Issues + PRs welcome.
MIT. Take what's useful. Engineering log of the wider operator stack is at sovgrid.org.