Zum Inhalt

Zero-Backend Service Portal: Homer Static PWA Dashboard, Live API Widgets & Subresource Integrity

Executive Summary: Many modern self-hosted dashboards run heavy Node.js or Python backend daemons that require privileged Docker socket access and store sensitive API tokens inside server-side databases. This architectural guide details the deployment of Homer—a zero-dependency, static client-side Vue.js Single Page Application (SPA) and Progressive Web App (PWA)—configured via a single declarative config.yml, enriched with live client-side telemetry cards (Pi-hole, AdGuard Home, Nextcloud, Uptime Kuma, Prometheus), and hardened with strict Content-Security-Policy (CSP) and keyboard-driven navigation.

  • Zero Server-Side Attack Surface


    Serve static HTML/JS/CSS assets in < 15 MB of RAM with zero server-side code execution, database engines, or Docker socket mounts.

  • Live Smart Service Cards


    Render real-time DNS block percentages, Prometheus query gauges, Uptime Kuma SLA badges, and Proxmox node status directly inside service tiles.

  • Keyboard-First Command Palette


    Press / to filter services instantaneously, Enter to launch the top match, and t to toggle Catppuccin/Nord dark and light themes.

  • Offline-Capable PWA Manifest


    Cache config.yml and local SVG icon packs via a browser Service Worker so your infrastructure directory loads even during partial LAN outages.


1. Architecture: Static Asset Delivery vs. Client-Side Smart Cards

Unlike server-rendered dashboards (Homepage, Dashy, Heimdall) that proxy API requests through a backend container, Homer is served as static files by a minimal lighttpd or nginx container. When your browser loads https://portal.lan.nejat-hakan.de, the Vue.js SPA fetches /assets/config.yml and executes asynchronous fetch() calls directly from the user's browser (over your LAN or WireGuard VPN) to internal service APIs:

%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
    subgraph ClientBrowser["Engineer Browser (LAN / WireGuard VPN)"]
        SPA["Homer Vue.js PWA<br/>Keyboard Search ('/')"]
        SW["Service Worker Cache<br/>Offline Icon & Config Store"]
    end

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

    subgraph InternalAPIs["Internal Infrastructure APIs (CORS Permitted)"]
        PiholeAPI["Pi-hole v6 / AdGuard<br/>Live Blocked %"]
        PromAPI["Prometheus /api/v1/query<br/>CPU & ZFS Pool Usage"]
        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. Hardened Read-Only Container Deployment

Because Homer never needs to mutate files at runtime once config.yml is written on the host, deploy the container with read_only: true, no-new-privileges:true, and INIT_ASSETS: 0:

/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

Create the local ./assets/icons directory and populate it with self-hosted SVG icons from selfh.st/icons so your browser never makes external requests to third-party CDNs:

Bootstrapping Local Self-Hosted SVG Icons
mkdir -p /opt/homer/assets/icons
chown -R 1000:1000 /opt/homer/assets

3. Production Declarative Configuration (assets/config.yml)

Below is a curated, high-contrast dark-slate config.yml implementing grouped infrastructure columns, custom search engines, keyboard-shortcut tags, and live smart cards (type: "PiHole", type: "UptimeKuma", type: "Prometheus"):

/opt/homer/assets/config.yml
---
title: "Sovereign Infrastructure Portal"
subtitle: "nejat-hakan.de • Core Operations & Telemetry"
documentTitle: "Ops Portal | nejat-hakan.de"
header: true
footer: '<p>Sovereign Self-Hosted Stack • Press <code>/</code> to search • Press <code>t</code> for 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: "Documentation"
    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 Page"
    icon: "fas fa-heart-pulse"
    url: "https://status.nejat-hakan.de/status/infra"

services:
  - name: "Network & Edge Security"
    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: "Observability & Storage"
    icon: "fas fa-server"
    items:
      - name: "Uptime Kuma"
        logo: "assets/icons/uptime-kuma.svg"
        subtitle: "Synthetic Availability & 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 Telemetry"
        tag: "metrics"
        tagstyle: "is-danger"
        url: "https://zabbix.lan.nejat-hakan.de"

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

  - name: "Sovereign Productivity"
    icon: "fas fa-cubes"
    items:
      - name: "Vaultwarden"
        logo: "assets/icons/vaultwarden.svg"
        subtitle: "Zero-Knowledge Credential Vault"
        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. Configuring CORS & Reverse-Proxy CSP Headers for Smart Cards

Because Smart Cards (type: "PiHole", type: "UptimeKuma", type: "Traefik") execute browser fetch() requests from https://portal.lan.nejat-hakan.de to https://status.nejat-hakan.de, the target services must permit Cross-Origin Resource Sharing (Access-Control-Allow-Origin) and your reverse proxy's Content-Security-Policy header on Homer must allow connect-src:

Reverse Proxy Security & CORS Snippet for 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;

Never Embed Admin API Keys in Publicly Accessible config.yml

Because /assets/config.yml is downloaded directly by every visitor's browser, any apikey: field placed inside a smart card configuration is visible in the browser's DevTools Network tab. Only use read-only tokens, public status slugs (slug: "infra"), or protect the entire Homer portal behind WireGuard / mTLS / Authelia ForwardAuth.


5. Verification & YAML Validation

Validate config.yml syntax before refreshing the browser (note: if the PWA Service Worker caches an older version of config.yml, perform a hard reload via Ctrl+Shift+R):

Validating Homer YAML Syntax & Asset Availability
# 1. Verify strict YAML syntax using python3-yaml or yq
python3 -c 'import yaml, sys; yaml.safe_load(open("/opt/homer/assets/config.yml"))' && echo "YAML Valid"

# 2. Verify every referenced local icon exists on disk
grep -oP 'assets/icons/[^"]+' /opt/homer/assets/config.yml | while read -r icon; do
    test -f "/opt/homer/${icon}" || echo "MISSING ICON: ${icon}"
done