# ๐Ÿ›ก๏ธ Guardian **A self-hosted dashboard for a Docker homelab โ€” server telemetry, container health, and an app launcher in one page.** [๐ŸŒ **Live Website & Quick Install Guide**](https://vineetkishore01.github.io/Guardian/) ยท [๐Ÿ“ฆ **GHCR Container Package**](https://github.com/vineetkishore01/Guardian/pkgs/container/guardian) Guardian reads your machine directly: `/proc` and `/sys` for host metrics, the Docker socket for containers. No agent, no database, no cloud. It runs in about 40 MB of RAM and pushes updates over Server-Sent Events rather than polling.

Guardian Dashboard Overview

--- ## Contents - [Showcase](#showcase) - [What it does](#what-it-does) - [How it works](#how-it-works) - [Quick start](#quick-start) - [Environment variables](#environment-variables) - [Enabling shutdown and reboot](#enabling-shutdown-and-reboot) - [Runtime settings](#runtime-settings) - [API reference](#api-reference) - [Local development](#local-development) - [Design notes](#design-notes) - [Project layout](#project-layout) --- ## Showcase | Dashboard & Host Telemetry | App Launcher & In-Tile Widgets | |:---:|:---:| | [![Dashboard Overview](docs/screenshots/01-dashboard-overview.png)](docs/screenshots/01-dashboard-overview.png) | [![App Launcher](docs/screenshots/02-app-launcher.png)](docs/screenshots/02-app-launcher.png) | | *Real-time telemetry, Intel GPU stats, power, and storage gauges* | *App launcher with live in-tile widgets (Jellyfin, Bazarr, etc.)* | | Historical Metrics & Interactive Analytics | Host & Container Process Monitor | |:---:|:---:| | [![Historical Metrics](docs/screenshots/03-metric-telemetry.png)](docs/screenshots/03-metric-telemetry.png) | [![Process Monitor](docs/screenshots/04-process-monitor.png)](docs/screenshots/04-process-monitor.png) | | *Interactive 24-hour charts, min/avg/peak stats, and process consumers* | *Live process explorer sorting CPU, memory, and network usage* |

Endpoint Health Prober
Endpoint Health Prober
Automated HTTP health checks across container ports with latency and status codes

