3.2 KiB
3.2 KiB
Service Health Check Cron
Pattern
A no_agent cron that checks all critical services every 5 minutes. Silent on success. Reports only when something is down. Implemented at ~/.hermes/scripts/service-health-check.sh.
What it checks
| # | Check | Method |
|---|---|---|
| 1 | Caddy (sign.itpropartner.com) | systemctl is-active |
| 2 | MySQL tunnel (wphost02) | systemctl is-active |
| 3 | SearXNG container | docker ps --format |
| 4 | DocuSeal container | docker ps --format |
| 5 | Portal mockup (port 8081) | curl -sf |
| 6 | DocuSeal web (port 3000) | curl -sf |
| 7 | SearXNG search (port 8888) | curl -sf with search query |
| 8 | WireGuard (home router) | ping 10.77.0.2 |
| 9 | MySQL through tunnel | mysql --skip-ssl -e "SELECT 1" |
| 10 | wphost02 SSH | ssh -o ConnectTimeout=5 hostname |
| 11 | Hermes gateway process | `ps aux |
| 12 | Cloudflare API token | curl -X GET /tokens/verify |
Cron setup
hermes cron create \
--name "service-health-check" \
--schedule "every 5m" \
--no_agent \
--script service-health-check.sh
Script design
exit 0when all services healthy — no output, no deliveryexit 1when any service fails — prints a bullet list of failed checks- Failed services array tracks which checks failed
- Each check is independent — one failure doesn't abort the script
- Check count shown in alert message
Pitfalls
- Sensitive info in error output: The MySQL connection string appears in the script. If a service fails, the error output may include the full password. The cron delivery is Telegram (visible only to user), not email.
- False negatives on restart: During a container restart, the health check may fire before the service comes back up. The 5-minute interval means this resolves within one cycle.
- Token expiry: The Cloudflare API token check is a direct API call. If the token expires, ALL Cloudflare checks fail. The script does not differentiate between expired token and Cloudflare outage.
- MySQL credential in plaintext: The password is in the script file and .env. If the cron output is ever delivered to the wrong channel, credentials leak. Consider moving MySQL check to a separate script that sources credentials from .env with tighter permissions.
⚠️ Fixed Bug: Bearer token sent as literal asterisks
Root cause discovered Jul 7, 2026: Line 38 of the script reads the token from .env into a variable, but line 39 sends Authorization: Bearer *** (literal asterisks) instead of Authorization: Bearer $TOKEN. This caused the health check to report the Cloudflare API token as "unhealthy" every 5 minutes, even though the actual token was valid.
Before (broken):
TOKEN=$(grep CLOUDFLARE_API_TOKEN ~/.hermes/.env | cut -d= -f2-)
curl -sf -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer ***" | ...
After (fixed):
CLOUD_TOKEN=$(grep CLOUDFLARE_API_TOKEN ~/.hermes/.env | cut -d= -f2-)
curl -sf -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer $CLOUD_TOKEN" | ...
Variable name was changed from $TOKEN to $CLOUD_TOKEN to avoid any confusion with other use of TOKEN in the script or environment.