TMCandClaude Opus 5.5 60b4c79b0f
CI / build (push) Successful in 40s
Show internal URLs, list label problems, copy a label template
Tiles now show the public URL and, when set, the internal URL as a second
link. Label names ignore case and - versus _. Containers HODA can't fully
use are listed above the tiles with the reason: labels without enable,
a missing url, or an unknown label name with a suggestion (category ->
group). To find labels without enable, discovery reads all containers.

A "Copy labels" button copies every label as a commented template, and
falls back to the copy command where the clipboard API is missing (plain
http on the LAN).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:55:06 +01:00

HODA

HODA (HOmelab DAshboard) shows every self-hosted app as a tile with its icon, name and link. Apps opt in with Docker labels in their own compose file, so adding an app never means editing the dashboard. It runs at apps.mariosem.es behind Pangolin, which handles login; HODA itself has none.

One image, two roles: the primary serves the page, and an agent on each other Docker host reports that host's apps to it. Each instance reads only its own Docker, read-only, through a socket proxy.

Add an app

Put labels on the service in its compose file, then docker compose up -d:

services:
  gitea:
    image: gitea/gitea
    labels:
      sema.dashboard.enable: "true"
      sema.dashboard.name: Gitea
      sema.dashboard.url: https://git.mariosem.es
      sema.dashboard.group: Dev
      sema.dashboard.description: Git hosting and CI
Label Required Effect
sema.dashboard.enable Yes true opts the container in
sema.dashboard.url Yes Link the tile opens (http or https)
sema.dashboard.name No Tile title; defaults to the compose service name
sema.dashboard.icon No A dashboard-icons name (jellyfin) or an image URL
sema.dashboard.internal_url No LAN address, shown on the tile as a second link; HODA also fetches the favicon from it
sema.dashboard.description No One line under the name
sema.dashboard.group No Section heading; apps without one go under "Apps", last
sema.dashboard.order No Position inside the group; ties sort by name

Label names ignore case and - versus _, so sema.dashboard.Group and sema.dashboard.internal-url work too. The Copy labels button at the top of the dashboard copies this whole set as a commented template.

Anything HODA can't use is listed in a label problems panel above the tiles: a container with sema.dashboard labels but no enable: "true", a missing or invalid url, or an unknown label name (with a suggestion, such as category → group). Containers without a url aren't shown until it's fixed.

Icons, first match wins: the icon label, the dashboard-icons set by app name, then by image name (generic images such as nginx or node are skipped), the app's own favicon, and finally the app's first letter. Icons are cached in /data/icons and refreshed daily.

Deploy

On the main host (docker-compose.yml):

cp .env.example .env
docker compose pull && docker compose up -d

HODA then listens on 10.20.30.10:10040 (HODA_BIND_IP, HODA_PORT). In Pangolin, point the apps.mariosem.es resource at http://10.20.30.10:10040 and turn SSO on. The stack brings its own read-only socket proxy.

A second Docker host runs the agent (docker-compose.agent.yml), which publishes port 8081 on that host's LAN IP only:

cp .env.agent.example .env            # set HODA_PEER_TOKEN and HODA_AGENT_BIND_IP
docker compose -f docker-compose.agent.yml pull && docker compose -f docker-compose.agent.yml up -d

Then set HODA_AGENT_URL (for example http://10.20.30.11:8081) and the same HODA_PEER_TOKEN (openssl rand -hex 32) in the main host's .env, and run docker compose up -d there again.

Configuration

All environment variables are optional.

Variable Default Purpose
MODE primary agent serves only /api/apps and /icons to the primary, with no page
PEERS Comma-separated agent URLs the primary pulls from
PEER_TOKEN Shared token between primary and agents; required with PEERS or MODE=agent
PEER_INTERVAL 5s How often the primary pulls each agent
NODE_NAME container hostname Host name in tile tooltips and search
DOCKER_HOST unix:///var/run/docker.sock Docker API; point it at the socket proxy
LABEL_PREFIX sema.dashboard Label namespace
DASHBOARD_TITLE HODA Page header and browser tab title
PORT 8080 HTTP port
LINK_TARGET _blank _self opens apps in the same tab
RESYNC_INTERVAL 60s Full container re-list, on top of Docker events
ICON_REFRESH 24h How often cached icons are fetched again
ICON_CDN jsDelivr's dashboard-icons Icon set base URL; empty turns the icon set off
DATA_DIR /data Where the icon cache lives
LOG_LEVEL info debug shows every icon lookup

Endpoints

Path Primary Agent
/ The dashboard A one-line notice
/api/apps All hosts' apps as JSON, with an ETag This host's apps; needs Authorization: Bearer <PEER_TOKEN>
/icons/<id> Cached icon, or fetched from the agent that owns it This host's icons; needs the token
/healthz ok ok, no token

Development

HODA is Go with only the standard library; the page is plain HTML, CSS and JavaScript embedded in the binary. Go doesn't need to be installed: everything runs in Docker.

docker build --target check .   # gofmt, go vet, go test
docker build -t hoda .          # the production image

Gitea Actions (.gitea/workflows/ci.yml) runs the checks on every push and pull request. Pushes to main publish git.mariosem.es/msemes/hoda:latest and :sha-<short>; a v1.2.3 tag publishes :1.2.3. Images are scanned with Trivy, and a CRITICAL finding blocks the push.

S
Description
No description provided
Readme
109 KiB
0 Stars 1 Watchers 0 Forks
Languages
Go 78.6%
JavaScript 11.4%
CSS 7.1%
Dockerfile 1.5%
HTML 1.4%