# Homarr Dashboard A customizable, widget-based dashboard for managing and monitoring self-hosted services. Supports Docker container status, Plex sessions, download clients, system monitoring, and the Arr stack. ## Deployment (Local, No VPS) Homarr runs on port 8080 directly — no nginx reverse proxy (it's the landing page for the homelab). ```yaml # ~/docker/homarr/docker-compose.yml services: homarr: container_name: homarr image: ghcr.io/homarr-labs/homarr:latest restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock # Docker integration (auto-detect containers) - ./appdata:/appdata # Config + icons environment: - SECRET_ENCRYPTION_KEY=<64-char-hex> # Generated via `openssl rand -hex 32` ports: - '8080:7575' # Host 8080 → container 7575 ``` ## Key Details - **Container internal port**: 7575 (maps to Next.js on 3000 internally — transparent to the user) - **Docker socket mount**: Required for Homarr to auto-discover running containers and show their status - **Data directory**: `~/docker/homarr/appdata/` — contains SQLite DB (`appdata/db/db.sqlite`), icons, and config - **First run**: Opens setup wizard on first visit to `http://192.168.50.98:8080/` - **`SECRET_ENCRYPTION_KEY`**: Must be a 64-char hex string. Generate with `openssl rand -hex 32`. If changed after initial setup, existing encrypted data becomes unreadable. ## Replacing an Existing Dashboard (e.g. Heimdall) To swap Heimdall → Homarr while keeping the same port: ```bash # 1. Stop and remove old container docker stop heimdall && docker rm heimdall # 2. (Optional) Keep old data volume if needed docker volume ls | grep heimdall # 3. Deploy Homarr on the same port (8080) mkdir -p ~/docker/homarr # write docker-compose.yml as above cd ~/docker/homarr && docker compose up -d # 4. Verify curl -sL -o /dev/null -w "%{http_code}" http://localhost:8080/ # → 200 ``` **⚠️ Stale iptables DNAT rule**: If `curl localhost:8080` works but `curl 192.168.50.98:8080` doesn't, the old container likely left a stale DNAT rule. See "Stale iptables DNAT rules after replacing a container on the same port" in the main SKILL.md for diagnosis and fix. **Docker loopback note**: After fixing the DNAT, localhost works and external clients on the network work, but curling the machine's own external IP from itself may still timeout — this is a known Docker kernel quirk, not a real connectivity problem. All existing bookmarks to `http://192.168.50.98:8080/` continue working. --- ## Homarr CLI (User & Admin Management) The Homarr container ships with a built-in CLI accessed via `docker exec homarr homarr `. ### Available Commands ``` homarr users — Group of commands to manage users homarr integrations — Group of commands to manage integrations homarr reset-password — Reset password for a user (generates random password) homarr fix-usernames — Changes all credentials usernames to lowercase homarr recreate-admin — Recreate credentials admin user if none exists anymore ``` ### User Management **List users:** ```bash docker exec homarr homarr users list ``` **Create admin (only when no users exist):** ```bash docker exec homarr homarr recreate-admin --username # Outputs: generated password and temporary group name ``` **Reset password (generates random password, displayed in terminal):** ```bash docker exec homarr homarr reset-password --username ``` **Set specific password (needs user to already exist):** ```bash docker exec homarr homarr users update-password --username --password '' # Quote the password with single quotes to protect shell special chars ($, ^, \, etc.) ``` **Delete user:** ```bash docker exec homarr homarr users delete --username ``` ### Pitfalls - **Shell special characters in passwords**: Always quote with single quotes (`'password'`), never double quotes. Double quotes still interpret `$`, `!`, and backticks inside the `docker exec` shell. - **Password with `$` and Docker Compose escaping**: If setting a password via `docker compose` environment or run command, any `$` must be doubled to `$$` because Docker Compose interprets `$` as variable expansion — regardless of YAML quote style. This is a compose-specific behavior, not bash. - **`users list` may hang/be interrupted** on some Homarr versions — add `2>&1 | head -30` and a timeout if it hangs. - **All sessions terminated**: After any password reset or update, all active sessions for that user are invalidated. They must re-login. - **User must exist first**: `update-password` and `reset-password` require the user to already exist. If the database has no users, use `recreate-admin` first. --- ## Programmatic Dashboard Population (SQLite) Homarr stores all configuration in a SQLite database at `appdata/db/db.sqlite` inside the container (mapped from `./appdata/db/db.sqlite` on the host). You can add services, place them on boards, and configure the layout by manipulating this database directly — much faster than clicking through the UI for 20+ services. ### Database Schema (Key Tables) ``` app — Service definitions (name, icon, URL, ping URL) item — Widget placements on boards (refers to app via options JSON) item_layout — Grid position of each item (x, y, width, height) board — Board definitions (name, colors, CSS) section — Board sections (empty/dynamic layouts) section_layout — Layout of sections on a board group — User groups user — Users (id, name, email, password hash) ``` ### The `app` Table Service definitions. Homarr auto-discovers Docker containers via the Docker socket and populates this table. ```sql CREATE TABLE `app` ( `id` TEXT PRIMARY KEY NOT NULL, -- ~25 char alphanumeric ID `name` TEXT NOT NULL, -- Display name `description` TEXT, -- Optional tooltip `icon_url` TEXT NOT NULL, -- URL to icon image `href` TEXT, -- URL to open on click `ping_url` TEXT -- URL for status checks (optional) ); ``` **Insert an app:** ```sql INSERT INTO app (id, name, description, icon_url, href, ping_url) VALUES ('', 'Service Name', '', 'https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/.png', 'http://192.168.50.98:', ''); ``` ### The `item` Table Widget placements on a board. The `kind` field determines the widget type. ```sql CREATE TABLE `item` ( `id` TEXT PRIMARY KEY NOT NULL, `board_id` TEXT NOT NULL, -- References board.id `kind` TEXT NOT NULL, -- 'app', 'clock', 'weather', 'bookmarks', etc. `options` TEXT DEFAULT '{"json": {}}' NOT NULL, -- Widget-specific JSON config `advanced_options` TEXT DEFAULT '{"json": {}}' NOT NULL ); ``` **Insert an app item:** ```sql INSERT INTO item (id, board_id, kind, options, advanced_options) VALUES ('', '', 'app', '{"json":{"appId":"","openInNewTab":true,"showTitle":true}}', '{"json":{}}'); ``` **Other widget kinds:** - `clock`: Simple clock widget — options: `{"json":{}}` - `weather`: Weather widget — options: `{"json":{"showCity":true,"hasForecast":true,"forecastDayCount":3}}` - `bookmarks`: Links collection — options: `{"json":{"title":"Name","layout":"grid","openNewTab":true,"items":["",""]}}` ### The `item_layout` Table Grid position for each item on a section. ```sql CREATE TABLE `item_layout` ( `id` TEXT PRIMARY KEY NOT NULL, `section_id` TEXT NOT NULL, -- References section.id `item_id` TEXT NOT NULL, -- References item.id `x` INTEGER NOT NULL, -- Column position (0-indexed) `y` INTEGER NOT NULL, -- Row position (0-indexed) `width` INTEGER NOT NULL, -- Width in grid units `height` INTEGER NOT NULL -- Height in grid units ); ``` **Insert a layout position:** ```sql INSERT INTO item_layout (id, section_id, item_id, x, y, width, height) VALUES ('', '', '', , , 1, 1); ``` ### Finding IDs for Existing Records ```bash sqlite3 /home/ray/docker/homarr/appdata/db/db.sqlite "SELECT id, name FROM board;" sqlite3 /home/ray/docker/homarr/appdata/db/db.sqlite "SELECT id FROM section LIMIT 5;" sqlite3 /home/ray/docker/homarr/appdata/db/db.sqlite "SELECT lid, section_id FROM section_layout LIMIT 5;" ``` ### Icon URLs Use one of the icon repositories: ``` # Homarr Labs dashboard-icons (PNG - most comprehensive) https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/.png https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/.svg # WalkXcode dashboard-icons (SVG) https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/svg/.svg # Logan Marchione homelab-svg-assets https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets@latest/assets/.svg ``` Common icon filenames: `immich`, `home-assistant`, `paperless-ngx`, `plex`, `audiobookshelf`, `mealie`, `qbittorrent`, `pihole`, `portainer`, `uptime-kuma`, `scrutiny`, `vaultwarden`, `pocketbase`, `glitchtip`, `searxng`, `sunshine`, `homarr`, `github`, `redis`, `postgres`, `valkey`, `chrome-beta`. ### Complete Population Workflow ```python import sqlite3 import secrets import string import json DB_PATH = "/home/ray/docker/homarr/appdata/db/db.sqlite" def gen_id(): """Generate a ~25 char alphanumeric ID matching Homarr's format.""" alphabet = string.ascii_lowercase + string.digits return ''.join(secrets.choice(alphabet) for _ in range(25)) def add_service(conn, board_id, section_id, name, icon_name, href, x, y, w=1, h=1): app_id = gen_id() item_id = gen_id() layout_id = gen_id() # 1. Create app icon_url = f"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/{icon_name}.png" conn.execute("INSERT INTO app (id, name, description, icon_url, href, ping_url) VALUES (?, ?, '', ?, '', '')", (app_id, name, icon_url, href)) # 2. Create item options = json.dumps({"json": {"appId": app_id, "openInNewTab": True, "showTitle": True}}) conn.execute("INSERT INTO item (id, board_id, kind, options, advanced_options) VALUES (?, ?, 'app', ?, '{\"json\":{}}')", (item_id, board_id, options)) # 3. Create layout conn.execute("INSERT INTO item_layout (id, section_id, item_id, x, y, width, height) VALUES (?, ?, ?, ?, ?, ?, ?)", (layout_id, section_id, item_id, x, y, w, h)) conn.commit() print(f" Added {name} at ({x},{y})") # Usage: # conn = sqlite3.connect(DB_PATH) # board_id = "un39cd3f4sj9ik35dkhuvo92" # from SELECT on board table # section_id = "r6ly519w0mp0ez7mgd8klg3z" # from SELECT on section table # services = [ # ("Immich", "immich", "http://192.168.50.98:2283", 0, 0, 2, 1), # ("Home Assistant", "home-assistant", "http://192.168.50.98:8123", 2, 0), # ... # ] # for name, icon, url, x, y, *size in services: # w, h = size if size else (1, 1) # add_service(conn, board_id, section_id, name, icon, url, x, y, w, h) # conn.close() ``` ### Restart After Population After any database modification, restart the container to reload: ```bash docker restart homarr ``` ### Pitfalls - **IDs must be unique across their respective tables** — generate them with `secrets.token_urlsafe()` or custom alphanumeric generation, never hardcode. - **Board and section IDs differ per instance** — always query the live database first to discover them. - **`openInNewTab` is optional but recommended** — set `true` so clicking a service opens it in a new browser tab rather than navigating away from the dashboard. - **`showTitle` defaults to true** — set `false` for icon-only widgets if the grid is dense. - **Docker auto-discovery may recreate apps** — if Homarr detects a container name matching an app name, it may overwrite the `href` field. Workaround: edit the `app` entry via the Homarr Web UI after database insertion, or in the `options` JSON reference the Homarr-generated `app.id` instead of creating new `app` rows. - **Restart required** — Homarr caches the board layout aggressively. SQLite writes alone won't reflect on the page until the container restarts. - **Backup before bulk edit** — always copy `db.sqlite` before adding many services: `cp /home/ray/docker/homarr/appdata/db/db.sqlite{,.bak}` ### Grid Layout Design Design a clean grid before inserting. Common patterns: - **4-column grid** works well for most layouts - **2-wide for primary services** (Immich, Home Assistant, Plex) - **1-wide** for standard services - **Leave gaps for future additions** at the end of each row Example layout for 16 services: ``` Row 0: Immich (2w), HA (1w), Paperless (1w) Row 1: Plex (1w), Audiobk (1w), Mealie (1w), qBit (1w) Row 2: Portainer (1w), Pi-hole (1w), Kuma (1w), Scrutiny (1w) Row 3: Vaultwrdn (1w), SearXNG (1w), PocketB (1w), HermesDb(1w) Row 4: HermesUI (1w), Sunshine(1w), Chrome (1w), GlitchT(1w) ```