Files
hermes-config/skills/self-hosting/docker-service-deployment/references/homarr.md
T
2026-07-12 10:17:17 -04:00

13 KiB

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).

# ~/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:

# 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 <command>.

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:

docker exec homarr homarr users list

Create admin (only when no users exist):

docker exec homarr homarr recreate-admin --username <username>
# Outputs: generated password and temporary group name

Reset password (generates random password, displayed in terminal):

docker exec homarr homarr reset-password --username <user>

Set specific password (needs user to already exist):

docker exec homarr homarr users update-password --username <user> --password '<password>'
# Quote the password with single quotes to protect shell special chars ($, ^, \, etc.)

Delete user:

docker exec homarr homarr users delete --username <user>

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.

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:

INSERT INTO app (id, name, description, icon_url, href, ping_url)
VALUES ('<generated-id>', 'Service Name', '', 'https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/<icon>.png', 'http://192.168.50.98:<port>', '');

The item Table

Widget placements on a board. The kind field determines the widget type.

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:

INSERT INTO item (id, board_id, kind, options, advanced_options)
VALUES ('<generated-id>', '<board-id>', 'app',
  '{"json":{"appId":"<app-id>","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":["<app-id1>","<app-id2>"]}}

The item_layout Table

Grid position for each item on a section.

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:

INSERT INTO item_layout (id, section_id, item_id, x, y, width, height)
VALUES ('<generated-id>', '<section-id>', '<item-id>', <x>, <y>, 1, 1);

Finding IDs for Existing Records

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/<service>.png
https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/<service>.svg

# WalkXcode dashboard-icons (SVG)
https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/svg/<service>.svg

# Logan Marchione homelab-svg-assets
https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets@latest/assets/<service>.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

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:

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)