Skip to content

Homepage — The Dashboard That Actually Gets Used

Every homelab grows the same way: one docker-compose.yml turns into five, then twenty. Before long you have a page of bookmarks named “Sonarr (local)”, “Sonarr (remote)”, “that thing on port 8686”, and a text file somewhere with IP:port combinations you keep forgetting.

Homepage (by gethomepage) fixes this with:

FeatureWhat it does
ServicesTiled links to your apps, grouped into sections you define
WidgetsLive status pulled from each app’s API — episodes downloading, VPN IP, disk usage, etc.
BookmarksA separate bookmark bar for external links (trackers, docs, registrar)
Docker integrationPulls container state (running / stopped / unhealthy) from the Docker socket
YAML configEverything lives in a handful of files. No clicks, no database, no migrations.

The whole thing is one static-feeling page that loads in ~200ms. It sits at home.falseviking.uk and is the browser homepage on every device in the house.

The container is tiny and mounts two things: a config directory and the Docker socket (read-only).

/opt/stacks/homepage/compose.yaml
services:
homepage:
image: ghcr.io/gethomepage/homepage:latest
container_name: homepage
environment:
HOMEPAGE_ALLOWED_HOSTS: ${HOMEPAGE_ALLOWED_HOSTS}
PUID: ${PUID}
PGID: ${PGID}
# API keys for widgets — pulled from .env
HOMEPAGE_VAR_PLEX_KEY: ${HOMEPAGE_VAR_PLEX_KEY}
HOMEPAGE_VAR_SONARR_KEY: ${HOMEPAGE_VAR_SONARR_KEY}
HOMEPAGE_VAR_RADARR_KEY: ${HOMEPAGE_VAR_RADARR_KEY}
HOMEPAGE_VAR_QBIT_USER: ${HOMEPAGE_VAR_QBIT_USER}
HOMEPAGE_VAR_QBIT_PASS: ${HOMEPAGE_VAR_QBIT_PASS}
ports:
- 3000:3000
volumes:
- /mnt/nfs/docker/docker/homepage/config:/config
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
networks:
- proxy
- mediastack
networks:
proxy:
external: true
mediastack:
name: mediastack_mediastack
external: true
.env
HOMEPAGE_ALLOWED_HOSTS=home.falseviking.uk,192.168.1.248:3000
PUID=1027
PGID=65536
# Add the HOMEPAGE_VAR_* keys as you wire up widgets

A few things to note:

  • HOMEPAGE_ALLOWED_HOSTS is mandatory on recent versions. If you load Homepage through a reverse proxy hostname that isn’t in this list, you’ll get a blank page. Include both the public domain and the LAN IP:port.
  • The Docker socket is mounted read-only. Homepage only needs to read container state.
  • Homepage needs to be on the same Docker networks as the apps it talks to. On this host it’s on both proxy (for Authentik, Grafana, etc.) and mediastack (for Sonarr, Radarr, qBit via gluetun).

Start it:

Terminal window
cd /opt/stacks/homepage
docker compose up -d

First load will render an empty dashboard. That’s expected — the config is next.

Everything is in the /config volume. On first boot, Homepage seeds these files with examples:

FilePurpose
settings.yamlGlobal: title, theme, background image, column layouts
services.yamlThe main tiles. Grouped into sections. Widgets live here.
bookmarks.yamlThe bookmark bar (separate from services — no widgets)
widgets.yamlThe info row at the top — clock, weather, resource usage, search
docker.yamlTells Homepage how to reach each Docker engine
kubernetes.yamlSame idea for k8s (leave empty if you don’t use it)

settings.yaml sets the global look and decides how each section is arranged.

settings.yaml
title: Sørens Mediehus
background:
image: https://example.com/your-wallpaper.jpg
blur: sm
brightness: 50
opacity: 100
theme: dark
color: slate
cardBlur: md
headerStyle: clean
favicon: https://cdn-icons-png.flaticon.com/512/1946/1946488.png
useEqualHeights: true
hideVersion: true
layout:
Streaming:
header: true
style: row
columns: 4
The Arr Suite:
header: true
style: row
columns: 3
Downloads & Indexers:
header: true
style: row
columns: 5
Infrastructure:
header: true
style: row
columns: 6

The layout keys must match the group names you use in services.yaml. columns controls how many tiles fit across before wrapping — tune per section, not globally.

Services are grouped into sections. Each service has a link, an icon, an optional container link (for health dot), and optionally a widget.

