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 MBof 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,Enterto launch the top match, andtto toggle Catppuccin/Nord dark and light themes. -
Offline-Capable PWA Manifest
Cache
config.ymland 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:
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:
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"):
---
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:
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):
# 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