Joghd is a small, self-hosted uptime monitor and website monitoring tool written in Go. It runs HTTP/HTTPS health checks against a list of endpoints, validates the returned status code, retries with exponential backoff, and sends downtime alerts and recovery notifications to Telegram and Mattermost.
It is a single static binary with no database, no web UI, and no agent to
install β you give it a TOML file listing URLs and where to shout, and it
either checks once and exits (oneshot, for cron and CI) or keeps
watching (continuous, for a systemd unit, Docker container, or
Kubernetes Deployment).
Joghd (Ψ¬ΨΊΨ―) is Persian for owl β a bird that stays awake all night watching things. Hence the ASCII owl in the banner.
- Why Joghd
- Features
- Installation
- Usage
- Configuration
- Environment variables
- Running in production
- How Joghd compares
- FAQ
Use Joghd when you want endpoint monitoring and status code checks without standing up a monitoring stack:
- Cron-friendly health checks.
oneshotmode exits1if anything is down, so it drops straight into a cron job, a CI pipeline, a KubernetesJob, or a deployment smoke test. - Alerting where your team already is. Telegram bot messages and Mattermost incoming webhooks, no PagerDuty contract required.
- Multi-tenant routing. One process can watch many customers' URLs and route each company's alerts to a different chat.
- No moving parts. No Postgres, no Redis, no Prometheus, no scrape config. One binary, one TOML file.
- Two modes:
oneshot(check once and exit) orcontinuous(persistent monitoring daemon) - Retry with exponential backoff: configurable attempts, initial wait, multiplier and cap, so a single blip is not paged out
- Failure, reminder and recovery alerts: you are told when a target goes down, periodically while it stays down, and when it comes back
- Multiple alert channels: Telegram and Mattermost, with per-company routing and a catch-all
- Per-target configuration: method, expected status code, custom headers, timeout and check interval
- TCP connect checks:
tcp://host:porttargets watch databases, SSH and anything else that is not HTTP - Proxy support: route checks and alerts through an HTTP, HTTPS or SOCKS5 proxy, globally or per target and per alerter, or force a target to go
direct - Certificate expiry warnings: an HTTPS check fails while its TLS certificate is within a configurable window of expiring, before it breaks the site
- Jittered scheduling: targets sharing an interval do not all probe on the same instant
- Prometheus metrics: an optional
/metricsendpoint with each target's up/down state, latency, status code and certificate expiry, plus alert delivery counters, for Grafana dashboards and Alertmanager rules - Correlation IDs: every alert carries a UUIDv7 that appears in the logs and in the message, so one incident is traceable end to end
- Extensible: implement the
Alerterinterface to add a channel - Flexible config: TOML file plus environment variable overrides via koanf
- Ships everywhere: static binary,
deb/rpm/apkpackages, and a distroless multi-arch container image onghcr.io
go install github.com/rahacloud/joghd/cmd/joghd@latestdocker pull ghcr.io/rahacloud/joghd:latest
docker run --rm -v "$PWD/config.toml:/etc/joghd/config.toml:ro" ghcr.io/rahacloud/joghd:latestPrebuilt deb, rpm, apk packages and linux/darwin/windows binaries
for amd64 and arm64 are attached to every
release.
git clone https://github.com/rahacloud/joghd.git
cd joghd
go build ./cmd/joghdBuilding from source requires Go 1.27 or newer.
# Oneshot mode (check once, exit 0 if healthy, 1 if any failures)
./joghd -config config.toml -mode oneshot
# Continuous mode (persistent monitoring)
./joghd -config config.toml -mode continuous| Flag | Default | Description |
|---|---|---|
-config |
config.toml |
Path to the configuration file |
-mode |
from config | oneshot or continuous, overrides app.mode |
-quiet |
false |
Suppress the startup banner (useful under cron) |
-version |
false |
Print version information and exit |
Configuration is validated at startup: an invalid mode, a non-positive concurrency, retry or interval value, or an enabled alerter missing its credentials all fail fast with an explanatory error rather than starting in a broken state.
Target state is kept in memory only, so a restart re-sends a failure alert for any target that is still down.
Create a config.toml file (see configs/config.example.toml):
[app]
mode = "continuous"
log_level = "info"
concurrency = 10
[http]
timeout = "10s"
user_agent = "Joghd/1.0"
# Default proxy for every target and alerter (optional, see "Proxies").
# proxy = "socks5h://127.0.0.1:1080"
# Fail an HTTPS check two weeks before its certificate expires (optional).
cert_expiry_warning = "336h"
[retry]
max_attempts = 3
initial_wait = "1s"
max_wait = "10s"
multiplier = 2.0
# Serve Prometheus metrics on this address (optional, see "Metrics").
[metrics]
listen = "127.0.0.1:9469"
# One or more named alerter instances. The table key is the instance
# name (used in logs). `companies` is an optional allow-list β empty or
# missing means "catch-all" and receives every alert.
[alerters.rahacloud]
type = "telegram"
enabled = true
# Secrets can be overridden via environment variables:
# JOGHD_ALERTERS__RAHACLOUD__BOT_TOKEN
# JOGHD_ALERTERS__RAHACLOUD__CHAT_ID
[alerters.acme_corp]
type = "telegram"
enabled = true
companies = ["Acme Corp"] # only receives alerts for matching targets
[alerters.team_mattermost]
type = "mattermost"
enabled = true
webhook_url = "https://mattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx"
# Optional: channel, username, icon_url to override webhook defaults
# Optional: its own route, instead of inheriting http.proxy
proxy = "direct"
[[targets]]
name = "Production API"
url = "https://api.example.com/health"
expected_status = 200
method = "GET"
interval = "30s"
[[targets]]
name = "Staging API"
url = "https://staging.example.com/health"
expected_status = 200
method = "GET"
interval = "1m"
[targets.headers]
Authorization = "Bearer token"
[[targets]]
name = "Primary database"
url = "tcp://db.example.com:5432"
interval = "1m"
[[targets]]
name = "Intranet portal"
url = "https://intranet.example.com/"
proxy = "http://proxy.internal:3128"
# Self-signed and renewed by hand: no expiry warning for this one.
cert_expiry_warning = "-1s"http.proxy is the default route for every target and every alerter. It takes a proxy URL with an http, https, socks5 or socks5h scheme, the literal direct for no proxy at all, or nothing, in which case HTTP traffic follows the standard HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables. A target or an alerter can set its own proxy with the same values; one that sets none inherits http.proxy, so a single setting covers both the checks and the alerts. direct also ignores the environment variables. Credentials in a proxy URL are accepted and redacted in the logs.
A target whose url is tcp://host:port is a TCP connect check: it passes when a connection to host:port is established within the target's timeout, and it is retried with the same backoff as an HTTP check. expected_status, method and headers do not apply to it, and its alerts say "TCP connect to host:port" instead of comparing status codes. A TCP check goes direct or through a socks5:// / socks5h:// proxy, which then also resolves the host name. An HTTP proxy cannot carry a raw TCP connection, so Joghd refuses to start when a tcp:// target would use one, including one inherited from http.proxy; give that target proxy = "direct" or a SOCKS5 proxy. TCP checks do not read the proxy environment variables.
http.cert_expiry_warning is a duration such as "336h" (two weeks). While an HTTPS target's certificate expires within that window, its check fails with certificate for <host> expires in N days (YYYY-MM-DD), which goes out through the usual failure, reminder and recovery alerts. The default 0 turns the check off. A target can set its own cert_expiry_warning: 0 or leaving it out inherits the global window, and a negative value such as "-1s" turns the check off for that target only. The certificate is the one from the check's own verified TLS handshake, and an expiring certificate is not retried, since retrying cannot renew it.
Set metrics.listen to a host:port, such as "127.0.0.1:9469", and Joghd serves Prometheus metrics at /metrics on it. It is off by default, Joghd refuses to start if the address is taken, and the endpoint has no authentication, so bind it to localhost or a private address. Metrics only mean something in continuous mode.
Every per-target series carries the labels target, company and scheme (http, https or tcp), and reflects the target's latest check:
| Metric | Meaning |
|---|---|
joghd_target_up |
1 if the latest check passed, 0 if it failed |
joghd_target_latency_seconds |
latency of the check's final attempt |
joghd_target_status_code |
HTTP status, 0 for a TCP check or no response |
joghd_target_attempts |
attempts the check took, retries included |
joghd_target_last_check_timestamp_seconds |
when the latest check ran |
joghd_target_cert_expiry_timestamp_seconds |
when the HTTPS certificate expires; kept through a failed handshake |
joghd_checks_total{result} |
checks run, result is success or failure |
joghd_alerts_sent_total{alerter,kind} |
alerts delivered, kind is failure, reminder or recovery |
joghd_alert_failures_total{alerter,kind} |
alerts an alerter failed to deliver |
joghd_build_info{version,commit} |
always 1 |
The counters exist at zero from startup, so rules such as increase(joghd_alert_failures_total[1h]) > 0 work before anything has happened. The Go runtime and process metrics (go_*, process_*) are served too. A useful pair of alert rules: joghd_target_up == 0 for a target that is down, and up{job="joghd"} == 0 or absent(joghd_build_info) for Joghd itself, since a stopped monitor sends no alerts of its own.
Environment variables override config file values (prefix: JOGHD_):
| Variable | Description |
|---|---|
JOGHD_APP__MODE |
Run mode (oneshot or continuous) |
JOGHD_APP__LOG_LEVEL |
Log level (debug, info, warn, error) |
JOGHD_HTTP__TIMEOUT |
Default HTTP timeout |
JOGHD_HTTP__PROXY |
Default proxy for checks and alerts (URL or direct) |
JOGHD_HTTP__CERT_EXPIRY_WARNING |
Certificate expiry window, e.g. 336h (0 turns it off) |
JOGHD_ALERTERS__<NAME>__BOT_TOKEN |
Telegram bot token for instance <name> (double underscores) |
JOGHD_ALERTERS__<NAME>__CHAT_ID |
Telegram chat ID for instance <name> |
JOGHD_ALERTERS__<NAME>__WEBHOOK_URL |
Mattermost incoming webhook URL for instance <name> |
Env variables use __ (double underscore) to separate structural levels,
matching the koanf provider β e.g. JOGHD_ALERTERS__RAHACLOUD__BOT_TOKEN
overrides alerters.rahacloud.bot_token.
*/5 * * * * /usr/bin/joghd -config /etc/joghd/config.toml -mode oneshot -quietA non-zero exit status means at least one target failed, so cron's own mail β or whatever wraps the job β sees the failure too.
The deb/rpm/apk packages install the binary to /usr/bin/joghd and an
example configuration to /etc/joghd/config.example.toml. Copy it to
/etc/joghd/config.toml and run the binary in continuous mode from a unit
file.
Run the container image as a Deployment for continuous mode, or as a
CronJob for oneshot mode. Mount the configuration from a ConfigMap at
/etc/joghd/config.toml β the image's default arguments already point there β
and inject bot tokens and webhook URLs from a Secret using the JOGHD_
environment variables above.
Joghd deliberately does less than a full monitoring platform. If you want a web dashboard, historical graphs, or a public status page, reach for something else:
| You want | Consider |
|---|---|
| A web UI, history, and a status page | Uptime Kuma, Gatus, Statping |
| Metrics scraped into Prometheus/Grafana | Prometheus Blackbox Exporter |
| Dead-man's-switch monitoring for cron jobs | healthchecks.io, Cronitor |
| A hosted service with SMS/phone escalation | Pingdom, Better Stack, UptimeRobot |
| One binary, a TOML file, and chat alerts | Joghd |
No. State lives in memory. A restart re-sends a failure alert for any target still down, which is the trade-off for having nothing to operate.
Add a [targets.headers] table to the target and set whatever header the
endpoint expects, such as Authorization = "Bearer β¦".
Yes β set expected_status per target. Any code from 100 to 599 is accepted,
so a redirect or a deliberate 401 can be the healthy answer.
Yes. Define several alerter instances and give each a companies allow-list.
A target's company field decides which chats hear about it; an alerter with
no allow-list receives everything.
Once every app.reminder_multiplier Γ the target's interval. Set
reminder_multiplier = 0 to alert only on the transitions into failure and
back to healthy.
TCP ports, yes: a tcp://host:port target is a TCP connect check (see TCP checks). ICMP ping and DNS checks are not supported.
Yes. Set metrics.listen and scrape /metrics (see Metrics).
Yes. Set http.cert_expiry_warning, for example to "336h", and an HTTPS check fails while its certificate is within two weeks of expiring (see Certificate expiry warnings).
Yes. http.proxy routes every check and alert through an HTTP, HTTPS or SOCKS5 proxy, and each target and alerter can override it, including with direct (see Proxies).
Joghd (Ψ¬ΨΊΨ―) is Persian for owl.
just build # build the binary
just test # run the test suite with coverage
just lint # run golangci-lint
just update # update dependenciesThe test suite runs on a fake clock via testing/synctest, so the retry,
timeout and scheduling tests complete in microseconds and are not timing
dependent.
GPL-3.0. See LICENSE.
Keywords: uptime monitoring, uptime monitor, website monitoring, HTTP health check, health check service, endpoint monitoring, URL monitoring, status code checker, downtime alerts, availability monitoring, site monitoring, self-hosted monitoring, Telegram alerts, Telegram bot notifications, Mattermost webhook alerts, Go monitoring tool, Golang uptime checker, CLI uptime monitor, cron health check, Kubernetes health check, Docker uptime monitor, synthetic monitoring, API monitoring, TCP port monitoring, SSL certificate expiry monitoring, TLS certificate expiry alerts, SOCKS5 proxy monitoring.