Skip to content

Homepage dashboard

Github GitHub


Overview

Homepage is a static, YAML-configured start page for a homelab: tiles with links, icons and descriptions, grouped by category, plus a few info widgets (search, date/time, weather). It can also show live per-service stats through API widgets and running/healthy dots through the Docker API.

I keep mine deliberately simple: links, icons and descriptions, no per-service API widgets. It loads instantly, and it doesn't need an API key for every service. The status dots come from Docker for containers on the same host.

Compose

compose.yaml
services:
  homepage:
    image: ghcr.io/gethomepage/homepage:v2.6.0
    container_name: homepage
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "3010:3000"
    volumes:
      # Config comes from this directory itself (git-managed, next to compose.yaml)
      - ./:/app/config
      - /var/run/docker.sock:/var/run/docker.sock:ro
.env.example
# Every host:port you open the dashboard by (comma-separated, no spaces) - see the gotcha below
HOMEPAGE_ALLOWED_HOSTS=home.<YOUR_LOCAL_DOMAIN>,<YOUR_SERVER_IP>:3010,<YOUR_HOSTNAME>:3010

Config straight from the repo

The volume ./:/app/config mounts the stack's own directory from git: services.yaml, settings.yaml, widgets.yaml, bookmarks.yaml, docker.yaml, custom.css, custom.js all sit next to compose.yaml. Editing a tile is a commit and a push; the GitOps redeploy recreates the container and the dashboard updates. No SSH, no editing files on the host.

This is safe only because Homepage never writes to its config: it has no in-app editor. Don't copy the pattern for apps that save settings or state into their config directory at runtime; those need a stable host path (e.g. /mnt/appdata/<service>), or every deploy fights the app.

The HOMEPAGE_ALLOWED_HOSTS gotcha

Since v1.0, Homepage only answers requests whose Host header is in HOMEPAGE_ALLOWED_HOSTS (localhost:3000 and 127.0.0.1:3000 are always allowed). It protects the API proxy against DNS-rebinding style attacks. Anything else gets a host validation error instead of the dashboard.

The catch: it matches host:port exactly as the request sends it. So

  • home.<YOUR_LOCAL_DOMAIN> (through the reverse proxy on 443) is one entry,
  • <YOUR_SERVER_IP>:3010 and <YOUR_HOSTNAME>:3010 (direct to the published port) are separate entries, and
  • an uptime monitor that checks http://<YOUR_HOSTNAME>:3010 needs its address in the list too, or the monitor goes red while the dashboard works fine in your browser.

That last one bit me: the monitor sat at DOWN for days because it used the direct host:port and only the proxy name was allowed. When you see host validation errors, check the container log, which prints the host exactly as received, and add that string. * disables the check; don't do that on anything reachable beyond your LAN.

Configuration notes

  • settings.yaml: layout: sets style: row and a column count per category, tuned to the number of tiles so rows fill evenly. hideVersion, useEqualHeights, and quicklaunch (type on the page to search tiles) are worth turning on.
  • services.yaml: each tile is icon, href, description, and optionally container: <name> for the status dot. Prefer the reverse-proxy hostname for href; fall back to host:port only where there's no proxy entry. Re-check the links now and then against docker ps: a dashboard full of dead tiles is worse than none.
  • docker.yaml: local: { socket: /var/run/docker.sock } gives dots for containers on the same host. For other hosts, use a docker-socket-proxy on each one that only allows read-only container endpoints, and point a docker.yaml entry at it. Don't publish a raw Docker socket over TCP. Even a :ro socket mount exposes the full read API (env vars included), so mount it only into containers you trust.
  • widgets.yaml: search, date/time, Open-Meteo weather. I dropped the host resources widget because it only shows the host Homepage runs on, which is misleading on a fleet dashboard; per-host stats live in Beszel.
  • custom.css / custom.js: loaded automatically from the config directory. Homepage is a client-rendered app, so custom JS has to wait for (and survive re-renders of) the elements it touches; poll and fail silently.
  • Pin the image. Tag pinned, Renovate opens the update PRs.

Sources & further reading