Derived from the same percpu psutil.cpu_percent() call rather than a second blocking call, so the aggregate and the per-core breakdown describe the same sampling window.
4.2 KiB
vps-dash-agent
A tiny stats agent you run on any host to expose its CPU, memory, disk, and network usage over HTTP — built to feed the System Stats block in dashboard, but plain enough to poll from anything that can send an HTTP header.
- Stack: Python, FastAPI,
psutil. No database, no state beyond an optional generated API key. - Auth: every data-bearing endpoint requires an API key, checked with a constant-time comparison and rate-limited after repeated failures. Nothing about the host is exposed to an unauthenticated caller.
Quick start
docker compose up --build -d
That mounts the host's /proc, /sys, and / read-only (see Host
visibility below) — needed for the numbers to reflect the
actual host rather than the container's own near-empty cgroup. No API_KEY
means one is generated on first start; check the logs or data/api_key.txt
on the agent-data volume for it.
Equivalent plain docker run:
docker run -d \
--name vps-dash-agent \
--restart unless-stopped \
-p 9090:9090 \
-e API_KEY=<a long random string> \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
-v /:/host/root:ro \
-v vps-dash-agent-data:/data \
vps-dash-agent
Host visibility
Every one of these is optional and auto-detected — nothing crashes if you skip a mount, it just falls back to the container's own (much less useful) view instead of the host's:
| Mount | Why |
|---|---|
/proc:/host/proc:ro |
CPU, memory, swap, load average, uptime, disk/network stats |
/sys:/host/sys:ro |
reserved for future use; harmless to include now |
/:/host/root:ro |
resolves each disk partition's real usage, and the host's /etc/hostname |
If /host/proc isn't mounted at all, /api/stats still returns data, just
for the container's own near-empty cgroup instead of the host —
network.hostNamespace in the response tells you which one you're getting.
API
Every endpoint except /api/health requires an X-API-Key header.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
Liveness check, no auth — used by the Docker HEALTHCHECK |
| GET | /api/stats |
A snapshot of CPU/memory/disk/network for the host |
GET /api/stats response shape:
{
"hostname": "myserver",
"timestamp": "2026-07-29T20:15:00+00:00",
"uptimeSeconds": 123456,
"cpu": { "percent": 12.3, "cores": 4, "model": "Intel(R) Xeon(R) ...", "loadAverage": [0.12, 0.34, 0.20], "perCore": [9.1, 15.4, 8.0, 16.7] },
"memory": { "totalBytes": 0, "usedBytes": 0, "availableBytes": 0, "percent": 0, "swapTotalBytes": 0, "swapUsedBytes": 0, "swapPercent": 0 },
"disks": [
{ "mountpoint": "/", "device": "/dev/sda1", "fstype": "ext4", "totalBytes": 0, "usedBytes": 0, "freeBytes": 0, "percent": 0 }
],
"network": {
"hostNamespace": true,
"interfaces": [
{ "name": "eth0", "bytesSent": 0, "bytesRecv": 0, "packetsSent": 0, "packetsRecv": 0 }
]
}
}
curl -H "X-API-Key: $API_KEY" http://localhost:9090/api/stats
Security notes
- The API key is the only thing standing between this endpoint and anyone who can reach the port — don't publish the port to the open internet without also putting it behind a VPN (Tailscale, WireGuard) or a reverse proxy that terminates TLS. This agent speaks plain HTTP by design; it's meant to sit on a private network or tunnel, not be internet-facing on its own.
- No CORS headers are sent, so a browser can't call this directly from another origin — only a server-side caller (like the dashboard's own backend) can. That's intentional: it forces the API key to stay server-side rather than ending up in a browser's network tab.
- Repeated failed API key attempts from the same source IP get rate-limited (429) rather than allowed to brute-force indefinitely.
- Runs as a non-root user; all host mounts are read-only.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
API_KEY |
auto-generated on first run | The key clients must send as X-API-Key |
AGENT_DATA_DIR |
/data |
Where the auto-generated key is persisted |
AGENT_HOSTNAME |
host's /etc/hostname, or the container's |
Overrides the reported hostname |