Self-hosted dashboard for homelab services — configure everything in the browser, no YAML. Tiles with health checks, live API data on hover, and a layout you can shape to your setup.
Deutsche Version: README_de.md
Your services as tiles in category columns with a glass-style UI. Each tile can show a health status via a separate check URL.
Theme presets (Stealth, Ember, Neon, Cobalt) or custom colors, dark/light mode, background image and color, logo upload — adjustable tile, window, and page width.
Hover a service tile for real-time API data: Plex, Proxmox, Pi-hole, qBittorrent, Home Assistant, FRITZ!Box, and more. Credentials are stored AES-256-GCM encrypted in SQLite.
Hold Shift and click field labels to show or hide individual widget rows — per service, stored in the database (logged-in admins only). Now Playing / Watching Now appear only during active playback.
Includes Jellyfin, Plex, Nextcloud, Immich, *arr stack, Docker/Portainer, NPM, Proxmox, QNAP, Navidrome, Kavita, n8n, Grafana, Uptime Kuma, Vaultwarden, Gitea, Syncthing, Authentik, FreshRSS, BookStack, Frigate, Autobrr, Romm, Trilium, and others — see the widget table below.
- Multiple dashboard pages with tabs and keyboard shortcuts
1–9 - Drag & drop categories and tiles (hold Shift on the dashboard when logged in)
- Up to three services per row — in admin or on the dashboard
- Web / LAN toggle per service: external URL vs. local IP/hostname in the header (can be disabled)
Available on the dashboard and in admin (not while typing in a field):
| Key | Action |
|---|---|
D |
Open dashboard |
S / E |
Open settings (admin) |
1–9 |
Switch page (first nine) |
. |
Toggle light / dark mode |
W |
Toggle Web / LAN mode (when LAN is enabled) |
L |
Switch language (EN / DE) |
T |
Hide services without LAN URL (when LAN is enabled) |
Shift |
Layout mode — drag & drop categories, tiles, and links (logged-in admins) |
Light/dark, Web/LAN, and hide-without-LAN preferences are stored in the browser (localStorage). The full list is also shown under Admin → Settings.
Backup & restore as ZIP (database + uploads + encrypted widget credentials). Optional dashboard sign-in so only authenticated users see the board.
Found a bug or missing API field for your setup? Please open an issue — I only run about half of the widgets myself.
Node.js 20+ & npm
or
Docker
cp .env.example .env
# Set ADMIN_PASSWORD and SESSION_SECRET in .env
npm install
npm run db:migrate
npm run dev| URL | Description |
|---|---|
| http://localhost:3000 | Dashboard |
| http://localhost:3000/admin | Admin (login with ADMIN_PASSWORD) |
No local build required — pull from GitHub Container Registry.
mkdir homelab-dashboard && cd homelab-dashboardCreate docker-compose.yml:
services:
dashboard:
container_name: homelab-dashboard
image: ghcr.io/maxdeavy/dashboard:latest
restart: unless-stopped
ports:
- "${PORT:-3333}:${PORT:-3333}"
env_file:
- .env
environment:
PORT: ${PORT:-3333}
volumes:
- ./data:/app/dataCreate .env (adjust values before starting):
ADMIN_PASSWORD=change-me
SESSION_SECRET=change-me-to-a-random-string-at-least-32-characters
PORT=3333
# Behind a reverse proxy (HTTPS):
# APP_URL=https://dashboard.example.com
# COOKIE_SECURE=truedocker compose pull
docker compose up -dDashboard: http://localhost:3333 (or the PORT set in .env)
Docker CLI only (without Compose):
mkdir homelab-dashboard && cd homelab-dashboardCreate .env as above, then:
docker pull ghcr.io/maxdeavy/dashboard:1.0.5
docker run -d \
--name homelab-dashboard \
--restart unless-stopped \
-p 3333:3333 \
--env-file .env \
-v "$(pwd)/data:/app/data" \
ghcr.io/maxdeavy/dashboard:1.0.5Available versions: GitHub Releases
Image tag matches the release version (v1.0.5 → ghcr.io/maxdeavy/dashboard:1.0.5). The latest tag always points to the most recent release.
cp .env.example .env
# Set PORT=3333 and production secrets
docker compose up -d --buildThe SQLite database lives in the ./data volume. Migrations run automatically on container start.
All settings are managed in the admin at /admin. The last opened tab is remembered in the browser.
| Feature | Description |
|---|---|
| Web URL / LAN URL | External address and optional local IP/hostname |
| Active / inactive | Disabled services are hidden |
| Icons | Presets from assets/, custom URL, or upload |
| Tile color | Individual accent color |
| Health check | Optional separate URL; per-service TLS exception |
| Hover widget | API integration with encrypted credentials |
| Link target | Same tab or new tab |
| Row layout | Arrange up to three services side by side per row |
Unsaved changes in service or category dialogs trigger a confirmation before closing.
In Admin under Services, the default view is still one full-width row per service. Via drag & drop you can also place services side by side or between existing tiles — up to three per row. The layout is saved automatically and reflected on the dashboard.
| Action | How |
|---|---|
| Own row | Drag onto the line above a row |
| Beside another | Drag onto the vertical line to the right of a tile |
| Between two | Drag onto the vertical line between two tiles |
| New row at bottom | Drag below the last row in the category |
Single services keep full width — only deliberately grouped tiles sit side by side.
| Layout | Appearance |
|---|---|
| 1 per row | Standard tile: icon left, name and subtitle |
| 2 per row | Compact tiles with spacing, uniform row height |
| 3 per row | Icon on top, subtitle below (no service name), uniform row height |
Logged-in admins can also adjust layout on the dashboard with Shift + drag & drop (horizontal and vertical insertion lines, same as in admin).
The selected services page in the admin is remembered in the browser.
Multiple dashboard pages with their own categories. Switch in the dashboard via the tab bar or keys 1–9.
See the keyboard shortcuts table in Features. Preferences for light/dark, Web/LAN, and hiding services without a LAN URL are saved per browser.
- Theme presets (Stealth, Ember, Neon, Cobalt) or custom colors
- Dark / light mode
- Background image, background color, and logo
- Layout sliders: icon size, tile shape, font, column spacing, max width
- Language — English or German (stored in a cookie, applies to dashboard and admin)
| Mode | Behavior |
|---|---|
| Web | Always uses the Web URL |
| LAN | Uses lanUrl when set; otherwise dimmed tile with hint |
You can optionally hide services without a LAN URL entirely in LAN mode (Admin → Settings or key T on the dashboard).
The selection persists after reload (localStorage). The Web/LAN toggle can be hidden in Admin → Settings if you do not need LAN URLs.
With a widget panel open, hold Shift and click a field label to hide or show it. The selection is stored per service in the database and applies on every device (logged-in admins only). In Shift mode, hidden fields appear struck through so you can restore them.
- Hold Shift
- Drag categories (⋮⋮ handle) and tiles to reorder
- Release Shift — normal navigation resumes
While dragging tiles, insertion lines appear: horizontal between rows, vertical between or beside tiles in a row. Only for logged-in admins; disabled while a search is active.
In Admin under Settings: export or import a ZIP backup (database and uploads, including encrypted widget credentials).
In Admin: Services → edit service → Links & widget
| Widget | Credentials | Shows e.g. |
|---|---|---|
| Sonarr / Radarr / Lidarr / Bazarr | API key | Queue, size, missing |
| Prowlarr | API key | Indexers, queue |
| qBittorrent / Transmission / Deluge | User + password | Speed, active torrents |
| SABnzbd | API key | Speed, queue |
| Proxmox | API token + node | CPU, RAM, disk, VMs/LXC, uptime |
| Docker Engine | — | Containers, images |
| Portainer | API token | Containers per endpoint |
| Nginx Proxy Manager | Email + password | Online hosts, SSL certs, redirects |
| Pi-hole / AdGuard Home | App password / API key | Queries, block rate |
| Technitium DNS | API token | Queries, blocklists |
| Jellyseerr / Overseerr | API key | Open requests |
| Jellyfin / Plex / Tautulli | API key / token | Active streams, library, sessions / stats |
| Nextcloud | NC token | Storage, users, apps |
| Immich | API key | Library statistics |
| Mealie | API token | Recipes |
| Kavita / Audiobookshelf | Auth key / API token | Libraries, series, storage / media |
| Navidrome | User + password | Artists, albums, now playing |
| Paperless-ngx | API token | Documents, inbox |
| Trilium Notes | ETAPI token | Notes, attachments, storage |
| Uptime Kuma | API key / status page | Monitors up/down, response time |
| Vaultwarden | Admin token | Users, organizations |
| Gitea | Access token | Repos, issues, pull requests |
| Syncthing | API key | Devices, folders, sync traffic |
| Authentik | API token | Users, apps, providers |
| FreshRSS | Email + API password | Unread, feeds, categories |
| BookStack | API token | Shelves, books, pages |
| Frigate | User + password | Cameras, detection, events |
| Autobrr | API key | Releases, filters, indexers |
| Romm | User + password | Platforms, ROMs, storage |
| WordPress | User + application password | Posts, pages, comments, media |
| Open-Meteo | — (location) | Temperature, wind, precipitation |
| OpenWeatherMap | API key + location | Current weather |
| Cloudflare | API token | Zones, account, status |
| DuckDNS / No-IP | Token or login | Domain, IP |
| Tailscale | API key | Devices online/offline |
| GitHub | PAT | Repos, notifications |
| Healthchecks.io | API key | Checks up/down |
| Gotify / ntfy / Pushover | Token | Messages, status |
| Telegram / Matrix / Discord / Slack | Bot token | Bot status, channels |
| UniFi / OPNsense / TrueNAS | Login or API key | Network, system |
| Glances / Netdata / Prometheus | URL (key optional) | CPU, RAM, metrics |
| OctoPrint / Komga / Shelly | Key or login | Print, manga, relay |
| Sentry / AfterShip / HetrixTools | API-Key | Errors, packages, uptime |
| Beszel | Email + password | Systems online/offline, CPU, RAM, disk (color-coded) |
| Homelab Dashboard | API key | Remote dashboard stats |
| n8n / Grafana | API key | Workflows / dashboards |
| Home Assistant | Access token | Entities, automations, or entity state |
| QNAP | User + password | CPU, RAM, volume, temperature |
| FileBrowser | User + password | Storage usage, shares, version |
| Guacamole | User + password | Connections |
| FRITZ!Box | — | Connection, speeds, external IP, traffic (TR-064) |
| Iframe / Embed | — | External page in hover panel |
| Generic | — | HTTP status, latency |
API credentials are stored AES-256-GCM encrypted in SQLite.
Under Admin → Settings → Security, you can enable a read-only REST API. Other dashboards or scripts can fetch services, health status, and widget data — without the admin password, using an API key only.
- Enable Public API (v1) and save
- Generate API key (shown once — store it securely)
- Send requests with
Authorization: Bearer <api-key>or headerX-API-Key
| Endpoint | Content |
|---|---|
GET /api/v1 |
API info and endpoint list |
GET /api/v1/dashboard |
Pages, categories, services, links, stats |
GET /api/v1/services |
All services incl. health |
GET /api/v1/health |
Health checks |
GET /api/v1/widgets/{id} |
Widget data for a service |
Widget credentials and encrypted secrets are never exported. Use widget type Homelab Dashboard to display data from another instance with API v1 enabled.
When Dashboard requires sign-in is enabled, internal read APIs (/api/dashboard, /api/health, /api/widgets, …) also accept the Public API key for automation (widget credentials remain session-only).
Talks to the Docker Engine API. docker-compose.yml includes an optional socket mount; docker-socket-proxy is safer:
- Enable the proxy in
docker-compose.yml(commented template) - Widget type Docker Engine, API URL
http://docker-socket-proxy:2375
- User with system monitoring permissions
- Disable 2FA for the API user
- API URL e.g.
https://nas.local:8080(QNAP web port) - Optional: volume name for a single volume
Copy .env.example to .env. Only a few values are required for a typical deployment.
| Variable | Description |
|---|---|
ADMIN_PASSWORD |
Admin login (plain text or bcrypt hash via npm run hash-password) |
SESSION_SECRET |
Session encryption (min. 32 characters) |
| Variable | Description | Default |
|---|---|---|
PORT |
HTTP port | 3000 (Docker: 3333) |
APP_URL |
Public URL for redirects behind a reverse proxy | from request headers |
COOKIE_SECURE |
Send session cookie only over HTTPS | false |
HOSTNAME (bind address inside the container) defaults to 0.0.0.0 in the Docker image — you do not need to set it in .env. In Compose, environment: overrides .env for the same key anyway.
APP_URL is not the same as HOSTNAME: the app listens on 0.0.0.0:PORT inside Docker, while APP_URL is the address users type in the browser (e.g. https://dashboard.example.com). Set APP_URL only when redirects after login go to the wrong host (typical behind nginx, Traefik, etc.).
| Variable | Default |
|---|---|
CREDENTIALS_ENCRYPTION_SECRET |
= SESSION_SECRET |
CREDENTIALS_ENCRYPTION_SALT |
homelab-dashboard-salt |
SESSION_COOKIE_NAME |
homelab-dashboard-session |
SESSION_MAX_AGE_DAYS |
7 |
DATABASE_URL |
file:./data/dashboard.db |
APP_STORAGE_PREFIX / NEXT_PUBLIC_APP_STORAGE_PREFIX |
homelab-dashboard |
MAX_BACKGROUND_UPLOAD_MB |
5 |
MAX_LOGO_UPLOAD_MB |
1 |
MAX_ICON_UPLOAD_MB |
1 |
Cookie name and storage prefix only matter if you run multiple instances on the same browser origin or need to migrate sessions — not for a normal single install.
In production, ADMIN_PASSWORD and SESSION_SECRET must be set.
For a bcrypt hash instead of plain text:
npm run hash-password -- "your-strong-password"Put the output into ADMIN_PASSWORD in .env.
Migrations run automatically when the server starts (instrumentation.ts). npm run db:migrate is optional for manual runs.
See SECURITY.md for deployment guidance.
Self-signed TLS certificates: enable per service in Admin under Health check.
Bundled icons live in assets/ and are copied to public/assets/ on build (npm run assets:sync).
- Next.js 16 (App Router), React 19, Tailwind CSS 4
- next-intl for internationalization
- SQLite with Drizzle ORM
- iron-session for admin auth
- Docker with multi-stage build
| Command | Description |
|---|---|
npm run dev |
Development server |
npm run build |
Production build |
npm run start |
Production server |
npm run db:migrate |
Run database migrations manually |
npm run db:generate |
Generate Drizzle migration |
npm run hash-password |
Generate bcrypt hash for ADMIN_PASSWORD |
npm run lint |
ESLint |
- Set strong values for
ADMIN_PASSWORDandSESSION_SECRETin production. - Prefer a bcrypt hash for
ADMIN_PASSWORD(npm run hash-password). - Set
COOKIE_SECURE=truebehind HTTPS. - Enable Dashboard requires sign-in when the instance is reachable beyond trusted networks.
- Enable Public API (v1) only when needed; treat the API key like a password.
- Login and failed API key attempts are rate-limited per IP.
- Widget credentials are encrypted in the DB; still grant only minimal API permissions.
- Mounting the Docker socket directly is convenient but risky — use a socket proxy.
Details: SECURITY.md
Bugs, missing widget data, or feature ideas — please open an issue (service name, widget type, expected vs. actual behavior). See also the note under Features.
