DEV Community

Cover image for Homepage: The Dashboard for All Your Self-Hosted Services
serverkueche.de
serverkueche.de

Posted on Originally published at serverkueche.de

Homepage: The Dashboard for All Your Self-Hosted Services

The more services you self-host, the more bookmarks and IP addresses you have to remember. A dashboard solves that: a single start page that links all your services, shows system load and even tells you which containers are running right now. Homepage is the nicest and fastest tool for it.

What are we building?

A central dashboard with Homepage (v2.2.0) behind Traefik, configured entirely through simple YAML files. By the end you'll have a tidy start page with tiles for all services, widgets for CPU/RAM/disk, a search box – and the killer feature: Homepage reads the Docker socket and shows live whether a container is running. Since version 2 Homepage also ships its own login, so the dashboard isn't sitting open on the internet (step 7). All statically generated, no database, with a tiny footprint.

Prerequisites

Step by step

Step 1: Create the project and config folder

Homepage is driven entirely by YAML files in a config folder. Create the structure:

mkdir -p /opt/homepage/config /opt/homepage/icons && cd /opt/homepage
Enter fullscreen mode Exit fullscreen mode

Step 2: Base settings

config/settings.yaml controls appearance and layout. We define two groups as a row layout with three columns each:

title: My Serverküche
theme: dark
color: slate
headerStyle: boxed
layout:
  Infrastructure:
    style: row
    columns: 3
  Media:
    style: row
    columns: 3
Enter fullscreen mode Exit fullscreen mode

The names under layout: (Infrastructure, Media) must later match the group names in the services file exactly – only then does the layout apply.

Step 3: Link your services

config/services.yaml is the heart – your tiles. Each service gets a name, a URL, a description and an icon (Homepage automatically pulls matching icons from a large catalog when you specify name.png):

- Infrastructure:
    - Nextcloud:
        href: https://cloud.YOUR_DOMAIN
        description: Your own cloud
        icon: nextcloud.png
    - Vaultwarden:
        href: https://vault.YOUR_DOMAIN
        description: Password manager
        icon: vaultwarden.png
- Media:
    - Immich:
        href: https://photos.YOUR_DOMAIN
        description: Photos
        icon: immich.png
    - Jellyfin:
        href: https://media.YOUR_DOMAIN
        description: Media server
        icon: jellyfin.png
Enter fullscreen mode Exit fullscreen mode

This is how you link your already-installed services like Nextcloud, Immich, Jellyfin or Vaultwarden in one central place.

Step 4: Widgets for system load and search

config/widgets.yaml controls the info bar at the top. Resource display, a search box and date/time:

- resources:
    cpu: true
    memory: true
    disk: /
- search:
    provider: duckduckgo
    target: _blank
- datetime:
    text_size: xl
    format:
      dateStyle: full
      timeStyle: short
Enter fullscreen mode Exit fullscreen mode

The resources widget shows the host's utilization. Homepage reads CPU and RAM straight from within the container (no Docker socket needed); we only wire up the socket in the next step for the container status.

Step 5: Connect Traefik – and the crucial host protection

Now the compose.yaml. Replace YOUR_DOMAIN with your real subdomain:

services:
  homepage:
    image: ghcr.io/gethomepage/homepage:v2.2.0
    restart: unless-stopped
    environment:
      HOMEPAGE_ALLOWED_HOSTS: YOUR_DOMAIN
    volumes:
      - ./config:/app/config
      - ./icons:/app/public/icons
    networks: [proxy]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.homepage.rule=Host(`YOUR_DOMAIN`)"
      - "traefik.http.routers.homepage.entrypoints=websecure"
      - "traefik.http.routers.homepage.tls.certresolver=le"
      - "traefik.http.services.homepage.loadbalancer.server.port=3000"

networks:
  proxy:
    external: true
Enter fullscreen mode Exit fullscreen mode

⚠️ HOMEPAGE_ALLOWED_HOSTS is mandatory

Since version 1.x, Homepage refuses to answer requests under an unknown hostname for security reasons (protection against DNS rebinding). Without the environment variable HOMEPAGE_ALLOWED_HOSTS set to your domain you get an HTTP 400 with {"error":"Host validation failed. See logs for more details."} instead of the dashboard. Enter your domain there exactly – multiple ones separated by commas.

Start the dashboard:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Verify from outside that it responds with valid HTTPS:

curl -sI https://YOUR_DOMAIN/ | head -1
Enter fullscreen mode Exit fullscreen mode
HTTP/2 200
Enter fullscreen mode Exit fullscreen mode

In the browser you now see your dashboard with tiles and the info bar:

The Homepage dashboard with groups for infrastructure and media, resource widgets and a search box

Step 6: Live container status via the Docker socket

The strongest feature: Homepage can read directly from Docker whether a container is running, and show CPU/RAM per container. For that create config/docker.yaml:

my-docker:
  socket: /var/run/docker.sock
Enter fullscreen mode Exit fullscreen mode

Then mount the socket read-only into the container – add under volumes: in compose.yaml:

      - /var/run/docker.sock:/var/run/docker.sock:ro
Enter fullscreen mode Exit fullscreen mode

And link a tile to its container (server refers to the name from docker.yaml, container to the real container name):

- Infrastructure:
    - Traefik:
        description: Reverse proxy
        icon: traefik.png
        server: my-docker
        container: traefik-traefik-1
Enter fullscreen mode Exit fullscreen mode

After docker compose up -d the tile shows a green status badge as soon as the container is running:

The dashboard with a Traefik tile that shows the status RUNNING live from Docker

⚠️ The Docker socket is powerful – mount it read-only

Access to /var/run/docker.sock is effectively root access to the host (see Users & permissions). Always mount it with :ro (read only) and don't expose Homepage unprotected on the internet. From here on it needs a login in front of it – that's what the next step builds.

Step 7: Protect the dashboard with a password

Up to version 1 Homepage had no login of its own; you had to put a BasicAuth middleware in Traefik in front of it. Since version 2 Homepage ships a login. For a dashboard that reads the Docker socket, it's mandatory.

The secret and the password don't belong in compose.yaml but in an .env next to it. First generate a random secret – Homepage signs the session cookie with it, and it needs at least 32 characters:

echo "HOMEPAGE_AUTH_SECRET=$(openssl rand -base64 32)" > .env
Enter fullscreen mode Exit fullscreen mode

Append your password and take read access away from everyone else. Replace YOUR_DASHBOARD_PASSWORD with a long one you use nowhere else:

echo "HOMEPAGE_AUTH_PASSWORD=YOUR_DASHBOARD_PASSWORD" >> .env
chmod 600 .env
Enter fullscreen mode Exit fullscreen mode

Add these four lines under environment: in compose.yaml. HOMEPAGE_EXTERNAL_URL is the address you open the dashboard at in the browser; Compose pulls the two ${…} from the .env automatically, so the secrets stay out of the compose file:

      HOMEPAGE_AUTH_ENABLED: "true"
      HOMEPAGE_EXTERNAL_URL: https://YOUR_DOMAIN
      HOMEPAGE_AUTH_SECRET: ${HOMEPAGE_AUTH_SECRET}
      HOMEPAGE_AUTH_PASSWORD: ${HOMEPAGE_AUTH_PASSWORD}
Enter fullscreen mode Exit fullscreen mode

Restart:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Every request now lands on the sign-in page first. From outside you see it because / no longer answers with 200 but redirects:

curl -sI https://YOUR_DOMAIN/ | head -1
Enter fullscreen mode Exit fullscreen mode
HTTP/2 307
Enter fullscreen mode Exit fullscreen mode

In the browser the sign-in form appears – with the title from your settings.yaml:

The Homepage sign-in page with the dashboard title and a single password field

There is no username, just this one password.

⚠️ Homepage does not rate limit attacks itself

Failed sign-in attempts run through unthrottled – Homepage has no rate limit of its own. If the dashboard is open on the internet, put a rate limit in front of the login route /api/auth/callback/credentials, e.g. with Traefik's rateLimit middleware (Tutorial expected in October) or fail2ban. And the .env with secret and password belongs in no Git repository.

Step 8: Add bookmarks (optional)

Besides services, Homepage can show plain bookmarks – via config/bookmarks.yaml:

- Development:
    - Serverküche:
        - abbr: SK
          href: https://serverkueche.de
Enter fullscreen mode Exit fullscreen mode

Now you have services, system load, search and bookmarks in one place. Homepage picks up changes to the YAML files automatically – a browser reload is usually enough, no restart needed.

When things go wrong

"Host validation failed" instead of the dashboard. The most common trap: HOMEPAGE_ALLOWED_HOSTS is missing or has the wrong domain. Enter exactly the domain you access it under, then docker compose up -d.

The resource widgets show no or wrong values. Homepage reads CPU and RAM from within the container (no socket needed). If disk: / shows the container filesystem instead of the host disk, mount the desired path as an additional volume (e.g. - /:/host:ro and disk: /host).

An icon doesn't load. The name doesn't match the icon catalog. Write the service name lowercase and without spaces (nextcloud.png), or place a custom image in the icons folder (mounted to /app/public/icons) and reference it as /icons/name.png.

The container tile shows no status. The value at container: must be the exact container name. Find it with docker ps --format '{{.Names}}' – with Compose it's usually folder-service-1 (e.g. traefik-traefik-1).

Changes to the YAML files don't take effect. A syntax error aborts the parsing. YAML is indentation-sensitive – check the logs with docker compose logs homepage for error lines and use consistent spaces (no tabs).

You get "Authentication error – Auth is disabled or misconfigured" instead of the sign-in form. Then HOMEPAGE_AUTH_ENABLED is set but the secret or the password arrives empty in the container – usually because the .env sits in the wrong folder or is missing a line. The log says Password auth is enabled but required settings are missing; docker compose config shows you which values Compose actually substitutes.

Maintenance & backups

  • Updates are low-risk. Homepage has no persistent state except your config files. Occasionally bump the image tag (ghcr.io/gethomepage/homepage:v2.2.0) to the new version and docker compose up -d; otherwise your normal update process handles it. After larger jumps check the release notes, as config options can change. Going from 1.x to 2.x, settings.yaml, services.yaml, widgets.yaml, bookmarks.yaml and docker.yaml all stay valid unchanged – the only new part is the login from step 7.
  • The backup is tiny. Back up the config folder (and any custom icons) – that's your entire configuration. It belongs in your Restic backup; the application itself is recreated from the image any time. Keep the .env with secret and password separate from it, for example in your password manager – if the secret is lost, only the open sessions become invalid and you simply sign in again.
  • Don't forget security. A dashboard with Docker insight is a worthwhile target. Run it only behind HTTPS (Traefik handles that), keep the login from step 7 enabled, and rotate the password if you ever shared it over an insecure channel.

This post first appeared on serverkueche.de.

Top comments (0)