--- ## What it does ### Host telemetry | Signal | Read from | Notes | | --- | --- | --- | | CPU utilisation | `/proc/stat` | Total plus **per-core**, drawn as a strip so one pegged thread is visible inside a calm average | | I/O wait | `/proc/stat` | Reported separately from utilisation โ€” a disk-bound host can sit at 3% CPU while everything stalls | | Load average | `/proc/loadavg` | Shown as a percentage of thread count, because "2.35" means nothing on its own | | Memory & swap | `/proc/meminfo` | Used, available, buffers/cache, swap | | Temperature | `/sys/class/hwmon`, `/sys/class/thermal` | hwmon first โ€” that's where `coretemp` package readings live | | Network | `/proc/net/dev` | Per-interface throughput; virtual and overlay interfaces are excluded from "primary link" | | Storage | `/proc/mounts` + `statfs` | Every real filesystem with its true device and fs type | | Uptime & availability | `/proc/uptime`, `/proc/stat` | Live continuous operation down to the second, boot record, and lifetime CPU efficiency | | S.M.A.R.T. & drive health | `smartctl`, `/sys/block` | Per-drive SMART status (PASSED/FAILING), temperatures, NVMe wearout & TBW, power-on hours, and critical sector alerts | Each metric tile opens a **history page**: full-size charts with 1H / 6H / 24H / 7D / 30D ranges, current / average / peak / minimum, and a table view. ### Container monitoring Guardian calls both `/containers/json` *and* `/containers/{id}/json`, because the first one alone will happily report a crash-looping container as "running": - **Restart counts, exit codes, OOM kills** and the daemon's own error string - **Healthcheck logs** โ€” the last few probe results with their output, so "unhealthy" comes with a reason - **Live CPU and memory** over a persistent stats stream, so values are ~1 second old rather than up to 30 - **Shared network namespaces** resolved by name (a container on `network_mode: container:gluetun` says *via gluetun*) - **Container logs** with stdout and stderr separated, filtering, follow mode, and adjustable tail Anything needing attention is sorted to the front of the grid, and a summary line says either how many containers have a problem or that everything is healthy. ### Endpoint health An HTTP prober checks every container's published port on a 60-second cycle, reporting latency and distinguishing `2xx`, redirects, `401`/`403`, client errors and server errors. **Targets are derived from what is actually running** โ€” plus any port your bookmarks point at, which covers host services that aren't containers. ### App launcher Auto-discovers containers and turns them into clickable tiles. Custom icons (60+ presets or any image URL), custom launch URLs with `{host}` / `{lan}` / `{tailscale}` placeholders, categories, search (`/` to focus), pinning, and bookmarks for anything outside Docker. ### Housekeeping - **Docker reclaimable space** with one-click prune of dangling images only - **Application logs** โ€” Guardian's own structured log, filterable by level and scope, persisted across restarts - **Shutdown / reboot**, off by default and heavily gated ([details below](#enabling-shutdown-and-reboot)) > **Running off-host?** Without `/proc`, `/sys` or a Docker socket, Guardian serves clearly-labelled sample data and names the source it couldn't reach. It never presents synthesized values as real measurements. --- ## How it works ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ browser โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ React SPA ยท SSE ยท hash routes โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ /api/live (SSE, heartbeated) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Express server โ”‚ โ”‚ โ”‚ โ”‚ one sampling loop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ snapshot โ”€โ”€โ–บ every reader โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€ /proc, /sys (host telemetry) โ”‚ โ”‚ โ”œโ”€ docker.sock (containers, stats streams) โ”‚ โ”‚ โ”œโ”€ HTTP prober (endpoint health) โ”‚ โ”‚ โ””โ”€ tiered history (30 days, on disk) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` **One sampler, many readers.** The CPU and network collectors diff kernel counters, so calling them twice in quick succession makes the second read report near-zero. A single loop samples on a fixed cadence and publishes an immutable snapshot; `/api/status` and every SSE client serve that snapshot rather than re-sampling. **Tiered history.** A month of 15-second samples would be ~180,000 points per metric. Guardian keeps every sample for 6 hours, 5-minute averages for 7 days, and hourly averages for 30 days โ€” about 3,600 points total, which fits in memory, serialises to a small JSON file, and survives restarts. **Streaming container stats.** One long-lived connection per running container instead of a request per container per poll. Connection count stays flat as the poll rate changes. --- ## Quick start ### Docker Compose Grab `docker-compose.yml.example` directly or paste the compose configuration below: ```bash # Download and start in one step curl -sSL https://raw.githubusercontent.com/vineetkishore01/Guardian/main/docker-compose.yml.example -o docker-compose.yml docker compose up -d ``` Or create `docker-compose.yml` manually: ```yaml services: guardian: image: ghcr.io/vineetkishore01/guardian:latest container_name: guardian restart: unless-stopped ports: - "3001:3001" volumes: # Container discovery, stats and logs (/var/run/docker.sock for rw actions) - /var/run/docker.sock:/var/run/docker.sock # Host telemetry - /proc:/host/proc:ro - /sys:/host/sys:ro - /etc/os-release:/host/etc/os-release:ro - /etc/hostname:/host/etc/hostname:ro # Storage โ€” mount root (rslave allows submounts like /mnt or /media to resolve) - /:/host/root:ro,rslave # Persisted icons, bookmarks, history and logs - guardian_data:/data environment: - HOST_PROC=/host/proc - HOST_SYS=/host/sys - HOST_ETC=/host/etc - HOST_ROOT=/host/root - PORT=3001 - NODE_ENV=production deploy: resources: limits: memory: 128M cpus: '0.50' volumes: guardian_data: ``` Then open `http://:3001`. ### Build from source ```bash git clone https://github.com/vineetkishore01/Guardian.git cd Guardian docker compose up -d --build ``` --- ## Environment variables Everything is optional โ€” Guardian starts with no configuration and discovers what it can. ### Networking | Variable | Default | Description | | --- | --- | --- | | `PORT` | `3001` | Port the HTTP server listens on. | | `BIND_HOST` | `0.0.0.0` | Address to bind. Set to `127.0.0.1` to accept only local connections, e.g. behind a reverse proxy. | ### Host telemetry paths Inside a container the host's filesystems are bind-mounted elsewhere, so each collector's root is configurable. Values must match the container-side path you mounted to. | Variable | Default | Description | | --- | --- | --- | | `HOST_PROC` | `/proc` | procfs mount point. Source of CPU, memory, load, uptime, network and mount data. Without it, host metrics fall back to sample values. | | `HOST_SYS` | `/sys` | sysfs mount point. Source of temperatures (`hwmon` and `thermal`). | | `HOST_ETC` | `/etc` | Used to read `os-release` for the distro name. Without it Guardian reports the kernel name instead of e.g. "Debian GNU/Linux 13". | | `HOST_ROOT` | `/` | The root filesystem to measure. Set to `/host/root` when you bind-mount `/` elsewhere. | | `HOST_NAS` | `/mnt/nas` | An additional volume to measure. Despite the name it can be any path. | | `HOST_MOUNTS` | *(empty)* | Comma-separated extra mount points to watch, e.g. `/host/mnt/backup,/host/srv`. Filesystems found in `/proc/mounts` are picked up automatically; use this for anything that isn't. | ### Docker | Variable | Default | Description | | --- | --- | --- | | `DOCKER_SOCKET` | `/var/run/docker.sock` | Path to the Docker daemon socket. Read-only access is enough for everything except pruning. Without it, Guardian shows a labelled sample container list. | ### Storage and persistence | Variable | Default | Description | | --- | --- | --- | | `DATA_DIR` | `/data` | Where `guardian.json` (icons, bookmarks, settings), `history.json` (30-day metrics) and `logs.json` are written. Falls back to `./data` when the directory does not exist, which is what happens during local development. | ### Launch targets These only seed the initial values; both are editable in **Settings** afterwards and stored in `guardian.json`. | Variable | Default | Description | | --- | --- | --- | | `SERVER_IP` | *(empty)* | LAN address used to build app URLs when the launch target is set to **LAN**. Left empty, the LAN option is not offered. | | `TAILSCALE_IP` | *(empty)* | Tailscale address, used when the launch target is set to **Tailscale**. Left empty, the option is not offered. | ### Logging | Variable | Default | Description | | --- | --- | --- | | `LOG_LEVEL` | `info` | Minimum level recorded: `debug`, `info`, `warn` or `error`. Entries appear in the dashboard's log panel and on stdout, so `docker logs guardian` keeps working. | ### Power control | Variable | Default | Description | | --- | --- | --- | | `ENABLE_POWER_CONTROLS` | `false` | Set to the exact string `true` to allow shutdown and reboot. While unset the control is hidden entirely โ€” not shown disabled. | | `GUARDIAN_POWER_COMMAND` | *(auto-detect)* | Custom command to run, with `{action}` expanded to `poweroff` or `reboot`. Split on whitespace and executed as an argument array โ€” never through a shell. Auto-detection tries `systemctl`, then `shutdown`. | --- ## Enabling shutdown and reboot This is the only endpoint that can take the machine down, so it is deliberately hard to trigger by accident: 1. It does nothing unless `ENABLE_POWER_CONTROLS=true`. 2. The caller must type the machine's **hostname** to confirm. 3. Every attempt, including refusals, is written to the application log. ```yaml environment: - ENABLE_POWER_CONTROLS=true ``` ### The container caveat Guardian runs in a container, so `systemctl poweroff` acts on **the container**, not the host โ€” you'd get a button that restarts Guardian. The dialog warns you when it detects this. To reach the host, supply a command that crosses the boundary: ```yaml volumes: - /path/to/key:/keys/guardian:ro environment: - ENABLE_POWER_CONTROLS=true - GUARDIAN_POWER_COMMAND=/usr/bin/ssh -i /keys/guardian -o StrictHostKeyChecking=yes admin@host sudo systemctl {action} ``` Restrict the key in the host's `authorized_keys` so it can only run those two commands: ``` command="/usr/bin/sudo /bin/systemctl $SSH_ORIGINAL_COMMAND",no-port-forwarding,no-pty ssh-ed25519 AAAA... ``` Alternatives are `pid: host` plus `nsenter`, or mounting the systemd D-Bus socket. Both grant the container substantially more power over the host than a scoped SSH key. > โš ๏ธ **Guardian has no authentication.** Anyone who can reach the port can read everything and, with power controls on, shut the machine down. Keep it on a trusted network or behind an authenticating reverse proxy. --- ## Runtime settings Editable in the UI, stored in `guardian.json`, no restart required: | Setting | Effect | | --- | --- | | **App launch target** | Whether tile URLs are built from the browser's address, the LAN IP, the Tailscale IP, or a custom domain. | | **LAN / Tailscale address** | The addresses those options use. Also seeds the `{lan}` and `{tailscale}` placeholders. | | **Dashboard title** | Shown in the browser tab. | | **Telemetry interval** | 5โ€“300 seconds. Applied immediately โ€” the sampling loop re-arms without a restart. | | **Backup** | Exports icons, categories and bookmarks as JSON. | --- ## API reference | Method | Route | Purpose | | --- | --- | --- | | `GET` | `/api/health` | Liveness, SSE client count, open stats streams, last sample time | | `GET` | `/api/status` | Current telemetry snapshot | | `GET` | `/api/live` | SSE stream of snapshots, with heartbeats | | `GET` | `/api/history/:metric` | One metric's history โ€” `?range=1h\|6h\|24h\|7d\|30d` | | `GET` | `/api/history` | Several at once โ€” `?metrics=cpu,ram&range=24h` | | `GET` | `/api/logs` | Application log โ€” `?level=&scope=&limit=` | | `DELETE` | `/api/logs` | Clear the log buffer | | `GET` | `/api/containers/:id/logs` | Container logs โ€” `?tail=200` | | `GET` | `/api/config` | Full persisted config, for backup | | `POST` | `/api/config/settings` | Update settings | | `POST` | `/api/containers/:name/custom` | Per-container icon, URL, category, pin, hide | | `POST` | `/api/custom-apps` | Create or update a bookmark | | `DELETE` | `/api/custom-apps/:id` | Delete a bookmark | | `POST` | `/api/docker/prune` | Prune dangling images | | `POST` | `/api/probes/refresh` | Force a health-probe cycle | | `GET` | `/api/power` | Whether power control is available, and why not if it isn't | | `POST` | `/api/power/:action` | `shutdown` or `reboot`, body `{ "confirmation": "" }` | Metric keys: `cpu`, `ram`, `swap`, `temp`, `netRx`, `netTx`, `disk`. --- ## Local development ```bash # Install npm install && (cd client && npm install) && (cd server && npm install) # Client on :5173 with hot reload, server on :3001 npm run dev # Or build and serve the production bundle from the server npm run build && npm start ``` On a non-Linux machine the host and Docker collectors fall back to sample data and the dashboard says so โ€” the UI is fully developable without a Linux host. To exercise the Docker code paths without a daemon, point `DOCKER_SOCKET` at any process speaking the Engine API subset Guardian uses (`/containers/json`, `/containers/{id}/json`, `/containers/{id}/stats`, `/containers/{id}/logs`, `/system/df`). --- ## Design notes A single neutral ramp carries the interface, one brand hue marks anything interactive, and three status hues are reserved for state. **Colour always encodes meaning**: a metric turns amber or red because it crossed a threshold, never for decoration. A healthy dashboard is almost entirely monochrome, so anything coloured is worth looking at. The dark theme is **AMOLED true black** โ€” the page plane is `#000`, where an OLED panel switches the pixel off. Since shadows are invisible on black, elevation comes from brighter borders and a lit top edge instead. Chart series use a four-slot categorical palette assigned in fixed order and validated in both themes against the real surfaces for lightness band, chroma floor, colour-vision separation and contrast. Charts start the y-axis at zero, break the line across data gaps rather than interpolating across them, and offer a table view so nothing depends on colour alone. --- ## Project layout ``` Guardian/ โ”œโ”€โ”€ Dockerfile Multi-stage production image โ”œโ”€โ”€ docker-compose.yml Local development deployment โ”œโ”€โ”€ docker-compose.yml.example Standalone production template (GHCR image) โ”œโ”€โ”€ server/ Node ยท TypeScript ยท Express โ”‚ โ””โ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ collectors/ โ”‚ โ”‚ โ”œโ”€โ”€ host.ts /proc & /sys โ€” CPU, memory, temps, network โ”‚ โ”‚ โ”œโ”€โ”€ disk.ts Filesystem discovery via /proc/mounts โ”‚ โ”‚ โ””โ”€โ”€ docker.ts Socket client, stats streams, log demux โ”‚ โ”œโ”€โ”€ prober.ts HTTP health checks on discovered ports โ”‚ โ”œโ”€โ”€ history.ts Tiered 30-day metric store โ”‚ โ”œโ”€โ”€ logger.ts Structured application log โ”‚ โ”œโ”€โ”€ power.ts Guarded shutdown / reboot โ”‚ โ”œโ”€โ”€ store.ts Persistence + payload validation โ”‚ โ””โ”€โ”€ index.ts Sampling loop, SSE, API routes โ””โ”€โ”€ client/ React 18 ยท Vite ยท Tailwind โ””โ”€โ”€ src/ โ”œโ”€โ”€ components/ โ”‚ โ”œโ”€โ”€ ui/ Card, Button, Dialog, Input, Badge, Progress, Tabs โ”‚ โ”œโ”€โ”€ layout/ Header, PowerMenu, SettingsModal, PruneBanner โ”‚ โ”œโ”€โ”€ metrics/ MetricCard, CoreStrip, HostStatsBar, StorageGauges โ”‚ โ”œโ”€โ”€ apps/ AppGrid, AppCard, Add/Edit modals โ”‚ โ”œโ”€โ”€ logs/ LogsPanel, ContainerLogsModal โ”‚ โ”œโ”€โ”€ services/ ServicesTable โ”‚ โ””โ”€โ”€ charts/ LiveSparkline, TimeSeriesChart โ”œโ”€โ”€ pages/ MetricDetailPage โ”œโ”€โ”€ hooks/ useLiveTelemetry โ””โ”€โ”€ lib/ formatters, severity, router, metrics, icons ``` --- ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.