An ultra-lightweight, single-binary server monitor with an embedded web dashboard β everything you need to glance at a box's health, in a binary smaller than a favicon.
- Single static binary under 1 MB, ~11 MB RSS, ~0.1% CPU β no runtime, no dependencies, nothing to install
- Embedded web dashboard (vanilla HTML/CSS/JS, htop-style; dark or light theme) served by the binary itself
- Metrics: CPU (model, total + per-core), memory/swap, load average, disk usage + I/O rates, network throughput, temperatures + fans, TCP connections + listening ports, full sortable process list with states, Docker/Podman containers
- History: ring buffer (1h default) served at
/api/historyβ charts survive page reloads and process restarts (saved to a small state file once a minute) - Alerts: thresholds on cpu/mem/swap/disk (percent) and temperature (Β°C), scopable to one mount or sensor (
--alert 'disk:/data>85'), with hysteresis; events log to stderr and optionally POST to a webhook - Prometheus:
/metricsendpoint in text exposition format β drop-in Grafana/Prometheus integration - Container stats via the engine's unix socket with a hand-rolled client β Docker and Podman sockets are probed automatically (
--container-socketoverrides; rootless Podman needssystemctl --user enable --now podman.socket), gracefully hidden when no engine is present
| Platform | Binary | Notes |
|---|---|---|
| Linux x86_64 / arm64 | β static (musl β works on any distro, glibc or not) | Full feature set |
| macOS arm64 / x86_64 | β | No disk I/O, TCP connections, or fan panels (they read Linux /proc//hwmon); temperatures work |
| Windows | β | Not supported β relies on unix sockets and /proc |
| FreeBSD & others | untested | May build via cargo install; Linux-only panels stay hidden |
Prebuilt static binaries come from Releases β no runtime, no package manager.
Linux (x86_64 or arm64):
curl -fsSL "https://github.com/atrastudhi/watchlite/releases/latest/download/watchlite-$(uname -m)-unknown-linux-musl" \
-o /usr/local/bin/watchlite && chmod +x /usr/local/bin/watchlitemacOS (Apple Silicon or Intel):
curl -fsSL "https://github.com/atrastudhi/watchlite/releases/latest/download/watchlite-$(uname -m | sed 's/arm64/aarch64/')-apple-darwin" \
-o /usr/local/bin/watchlite && chmod +x /usr/local/bin/watchliteAny OS with a Rust toolchain (1.95+):
cargo install watchliteDocker (multi-arch, <1 MB image):
docker run -d --name watchlite --pid=host --net=host --uts=host \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
ghcr.io/atrastudhi/watchlite:latestThe host namespaces are what let it report the host's processes, interfaces, connections, and hostname instead of the container's (the same flags Glances and netdata require); drop them (and add -p 8077:8077) if you only want a demo. With --net=host it binds 0.0.0.0:8077 by default β add --auth user:pass or bind to localhost behind a proxy.
The native binary needs none of this β it's static, smaller than the image, and sees everything by default. Prefer it unless your infra is containers-only.
The panel hides itself when no engine socket is reachable. With the native binary, the usual causes:
- Not in the
dockergroup (socket isroot:docker):Group changes need a fresh login β and if watchlite runs as a systemd user service, a plain re-login isn't enough because the lingering user manager caches the old groups:sudo usermod -aG docker $USERsudo loginctl terminate-user $USER(or reboot), then restart the service. System-wide units instead just needSupplementaryGroups=docker(see the unit below). - Rootless Podman socket not enabled (it's off by default):
systemctl --user enable --now podman.socket - Non-standard socket path β point at it directly with
--container-socket /path/to/sock.
To see what the collector sees: watchlite's stderr logs docker collector: connected/unavailable on state changes and a one-line HTTP error if the engine rejects requests; curl --unix-socket /var/run/docker.sock http://localhost/containers/json reproduces its exact call.
watchlite # serves http://127.0.0.1:8077
watchlite --bind 0.0.0.0:8077 --auth admin:secret # remote access with basic auth| Flag | Default | Description |
|---|---|---|
--bind <ADDR> |
127.0.0.1:8077 |
Listen address (0.0.0.0:... for remote access) |
--interval <SECS> |
2 |
Sampling interval (0.5β3600) |
--top <N> |
0 (all) |
Cap the process list sent to the UI (0β10000) |
--no-docker |
Disable the container collector | |
--container-socket <P> |
auto | Engine socket; probes Docker then Podman (rootful, rootless) paths |
--auth <USER:PASS> |
Require HTTP Basic auth | |
--history <SECS> |
3600 |
Sample history kept in RAM (60β86400) |
--history-file <P> |
state dir | Persist chart history across restarts (none disables); defaults to systemd's $STATE_DIRECTORY or ~/.local/state/watchlite/ |
--alert <SPEC> |
Alert rule, repeatable: metric[:target]>N. Metrics: cpu, mem, swap, disk (percent) and temp (Β°C). :target scopes disk to a mount or temp to a sensor-label substring, e.g. disk:/data>85, temp>80. Quote in shells |
|
--webhook <URL> |
POST alert events as JSON via curl; Discord webhook URLs are auto-detected and get a Discord-formatted message |
|
--once |
Print one JSON snapshot to stdout and exit β for scripts: watchlite --once | jq .cpu.total_pct |
|
--check-update |
Check GitHub releases for a newer version and exit (exit 2 if one exists; never runs automatically) |
Env-var equivalents: WATCHLITE_BIND, WATCHLITE_INTERVAL, WATCHLITE_TOP, WATCHLITE_AUTH, WATCHLITE_HISTORY, WATCHLITE_HISTORY_FILE, WATCHLITE_WEBHOOK, WATCHLITE_CONTAINER_SOCKET (flags win).
| Endpoint | Returns |
|---|---|
GET /api/stats |
Latest snapshot as JSON (one sample per interval; rates are bytes/sec from counter deltas) |
GET /api/history |
Ring buffer of compact points: {ts, cpu, mem, rx, tx} |
GET /metrics |
Prometheus text exposition format (watchlite_* gauges) |
GET /healthz |
Liveness probe: 200 ok (never requires auth) |
/api/stats, /api/history and /metrics are behind --auth when set; /healthz never is, so it works as a systemd WatchdogSec/Kubernetes liveness probe without leaking credentials. Scrape Prometheus straight off it:
scrape_configs:
- job_name: watchlite
static_configs: [{ targets: ["server:8077"] }]disk_io and connections are null on non-Linux hosts; docker is null when the Docker socket is unavailable; sensors is null when the host exposes none (typical for VMs). Fan speeds are Linux-only (/sys/class/hwmon).
Alerts fire after the threshold is exceeded for 3 consecutive samples and resolve the same way (no flapping). Webhook payload: {"host", "metric", "value", "threshold", "unit", "state": "firing"|"resolved"} (metric includes the :target for scoped rules). On SIGTERM/SIGINT (e.g. systemctl stop) watchlite flushes chart history before exiting so the dashboard's graphs survive restarts.
cargo build --release # nativeFully static Linux binary (deploy by copying one file):
docker run --rm -v "$PWD":/app -w /app rust:alpine \
sh -c "apk add musl-dev && cargo build --release --target x86_64-unknown-linux-musl"[Unit]
Description=watchlite
After=network.target
[Service]
ExecStart=/usr/local/bin/watchlite --bind 0.0.0.0:8077 --auth admin:CHANGE_ME
Restart=always
DynamicUser=yes
# persists chart history across restarts (/var/lib/watchlite)
StateDirectory=watchlite
# Docker panel needs socket access; remove if unused:
SupplementaryGroups=docker
[Install]
WantedBy=multi-user.target