Homepage dashboard

Když se vám na domácím serveru začnou kupit služby jako Jellyfin, Kavita, Audiobookshelf a lokální LLM přes OpenWebUI, dříve nebo později zjistíte, že potřebujete centrální rozcestník. Na Zalmanovi toho běží už docela dost a pamatovat si všechny porty přestává být reálné (alespoň pro Eriku). Rozhodl jsem se nasadit aplikaci Homepage. Je rychlá, moderní a hlavně kompletně se řídí přes YAML soubory. Žádné klikací GUI, zkrátka ideální adept pro plnou automatizaci přes Ansible.

Tady je shrnutí toho, jak jsem postupoval, a hlavně řešení několika zajímavých pastí, na které jsem cestou narazil.

Prvotní nasazení a ochrana hostitele

Jelikož vše nasazuji přes Ansible, recykloval jsem už existující šablonu pro systemd službu a Docker Compose. Oproti aplikacím jako Audiobookshelf jsem mohl z playbooku vyhodit připojování sdílených SMB složek. Homepage potřebuje pouze lokální adresář pro konfiguraci.

Mapování SMB disků (pouze pro čtení) do kontejneru vracím, abych mohl v dashboardu zobrazit, kolik místa na discích zbývá a kolik je obsazeno.

První zádrhel přišel hned po startu kontejneru. Místo hezkého webu na mě vyskočila chyba Host validation failed. Ukázalo se, že autoři Homepage nedávno přidali bezpečnostní ochranu proti podvržení hlavičky hostitele. Kontejner zkrátka ignoruje požadavky, pokud mu explicitně neřeknete, na jaké IP adrese nebo doméně reálně poslouchá. Stačilo do docker-compose.yml přidat proměnnou prostředí:

# Add explicitly allowed IPs and hostnames
environment:
  - HOMEPAGE_ALLOWED_HOSTS=192.168.1.101:3000,zalman:3000

Boj s oprávněními a past jménem docker.sock

Homepage umí u každé služby zobrazit, jestli kontejner běží a kolik žere paměti či procesoru. K tomu ale potřebuje přístup k Docker daemonu. Připojil jsem tedy do kontejneru /var/run/docker.sock, ale aplikace neustále házela chybu EACCES.

Problém je v právech. Homepage z bezpečnostních důvodů startuje jako neprivilegovaný uživatel (v mém případě přes proměnné PUID=1108 a PGID=2000). Zjistil jsem si tedy přes stat -c '%g' /var/run/docker.sock, že na hostiteli má skupina docker ID 103, a přidal jsem do compose souboru direktivu group_add: ["103"].

Tohle normálně funguje, ale ne tady. Startovací skript v kontejneru totiž nejprve jako root srovná oprávnění lokálních složek a pak přes su-exec zahodí práva a přepne se pod UID 1108. Během tohoto zahození práv ale proces ztratí dodatečné skupiny definované přes group_add. Z pohledu bezpečnosti je nesmysl dělat na socketu hostitele chmod 666, takže jsem sáhl po čistším sysadmin řešení – Docker Socket Proxy.

Přidal jsem do stacku malý proxy kontejner, který jako jediný čte socket a dovnitř interní Docker sítě ho vystavuje jako read-only API na portu 2375.

# Secure way to access Docker API without messing with permissions
services:
  docker-proxy:
    image: tecnativa/docker-socket-proxy:latest
    container_name: homepage-docker-proxy
    environment:
      - CONTAINERS=1
      - POST=0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

V konfiguraci Homepage (docker.yaml) pak stačilo nastavit napojení na proxy a bylo po problému.

---
docker-runner:
  host: docker-proxy
  port: 2375

Záludnosti striktního YAML parseru

Když se řeší Infrastructure as Code, člověk občas narazí na to, jak striktní některé parsery umí být. Při definování služeb v services.yaml mi Homepage několik položek s chybou Error parsing service úplně zahodila a vůbec nevykreslila.

