# 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= \ -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` |