
Build a Service Health Dashboard with Cron and Static HTML
Before you run this
This guide builds a small read-only status page. A bash script checks whether a list of systemd services are active and whether a few HTTP endpoints answer, then writes a plain HTML file that your web server already serves. Cron re-runs it every few minutes so the page stays current. Nothing here restarts, reconfigures, or stops any service — it only reads state and writes one HTML file.
Privileges: The checks themselves (systemctl is-active, curl) run fine as an unprivileged user. You need elevated rights only twice: to place the output file into a web root you don't own (or, better, to chown that file's directory to your script's user once), and to install a cron job under a service account. Run the script as a dedicated low-privilege user, not root — it has no reason to be root.
Test first: Read the script before you run it. Run it by hand and inspect the HTML it produces before you wire it into cron, and do that on a test VM or a staging box first, not on the machine everyone watches. The only file it writes is the output HTML at a path you choose — point that at /tmp while you test so you can't clobber anything important.
The one destructive edge: the script overwrites its output file every run (that's the point). If you aim OUTPUT_FILE at an existing file you care about, you will lose it. Aim it at a new, dedicated filename.
What I'm assuming
- Debian 12 or Ubuntu 22.04/24.04, systemd, bash.
- A web server (nginx or Apache) already serving a directory — I'll use
/var/www/htmlas the example web root. Substitute your own. - The services you want to watch are systemd units on this same host.
curlis installed (sudo apt install curlif not).
If you're on RHEL/Alma the only differences are the web root (often /usr/share/nginx/html or /var/www/html) and that curl comes from dnf. The commands below are otherwise identical.
The idea
Keep it boring and legible: one script, two lists (systemd units, HTTP URLs), one HTML file. No agents, no database, no JavaScript. If you outgrow this you graduate to Prometheus + Grafana or Uptime Kuma — but for "are my six services up?" on one box, a cron script is the right size.
The script
Save this as /usr/local/bin/health-dashboard.sh. Edit the two lists and the paths at the top.
#!/usr/bin/env bash
# Generate a static HTML status page for a fixed list of services.
# Read-only: checks state, writes one HTML file. Restarts nothing.
set -euo pipefail
# --- CONFIG: edit these ------------------------------------------------
# systemd units to check. Use the real unit names (systemctl list-units).
SERVICES=("nginx.service" "ssh.service" "postfix.service")
# HTTP(S) endpoints to check. Any 2xx/3xx status counts as "up".
declare -A ENDPOINTS=(
["Web front page"]="https://example.com/"
["API health"]="https://example.com/health"
)
OUTPUT_FILE="/var/www/html/status.html" # point at /tmp while testing
CURL_TIMEOUT=5 # seconds per HTTP check
# -----------------------------------------------------------------------
# Write to a temp file, then move it into place atomically so a browser
# never sees a half-written page.
TMP_FILE="$(mktemp)"
trap 'rm -f "$TMP_FILE"' EXIT
now="$(date '+%Y-%m-%d %H:%M:%S %Z')"
{
echo '<!DOCTYPE html>'
echo '<html lang="en"><head><meta charset="utf-8">'
echo '<meta name="viewport" content="width=device-width, initial-scale=1">'
# Tell the browser to reload periodically so the page stays fresh.
echo '<meta http-equiv="refresh" content="60">'
echo '<title>Service status</title>'
echo '<style>body{font-family:sans-serif;margin:2rem}'
echo 'table{border-collapse:collapse}td,th{padding:.4rem .8rem;border:1px solid #ccc}'
echo '.up{color:#137333}.down{color:#c5221f;font-weight:bold}</style></head><body>'
echo "<h1>Service status</h1>"
echo "<p>Last checked: ${now}</p>"
echo '<table><tr><th>Check</th><th>Type</th><th>State</th></tr>'
} > "$TMP_FILE"
# --- systemd unit checks ---
for unit in "${SERVICES[@]}"; do
# is-active prints active/inactive/failed and returns non-zero when not active.
if state="$(systemctl is-active "$unit" 2>/dev/null)"; then
cls="up"
else
cls="down"
fi
printf '<tr><td>%s</td><td>systemd</td><td class="%s">%s</td></tr>\n' \
"$unit" "$cls" "$state" >> "$TMP_FILE"
done
# --- HTTP endpoint checks ---
for name in "${!ENDPOINTS[@]}"; do
url="${ENDPOINTS[$name]}"
# -o /dev/null discards the body; -w prints just the status code.
# --max-time bounds the wait; a failed connect yields code 000.
code="$(curl -s -o /dev/null -w '%{http_code}' \
--max-time "$CURL_TIMEOUT" "$url" || echo "000")"
if [[ "$code" =~ ^[23] ]]; then
cls="up"; label="HTTP ${code}"
else
cls="down"; label="HTTP ${code}"
fi
printf '<tr><td>%s</td><td>http</td><td class="%s">%s</td></tr>\n' \
"$name" "$cls" "$label" >> "$TMP_FILE"
done
echo '</table></body></html>' >> "$TMP_FILE"
# Atomic replace; keep the file group-readable for the web server.
mv "$TMP_FILE" "$OUTPUT_FILE"
chmod 644 "$OUTPUT_FILE"
trap - EXIT
A few notes on choices I made deliberately:
set -euo pipefailmakes the script fail loudly on a typo rather than write a broken page. If asystemctl is-activecheck "fails" that's caught by theif, so it won't abort the run.- I write to a temp file and
mvit into place.mvwithin the same filesystem is atomic, so a browser refreshing at the wrong moment never sees a truncated file. KeepTMP_FILEandOUTPUT_FILEon the same filesystem for this to hold —mktempdefaults to/tmp, so if your web root is a separate mount, setTMPDIRto somewhere on the same mount or usemktemp -pwith a directory beside the output. - HTTP status
000is curl's convention for "couldn't connect at all" — it's not an invented code, it's what%{http_code}returns on connection failure.
Make it runnable and test by hand
sudo install -m 0755 health-dashboard.sh /usr/local/bin/health-dashboard.sh
# Test to /tmp first — edit OUTPUT_FILE to /tmp/status.html for this run.
/usr/local/bin/health-dashboard.sh
xdg-open /tmp/status.html 2>/dev/null || cat /tmp/status.html
Read the output. Confirm each service shows the state you expect (stop a non-critical test service and re-run to see it flip to red). Only once you're happy, point OUTPUT_FILE at your real web root.
File ownership for the web root
Rather than run the script as root to write into /var/www/html, create a dedicated file it can write. As root, once:
sudo touch /var/www/html/status.html
sudo chown youruser:youruser /var/www/html/status.html
Now the script, running as youruser, can overwrite that one file without any elevated rights. Substitute your service account for youruser.
Schedule it with cron
Install it under the same unprivileged user. Edit that user's crontab:
crontab -e
Add a line to run every five minutes:
# Regenerate the service status page every 5 minutes
*/5 * * * * /usr/local/bin/health-dashboard.sh >> /var/log/health-dashboard.log 2>&1
Make sure the log file is writable by that user, or drop the redirect and let cron mail you failures. If you prefer systemd, the equivalent is a .service unit plus a .timer — that's the other standard way; I'm using cron here because it's one line.
Verify it worked
- The file updates on schedule. Watch the timestamp in the page, or:
stat -c '%y' /var/www/html/status.html # modification time should be recent - Cron actually fired. On Debian/Ubuntu, cron logs to the journal:
and checkjournalctl -u cron --since "10 minutes ago" | grep health-dashboard/var/log/health-dashboard.logfor any error output from the script. - The page is served. From another machine,
curl -I https://example.com/status.htmland confirm a200. - Red actually goes red. Stop a test unit, wait for the next run, refresh. Start it again afterward.
Undo
This is easy to remove cleanly because it changed so little:
crontab -e # delete the */5 line
sudo rm /var/www/html/status.html # remove the page
sudo rm /usr/local/bin/health-dashboard.sh # remove the script
sudo rm -f /var/log/health-dashboard.log # remove the log
Nothing else on the system was touched — no services were reconfigured, so there's nothing else to reverse.
For the exact meaning of systemctl is-active exit codes see the systemctl man page, and for --write-out variables like %{http_code} see the curl documentation (man curl). Both ship with the tools; I'd rather you confirm a flag there than trust my memory of it.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
