Zum Inhalt

Zero-Backend Service-Portal: Homer Statisches PWA-Dashboard, Live API-Widgets & Subresource-Integrität

Erstellt: 06.11.2026 Aktualisiert: 06.11.2026

Management Summary: Viele moderne Dashboards erfordern ressourcenintensive Node.js- oder Python-Hintergrunddienste mit privilegiertem Docker-Socket-Zugriff und speichern sensible API-Tokens serverseitig in Datenbanken. Dieser Leitfaden beschreibt das Deployment von Homer – einer völlig zustandslosen, statischen Vue.js Single Page Application (SPA) und Progressive Web App (PWA). Homer wird über eine einzige deklarative config.yml gesteuert, bietet dynamische Client-seitige Telemetrie-Karten (Pi-hole, AdGuard Home, Uptime Kuma, Prometheus) und wird durch eine strikte Content-Security-Policy (CSP) abgesichert.

  • Minimale Angriffsfläche ohne Backend


    Liefert reine statische HTML/JS/CSS-Dateien bei < 15 MB RAM-Bedarf aus – ganz ohne serverseitige Codeausführung oder Docker-Socket-Mounts.

  • Dynamische Smart-Service-Cards


    Zeigt geblockte DNS-Anfragen, Prometheus-Messwerte, Uptime Kuma SLAs und Proxmox-Zustände in Echtzeit direkt auf den Service-Kacheln an.

  • Tastaturfokussierte Navigation


    Mit / blitzschnell nach Diensten filtern, mit Enter den ersten Treffer öffnen und mit t zwischen hellem und dunklem Design umschalten.

  • Offline-Fähiges PWA-Manifest


    Cacht config.yml und SVG-Iconsets über einen Browser Service Worker, sodass das Infrastrukturportal selbst bei partiellen Netzwerkausfällen lädt.


1. Architektur: Statische Bereitstellung vs. Client-seitige Smart-Cards

Im Gegensatz zu servergerenderten Dashboards, die API-Anfragen serverseitig weiterleiten, wird Homer von einem minimalen Webserver als statisches Dateipaket ausgeliefert. Beim Aufruf von https://portal.lan.nejat-hakan.de lädt die Vue.js-Applikation /assets/config.yml und führt fetch()-Anfragen direkt aus dem Browser des Administrators (über LAN oder WireGuard VPN) an die jeweiligen Dienst-APIs aus:

%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
    subgraph ClientBrowser["Administrator-Browser (LAN / WireGuard)"]
        SPA["Homer Vue.js PWA<br/>Tastatursuche ('/')"]
        SW["Service Worker Cache<br/>Offline Icon- & Konfig-Speicher"]
    end

    subgraph StaticHost["Homer Statischer Container (<15MB RAM)"]
        Assets["Statisches Bundle +<br/>/assets/config.yml"]
    end

    subgraph InternalAPIs["Interne Service-APIs (CORS erlaubt)"]
        PiholeAPI["Pi-hole v6 / AdGuard<br/>Live Blockrate in %"]
        PromAPI["Prometheus /api/v1/query<br/>CPU- & Storage-Nutzung"]
        KumaAPI["Uptime Kuma Status<br/>Public Slug Badge"]
    end

    SPA -->|"1. GET /assets/config.yml"| Assets
    Assets --> SW
    SPA -->|"2. Client-Side Fetch"| PiholeAPI
    SPA -->|"3. Client-Side Fetch"| PromAPI
    SPA -->|"4. Client-Side Fetch"| KumaAPI

2. Schreibgeschütztes Container-Deployment

Da Homer nach dem Bereitstellen der config.yml keinerlei Schreibzugriffe benötigt, wird der Container mit read_only: true und no-new-privileges:true gehärtet:

/opt/homer/compose.yaml
services:
  homer:
    image: b4bz/homer:latest
    container_name: homer-portal
    restart: unless-stopped
    user: "1000:1000"
    read_only: true
    security_opt:
      - no-new-privileges:true
    ports:
      - "127.0.0.1:8090:8080/tcp"
    environment:
      INIT_ASSETS: 0
    volumes:
      - ./assets:/www/assets:ro

Legen Sie das Verzeichnis ./assets/icons an und befüllen Sie es mit lokalen SVG-Icons (z. B. von selfh.st/icons), um externe CDN-Aufrufe vollständig zu vermeiden:

Lokale SVG-Icons bereitstellen
mkdir -p /opt/homer/assets/icons
chown -R 1000:1000 /opt/homer/assets

3. Deklarative Produktivkonfiguration (assets/config.yml)

/opt/homer/assets/config.yml
---
title: "Souveränes Infrastruktur-Portal"
subtitle: "nejat-hakan.de • Systembetrieb & Telemetrie"
documentTitle: "Ops Portal | nejat-hakan.de"
header: true
footer: '<p>Souveräner Self-Hosted Stack • <code>/</code> zum Suchen • <code>t</code> für Theme</p>'

columns: "3"
connectivityCheck: true

defaults:
  layout: list
  colorTheme: dark