Důvody byly dva:

  1. Odsazení: Vlastnosti jako icon nebo href nesmí být na stejné úrovni jako název služby, jinak si YAML parser myslí, že je uzel prázdný.
  2. Ping s portem: U OpenWebUI jsem chtěl testovat dostupnost přes parametr ping. Napsal jsem tam 192.168.1.84:3000. Jenže ping na pozadí volá ICMP, které logicky žádné porty nezná, takže dotaz selhal. Bylo potřeba vynutit HTTP vrstvu přidáním protokolu na začátek ([http://192.168.1.84:3000](http://192.168.1.84:3000)).

Menší tip pro ikony. Místo toho, abych stahoval PNG obrázky log (třeba pro přístupový bod T-Mobile) a mapoval je do kontejneru, využil jsem nativní podporu Simple Icons. Stačí do konfigurace zadat icon: si-tmobile a Homepage si příslušné SVG logo dotáhne sama.

Když Proxmox a Jellyfin API mlčí

Chtěl jsem mít v dashboardu nejen odkazy, ale i živá data (API widgety).

U Jellyfinu mi dashboard začal chrlit chyby HTTP 404. Zjistil jsem, že Jellyfin od verze 10.9.0 trvale zařízl staré zpětně kompatibilní /emby/ endpointy, které Homepage ve výchozím stavu používá. Stačilo do konfigurace widgetu přidat řádek version: 2, aby aplikace přešla na nové API.

Větší záhadou byl Proxmox. Widget do něj sice viděl, ale sveřepě ukazoval zátěž NaN% a 0 VMS. První chyba byla, že jsem mu neřekl, jaký fyzický server v clusteru má číst (nutnost přidat node: zalman). Druhá past se ukrývala přímo v Proxmox GUI. Když vytváříte API token pro externí aplikaci a necháte aktivní volbu “Privilege Separation”, token ve výchozím stavu nemá vůbec žádná oprávnění. Proxmox neumožňuje přiřadit roli přímo při tvorbě tokenu. Musíte jít do sekce Datacenter -> Permissions, přidat nové pravidlo na cestu /, vybrat váš token a explicitně mu přidělit roli PVEAuditor (pouze pro čtení).

Po uložení se rázem načetly reálné statistiky CPU a RAM celého serveru.

Ukázka services.yaml:

---
- Infrastructure:
  - Proxmox:
      icon: proxmox.png
      href: https://<IP>:8006
      description: Virtualizace a kontejnery
      ping: <IP>
      widget:
        type: proxmox
        url: https://<IP>:8006
        username: root@pam!Homepage
        password: <API_TOKEN>
        node: zalman
        ignoreTls: true

  - "T-mobile router":
      icon: si-tmobile
      href: https://192.168.1.1
      description: T-mobile pristupovy bod
      ping: 192.168.1.1

- Media:
  - Audiobookshelf:
      icon: audiobookshelf.png
      href: http://<IP>:13378
      description: Audioknihy a podcasty
      server: docker-runner
      container: audiobookshelf
      widget:
        type: audiobookshelf
        url: http://<IP>:13378
        key: <API_KEY>

Záhlaví na stránce

Soubor widget.yaml definuje co se má zobrazit v horní části stránky.

---
- resources:
    label: Docker runner
    cpu: true
    memory: true
    disk: /
    units: metric

- resources:
    label: Data pool
    expanded: true
    disk:
      - /mnt/data

- resources:
    label: Movies pool
    expanded: true
    disk:
      - /mnt/movies

Zde jsem narazil na chybějící mounty SMB disků, které se nepropagují do kontejneru. V Ansible roli jsem je tedy přidal přes volumes a v widget.yaml pak použil cestu /mnt/data a /mnt/movies.

Závěr

Obalit celou konfiguraci do Ansible repozitáře dalo trochu práce, hlavně kvůli ladění správného přístupu na Docker socket a pochopení specifik jednotlivých API. Výsledek za to ale stojí. Mám teď plně automatizovaný rozcestník, který nejenže dobře vypadá, ale rovnou mi ukazuje aktivní streamy, počty knih i zdraví celé infrastruktury, aniž bych musel ručně spravovat jediný soubor přímo na serveru.