services.yaml
---
- Streaming:
- Plex:
href: http://192.168.1.248:32400
description: Movies & TV
icon: plex
server: local
container: plex
widget:
type: plex
url: http://192.168.1.248:32400
key: "{{HOMEPAGE_VAR_PLEX_KEY}}"
- Jellyfin:
href: https://jellyfin.falseviking.uk
description: Movies & TV (Open Source)
icon: jellyfin
server: local
container: jellyfin
widget:
type: jellyfin
url: http://jellyfin:8096
key: "{{HOMEPAGE_VAR_JELLYFIN_KEY}}"
- The Arr Suite:
- Sonarr:
href: http://192.168.1.248:8989
description: TV Shows
icon: sonarr
server: local
container: sonarr
widget:
type: sonarr
url: http://sonarr:8989
key: "{{HOMEPAGE_VAR_SONARR_KEY}}"
- Radarr:
href: http://192.168.1.248:7878
description: Movies
icon: radarr
server: local
container: radarr
widget:
type: radarr
url: http://radarr:7878
key: "{{HOMEPAGE_VAR_RADARR_KEY}}"
  • href — where clicking the tile takes you. Usually your public URL (https://...) for the human-facing link.
  • icon — Homepage ships with ~1500 icons. Use the app’s short name (sonarr, plex) or an mdi-* Material Design icon name (mdi-shield-search).
  • server: local + container: sonarr — tells Homepage to watch the Docker container and show a coloured status dot (green = running, red = stopped, amber = unhealthy). The container name must match exactly.
  • widget — the fun part. Different apps have different widget types.

href is for the human — use the public URL. widget.url is for the container — use the internal Docker hostname and port:

# External — opens in a new tab
href: https://jellyfin.falseviking.uk
# Internal — Homepage talks to this directly from inside the network
widget:
type: jellyfin
url: http://jellyfin:8096

This matters because most apps on this host bind their Pangolin-routed hostname to HTTPS with a certificate valid only for the public name. Homepage’s container would fail the TLS check if you pointed the widget at the public URL. Going container-to-container over the Docker network is faster and doesn’t involve certificates.

  • Sonarr / Radarr / Lidarr / Prowlarr / Bazarr — Settings → General → Security → API Key
  • Plex — Settings → Account → “Get current X-Plex-Token” (or grab it from plex.tv → devices)
  • Jellyfin — Dashboard → Administration → API Keys → ”+”
  • Seerr / Jellyseerr — Settings → General → API Key
  • qBittorrent — use the WebUI username and password (no API key system)
  • Gluetun — username/password for the HTTP control server (set via HTTP_CONTROL_SERVER_AUTH env)

Separate from services — no health dots, no widgets, just links. Good for stuff that isn’t yours: trackers, registrar, Cloudflare, documentation.

bookmarks.yaml
---
- Developer:
- GitHub:
- abbr: GH
href: https://github.com/
- Gitea (self-hosted):
- abbr: GT
href: https://gitea.falseviking.uk
- Trackers:
- Aither:
- abbr: AI
href: https://aither.cc/
- DanishBytes:
- abbr: DB
href: https://danishbytes.club/

The strip at the top of the page — clock, weather, resource graph, search bar — is widgets.yaml:

widgets.yaml
---
- greeting:
text_size: 2xl
text: "Velkommen til Sørens Mediehus"
- datetime:
text_size: l
format:
dateStyle: long
timeStyle: short
hour12: false
- openmeteo:
label: Copenhagen
latitude: 55.6761
longitude: 12.5683
units: metric
cache: 15
- resources:
label: System
cpu: true
memory: true
expanded: true
disk:
- /
uptime: true
- search:
provider: duckduckgo
target: _blank

resources reads directly from /proc inside the container. It sees the host’s CPU/memory because Docker doesn’t hide those by default, and it sees / because Homepage’s root filesystem is (by default) the host’s Docker storage driver layer. Add NFS mountpoints by listing them under disk: — but the path must be mounted into the Homepage container first.

If you want the status dot on every tile, you need to tell Homepage how to reach the Docker engine:

docker.yaml
local:
socket: /var/run/docker.sock

That’s all for a same-host setup — the /var/run/docker.sock:/var/run/docker.sock:ro mount in the compose does the rest. For a remote Docker host over TCP, add:

docker.yaml
nas:
host: 192.168.1.154
port: 2375

Then reference it on individual services with server: nas.

The Homepage container listens on 0.0.0.0:3000 inside Docker. Front it with Pangolin (or Traefik / Caddy / nginx) the same way as any other service — route home.example.com to homepage:3000 over HTTP.

HOMEPAGE_ALLOWED_HOSTS must list every hostname you use to reach it, comma-separated. A request arriving with a Host: header that isn’t in the list is rejected with a blank page. Put your public hostname and the LAN address (if you use that too).

Once it’s running, the workflow is:

  1. Add a new app to your stack. Deploy its container as usual.
  2. Add a block to services.yaml under the right group. Include container: <name> so the health dot lights up.
  3. (Optional) Add a widget. Grab the API key, add it to .env as HOMEPAGE_VAR_FOO_KEY, reference it in the widget block.
  4. Recreate the Homepage container if you added a new env var: docker compose up -d. For pure config file edits (no new env vars), just reload the browser.

Over time, services.yaml becomes a living inventory of your homelab — more accurate than any docs, because if a service isn’t on Homepage you won’t use it, and if Homepage says it’s red you’ll go fix it.

Blank page / “bad host” error. Add the hostname to HOMEPAGE_ALLOWED_HOSTS, recreate the container.

Widget shows “error”. Four usual suspects, in order:

  1. Wrong API key (swap and reload)
  2. widget.url is unreachable from inside the Homepage container — test with docker exec homepage wget -qO- http://sonarr:8989/api/v3/system/status?apikey=...
  3. Networks mismatch — the app and Homepage must share at least one Docker network
  4. App version too old for the widget (rare; check the Homepage widget docs for min versions)

Health dot is always red. Container name mismatch. docker ps shows the actual name; the YAML container: field must match exactly.

Config change doesn’t apply. Almost always a YAML parse error. docker logs homepage will show the line.

Icons showing as broken image. Either a typo in the icon name, or you’re offline — Homepage fetches many icons from a CDN by default. Self-host icons by dropping them in /config/icons/ and referencing them as /icons/myicon.png.