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.
114 lines
4.2 KiB
Markdown
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], "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 }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
```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` |
|