Apps opt in with sema.dashboard.* labels in their own compose file. The primary reads its host's containers through a read-only socket proxy, follows Docker events, and merges in what the agent on the second host reports. Icons come from the icon label, the dashboard-icons set or the app's favicon, with a letter as the fallback. Go with only the standard library, a distroless image, and a Gitea Actions workflow copied from FiMa's: checks in the Dockerfile's check stage, a Trivy scan, and a push to git.mariosem.es/msemes/hoda. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.4 KiB
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 | Where HODA fetches the favicon when the public URL sits behind Pangolin's login |
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 |
A container with enable but a missing or invalid url stays off the dashboard and is logged once.
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
- Host 1, the primary: deploy/primary/compose.yml
- Host 2, the agent: deploy/agent/compose.yml
Both need the same HODA_PEER_TOKEN in a .env file next to them (deploy/.env.example).
Publish the primary through Pangolin with SSO on; give it no public port of its own.
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.