theme: default
colors:
  dark:
    highlight-primary: "#1e293b"
    highlight-secondary: "#0f172a"
    highlight-hover: "#334155"
    background: "#090d16"
    card-background: "#111827"
    text: "#f8fafc"
    text-header: "#f8fafc"
    text-title: "#e2e8f0"
    text-subtitle: "#94a3b8"
    card-shadow: rgba(0, 0, 0, 0.5)
    link: "#38bdf8"
    link-hover: "#7dd3fc"

links:
  - name: "Dokumentation"
    icon: "fas fa-book"
    url: "https://nejat-hakan.de"
    target: "_blank"
  - name: "Git Repositories"
    icon: "fas fa-code-branch"
    url: "https://git.nejat-hakan.de"
  - name: "Status-Seite"
    icon: "fas fa-heart-pulse"
    url: "https://status.nejat-hakan.de/status/infra"

services:
  - name: "Netzwerk & Edge-Sicherheit"
    icon: "fas fa-shield-halved"
    items:
      - name: "AdGuard Home / Pi-hole"
        logo: "assets/icons/pihole.svg"
        subtitle: "DNS Sinkhole & Unbound Resolver"
        tag: "dns"
        tagstyle: "is-success"
        url: "https://dns.lan.nejat-hakan.de"
        type: "PiHole"
        apiType: "pihole6"

      - name: "Traefik Edge Router"
        logo: "assets/icons/traefik.svg"
        subtitle: "Ingress TLS 1.3 + HTTP/3"
        tag: "edge"
        tagstyle: "is-info"
        url: "https://traefik.lan.nejat-hakan.de"
        type: "Traefik"

      - name: "WireGuard VPN"
        logo: "assets/icons/wireguard.svg"
        subtitle: "10.88.0.1/24 Mesh Gateway"
        tag: "vpn"
        tagstyle: "is-warning"
        url: "https://vpn.lan.nejat-hakan.de"

  - name: "Beobachtbarkeit & Speicher"
    icon: "fas fa-server"
    items:
      - name: "Uptime Kuma"
        logo: "assets/icons/uptime-kuma.svg"
        subtitle: "Synthetische Verfügbarkeit & Push-Probes"
        tag: "sla"
        tagstyle: "is-success"
        url: "https://status.nejat-hakan.de"
        type: "UptimeKuma"
        slug: "infra"

      - name: "Zabbix 7 LTS"
        logo: "assets/icons/zabbix.svg"
        subtitle: "TimescaleDB Hardware- & SMART-Telemetrie"
        tag: "metrics"
        tagstyle: "is-danger"
        url: "https://zabbix.lan.nejat-hakan.de"

      - name: "Proxmox VE Cluster"
        logo: "assets/icons/proxmox.svg"
        subtitle: "KVM- & ZFS-Virtualisierungshost"
        tag: "hv-01"
        tagstyle: "is-primary"
        url: "https://proxmox.lan.nejat-hakan.de"

  - name: "Souveräne Produktivität"
    icon: "fas fa-cubes"
    items:
      - name: "Vaultwarden"
        logo: "assets/icons/vaultwarden.svg"
        subtitle: "Zero-Knowledge Passwort-Tresor"
        tag: "sec"
        tagstyle: "is-success"
        url: "https://vault.nejat-hakan.de"

      - name: "Nextcloud Hub"
        logo: "assets/icons/nextcloud.svg"
        subtitle: "Cloud-Sync, CalDAV & Collabora"
        tag: "cloud"
        tagstyle: "is-info"
        url: "https://cloud.nejat-hakan.de"

      - name: "Forgejo Git"
        logo: "assets/icons/forgejo.svg"
        subtitle: "Self-Hosted Git & Actions Runner"
        tag: "git"
        tagstyle: "is-warning"
        url: "https://git.nejat-hakan.de"

4. CORS & Reverse-Proxy CSP für Smart-Cards konfigurieren

Da Smart-Cards Browser-fetch()-Aufrufe von Homers Origin an Ziel-APIs initiieren, müssen die Zielsysteme CORS (Access-Control-Allow-Origin) gestatten und Homers CSP-Header connect-src zulassen:

Reverse-Proxy CSP-Snippet für Homer
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://*.nejat-hakan.de https://*.lan.nejat-hakan.de;" always;

Niemals sensible Admin-API-Keys in öffentlich lesbare config.yml schreiben

Da /assets/config.yml von jedem Browser heruntergeladen werden kann, sind eingetragene API-Schlüssel in den Entwicklertools sichtbar. Nutzen Sie ausschließlich Leseschlüssel oder Status-Slugs (slug: "infra") und sichern Sie das Dashboard über VPN, mTLS oder ForwardAuth ab.


5. Verifikation & YAML-Validierung

Homer YAML-Syntax & lokale Icons validieren
# 1. Strenge YAML-Syntax prüfen
python3 -c 'import yaml, sys; yaml.safe_load(open("/opt/homer/assets/config.yml"))' && echo "YAML Valid"

# 2. Prüfen, ob alle referenzierten Icons lokal vorhanden sind
grep -oP 'assets/icons/[^"]+' /opt/homer/assets/config.yml | while read -r icon; do
    test -f "/opt/homer/${icon}" || echo "FEHLENDES ICON: ${icon}"
done