Files
dash-vps-agent/README.md
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

114 lines
4.2 KiB
Markdown

# 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](https://git.minzkraut.com/Minz/vibe-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
```bash
docker compose up --build -d
```
That mounts the host's `/proc`, `/sys`, and `/` read-only (see [Host
visibility](#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`:
```bash
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:
```json
{
"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 }
]
}
}
```
```bash
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` |