# 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`: ```yaml 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](https://github.com/homarr-labs/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](docker-compose.yml)): ```sh 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](docker-compose.agent.yml)), which publishes port 8081 on that host's LAN IP only: ```sh 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 ` | | `/icons/` | 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. ```sh docker build --target check . # gofmt, go vet, go test docker build -t hoda . # the production image ``` Gitea Actions ([.gitea/workflows/ci.yml](.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-`; a `v1.2.3` tag publishes `:1.2.3`. Images are scanned with Trivy, and a CRITICAL finding blocks the push.