Minz ef6b313d47 Add a stats agent container: FastAPI + psutil behind an API key
Exposes host CPU, memory, disk, and network usage over HTTP for the
dashboard's System Stats block (or anything else that can send a header)
to poll. Auth is a required X-API-Key, auto-generated and persisted on
first run if not supplied, checked with a constant-time comparison and
rate-limited after repeated failures. No CORS, so a browser can't call it
directly from another origin -- only a server-side caller can, keeping
the key out of anyone's network tab.
2026-07-29 20:20:48 +02:00

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] },
  "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
Description
No description provided
Readme 38 KiB
Languages
Python 94.9%
Dockerfile 5.1%