A self-hosted, plugin-extensible homelab stats dashboard. Single static Go binary, minimal Docker image, per-user customizable dashboards, and a public read-only share link for anything you want to show off without giving out access to the rest of your homelab.
- Built-in host stats — CPU, memory, network throughput, always on.
- Plugin system — every other widget is a plugin: some ship built in, and users can add their own configured instances at runtime (no rebuild needed) from the dashboard's "Add widget" button.
- Per-user dashboards — each authenticated user has their own saved widget layout and their own plugin instances (their own Traefik URL, their own weather location, ...).
- Public share links — every user gets a
/share/<token>URL they can turn on, exposing only the specific widgets they've explicitly marked public, read-only, no login required for viewers. - Centralized API key vault — enter a service's API key once on the Settings page, then pick it from a dropdown in any plugin instance that needs it, instead of re-typing it per instance.
- Backup & restore — each user can download their own dashboard (instances, API keys, layout, share settings) as a JSON file and restore it later, live, no restart required.
- Built-in login, or bring your own SSO — a first-run setup wizard creates the admin account; after that, anyone can sign up unless you disable it. If a forward-auth header (Authentik, Authelia, Traefik forward-auth, ...) is present on a request, it's trusted instead — so you can point this at your existing SSO and skip local accounts entirely.
| Plugin | What it shows | Needs |
|---|---|---|
| CPU / Memory / Network | Host utilization | nothing (built in) |
| Traefik | Router/service counts, request volume by status code | Traefik API reachable, optionally its /metrics endpoint enabled |
| Immich | Photo/video counts, library size, disk usage | Immich API key |
| Nextcloud | Users, files, active sessions, memory/CPU load | Admin username + app password, serverinfo app enabled |
| Weather | Current conditions for a chosen location | nothing — uses Open-Meteo, no key |
| World Clock | Live clocks for any timezones you list | nothing |
| Stock Watchlist | Quote + mini price history per symbol | Finnhub API key (free tier) |
| Crypto Tracker | Quote + 7-day sparkline per coin | nothing — uses CoinGecko, no key |
curl -O https://raw.githubusercontent.com/rangoDJ/saarthi/main/docker-compose.yml
docker compose up -dThis pulls the published image from GHCR — nothing to build locally. Then:
- Open the dashboard — you'll land on a setup wizard to create the admin account (only shown while no accounts exist).
- Log in, click Settings, add any API keys you need under API keys, then add plugin instances from the main dashboard's + Add widget button.
Two ways to authenticate, checked in this order:
- Forward-auth header — if your reverse proxy (Authentik, Authelia,
Traefik forward-auth, ...) sets an identity header on the request,
saarthi trusts it outright and skips its own login entirely. Default
header name is
X-Authentik-Username; pointDASHBOARD_AUTH_HEADERat whatever your proxy calls it (e.g. Authelia'sRemote-User). - Built-in accounts — if no header is present, saarthi falls back to
its own login (bcrypt-hashed passwords, session cookies). The first
account ever created (via the
/setupwizard) is the admin; anyone else can self-register at/signupunless you setDASHBOARD_ALLOW_SIGNUPS=false. There's no admin/user permission split yet beyond "who created the account first" — see Known limitations.
All configuration is environment variables, prefixed DASHBOARD_.
| Variable | Default | Purpose |
|---|---|---|
DASHBOARD_HOST |
0.0.0.0 |
Listen address |
DASHBOARD_PORT |
8090 |
Listen port |
DASHBOARD_TOKEN |
(empty) | Shared secret required (as X-Dashboard-Token) on mutating requests |
DASHBOARD_ALLOWED_HOSTS |
(empty) | Comma-separated allowlist of Host headers / CIDR ranges (DNS-rebinding protection) |
DASHBOARD_AUTH_HEADER |
X-Authentik-Username |
Header your reverse proxy sets with the authenticated username |
DASHBOARD_ALLOW_SIGNUPS |
true |
Set to false to disable /signup (the setup wizard for the first admin account still always works) |
DASHBOARD_ACCOUNTS_FILE |
<settings dir>/accounts.json |
Where local login accounts (bcrypt hashes only) are stored |
DASHBOARD_SETTINGS_DIR |
data |
Directory for settings storage — global.json plus one users/<name>.json per user, created/read lazily as each user is seen |
DASHBOARD_LOG_LEVEL |
INFO |
Set to DEBUG for verbose per-request and per-collector logging |
Security note: if the server is reachable on all interfaces (
DASHBOARD_HOST=0.0.0.0, the default) with neitherDASHBOARD_TOKENnorDASHBOARD_ALLOWED_HOSTSset, all mutating routes (saving settings, creating plugin instances, etc.) are automatically disabled at startup — a deliberate fail-safe rather than silently exposing them. Set one of the two before exposing this beyond localhost.
cmd/dashboard/ entrypoint — loads config, blank-imports every plugin package, starts the server
internal/plugin/ the Widget interface + registry every plugin implements/self-registers into
internal/manager/ resolves each widget's effective active state (enabled + detected/reachable) and drives the scheduler
internal/scheduler/ runs each active widget's Collect() on its own interval, holds the latest-value snapshot
internal/settings/ JSON-file-backed persistence: global toggles, per-user instances/credentials/layout
internal/localauth/ bcrypt-hashed local account store (setup wizard, signups)
internal/session/ in-memory session cookies for local accounts
internal/auth/ request-context carrier for the resolved username (see internal/api/authflow.go for how it's resolved)
internal/api/ HTTP routes + security middleware
internal/plugins/*/ one package per plugin (cpu, mem, net, traefik, weather, worldclock, immich, nextcloud, stocks, crypto)
web/ frontend — vanilla JS widget registry + GridStack.js grid, no build step
Every plugin implements plugin.Widget (ID(), Name(), Category(),
Detect()) plus whichever of these it needs:
plugin.Collector— has a live value polled on an interval (Interval(),Collect()).plugin.Configurable— needs user-supplied settings (SettingsFields(),ApplySettings()); this is what makes a plugin appear as an instantiable type on the "Add widget" flow.plugin.RouteProvider— exposes its own mutating HTTP routes.plugin.Redetectable— its availability can change while running (e.g. a socket that comes and goes), so it should be re-checked periodically.
Look at internal/plugins/weather/weather.go for the simplest complete
example (no auth, one external API call), or internal/plugins/traefik/
for one with a settings form and best-effort partial data. Register it with
plugin.RegisterFactory(typeID, name, func() plugin.Widget { return &Widget{} })
in an init(), then add a blank import in cmd/dashboard/main.go.
On the frontend, add a matching web/static/widgets/<name>.js file calling
registerWidgetType({ type, template, onMetrics, ... }), and include the
<script> tag in web/templates/index.html and share.html.
go build ./...
go vet ./...
go run ./cmd/dashboardRequires Go 1.25+. No cgo, no external tools — the built-in plugins (CPU/mem/net) and every optional plugin talk to plain HTTP APIs, so the binary is fully static and the runtime image needs nothing beyond ca-certificates.
- No historical charts / SQLite-backed metrics history yet — every widget shows current/live values only (some, like stocks and crypto, include a short embedded sparkline from their own API).
- No disk-usage plugin yet.
- Third-party plugins currently require writing Go and rebuilding the image — there's no external/sidecar plugin protocol (yet).
- No admin/user roles — every authenticated user can toggle the shared host-wide widgets (CPU/mem/net) on or off for everyone, and any account can back up/restore only its own data, but there's no privileged "manage other users" surface yet.
- Sessions for local accounts are in-memory only — a server restart logs everyone out (forward-auth-header users are unaffected, since that identity comes from the proxy on every request).
MIT