↓ Zum Hauptinhalt springen
  1. Beiträge/
  2. Kleines Homelab/
  3. k3s-prod: Kubernetes Cluster Konfiguration und Anwendungsbereitstellung/

Localkit

Inhaltsverzeichnis

Localkit
#

Localkit gibt sich gegenüber Petkit-Geräten als deren Cloud aus: Es beantwortet die HTTP-API unter den Petkit-Hostnamen und betreibt einen MQTT-Broker unter dem Aliyun-IoT-Hostnamen, mit dem die Geräte sonst sprechen. Die Geräte erscheinen über MQTT-Discovery in Home Assistant.

Unterstützt und getestet ist hier nur der Trinkbrunnen Eversweet Ultra (W7H).

Quelle
#

  • Localkit läuft aus dem eigenen Fork git.zyria.de/pyrox/localkit (Repository pyrox/localkit, Abschnitt “About this fork” in dessen README), der Broker aus ghcr.io/dwyschka/localkit-broker. Der Fork veröffentlicht Versionstags (X.Y.Z), gepinnt ohne Digest: Sein wöchentlicher Neubau pusht denselben Tag erneut, und die Forgejo-Registry löscht dabei das vorige Manifest. Ein gepinnter Digest lieferte danach 404 (21.10.2026, Pod in ImagePullBackOff). Den Broker gibt es nur als Branch-Tag main, er ist per Digest gepinnt. Renovate hebt beide.
  • Gründe für den Fork: Upstream kodiert bei jedem etwa 4 s langen Kamerasegment das ganze bisherige Event neu nach MP4, in der Anfrage des Geräts. Bei längeren Events wurde der Container deshalb mehrfach am Speicherlimit beendet (17.10.2026). Der Fork hängt Segmente nur noch an und kodiert einmal pro Event im Scheduler. Die Änderung ist Upstream als Pull Request angeboten. Außerdem baut der Fork sein Image seit dem 21.10.2026 selbst auf dem offiziellen PHP-Image: Upstreams Basisimage ghcr.io/dwyschka/localkit-docker (serversideup/php) wurde seit August nicht neu gebaut und blieb auf PHP 8.4.24, ohne die Fixes für PHP2Shell (CVE-2026-109618 und CVE-2026-109619, behoben in 8.4.27).
  • Rückweg: Erst wenn Upstream die Änderung übernommen hat und sein Image ein aktuelles PHP mitbringt. Dann main wieder als einen Container aus ghcr.io/dwyschka/localkit (Tag main mit aktuellem Digest) mit nginx und s6 betreiben, wie vor diesem Umbau (Git-Historie von values.yaml), und den Fork archivieren.
  • Gerätedateien: siehe device/.

Dokumentation
#

Aufbau
#

Datei Inhalt
ca.yaml Selbstsignierte CA localkit-ca und der Issuer, der damit signiert
certs.yaml Serverzertifikate für die Petkit-API und den MQTT-Broker, Let’s-Encrypt-Zertifikat für die Weboberfläche
values.yaml app-template von bjw-s mit drei Controllern: main (Localkit, siehe Container), broker (MQTT-Broker der Geräte) und storage (Garage als S3-Speicher für Bilder und Clips); dazu LoadBalancer, Ingress, Longhorn-Volumes und garage.toml
networkPolicy.yaml Lässt nur die Geräte, Traefik und die Pods untereinander zu
dns-records.yaml Öffentlicher Name der Weboberfläche über den Core-Networks-Operator, nötig für das Let’s-Encrypt-Zertifikat
kustomization.yaml Chart-Einbindung und die Secrets
device/ Patch, Build und Installation auf dem Gerät

Medien
#

Die Kamera lädt Ereignisbilder und Clips verschlüsselt zu Localkit hoch (LOCALKIT_STORAGE_ENDPOINT, die LoadBalancer-IP). Localkit legt sie in Garage ab, entschlüsselt sie und wandelt Clips in MP4 um. Zu sehen sind sie in der Weboberfläche unter System, Media und beim Gerät.

Aufnahmen (CLOUD_STORAGE) kommen in Segmenten von etwa 4 s. Localkit hängt jedes Segment sofort an den Stream des Events an. Das MP4 entsteht erst, wenn 60 s lang kein Segment mehr kam (LOCALKIT_CLOUD_STORAGE_ENCODE_DELAY); bis dahin wandelt die Weboberfläche den Stream beim Abspielen um.

Es gibt keinen Queue-Worker. Jobs laufen mit QUEUE_CONNECTION=sync in dem Prozess, der sie auslöst. Der Container scheduler führt schedule:work aus und damit jede Minute den MP4-Encode fertiger Aufnahmen und täglich das Löschen von Medien, die älter als LOCALKIT_MEDIA_RETENTION_DAYS (7) sind. Der Encode braucht deshalb Speicher im Container scheduler, nicht in php-fpm.

Garage legt beim ersten Start Layout, Bucket localkit und den Zugangsschlüssel aus den GARAGE_DEFAULT_*-Werten an (--single-node --default-bucket).

Localkit und Broker brauchen je eine eigene IP, weil die Geräte fest auf den Ports 80, 443 und 443 (MQTT über TLS) sprechen. Beide Pods sind per nodeSelector auf die Control-Plane-Nodes gelegt: MetalLB kündigt die LoadBalancer-IPs nur von dort an, und mit externalTrafficPolicy: Local verwirft ein Node ohne eigenen Endpunkt die Pakete. Auf einem Worker erreichen die Geräte Localkit sonst nicht (am 21.10.2026 nach dem Umzug von localkit-main auf worker2).

Container
#

Das Fork-Image enthält nur php-fpm und die App auf Basis des offiziellen PHP-Images mit fester Version (Abschnitt “Image layout” im README des Forks). Jeder Localkit-Prozess läuft als eigener Container im Pod localkit-main:

Container Aufgabe
init (Init-Container) kopiert public/ und die nginx-Konfiguration des Images in das emptyDir web und führt die Migrationen aus
main php-fpm, nur auf 127.0.0.1:9000
nginx nginx-unprivileged, HTTP auf 8080 und HTTPS auf 8443 mit localkit-api-tls, liefert public/ aus und reicht PHP per FastCGI weiter
listen php artisan localkit:listen, Gerätekommunikation über den Broker
homeassistant php artisan localkit:homeassistant, MQTT-Bridge zu Home Assistant
scheduler php artisan schedule:work, Encode und Aufräumen

Endet ein Prozess, startet Kubernetes nur seinen Container neu. Die Home-Assistant-Bridge veröffentlicht die MQTT-Discovery nur beim Start; nach einem neuen Gerät deshalb:

1kubectl -n localkit exec deploy/localkit-main -c homeassistant -- sh -c 'kill 1'

Die Seite System, Services der Weboberfläche ist ohne s6 ausgeblendet.

DNS-Umleitung
#

Die Geräte müssen die Petkit- und Aliyun-Namen auf diese IPs auflösen. Das erledigt nicht Pi-hole, sondern das UniFi-Gateway des Netzes, in dem die Geräte hängen, mit eigenen DNS-Einträgen (A-Records) außerhalb dieses Repositories:

Name Ziel
api-eu.petkt.com LoadBalancer-IP von localkit-main
api.eu-pet.com LoadBalancer-IP von localkit-main
<subdomain>.iot-as-mqtt.eu-central-1.aliyuncs.com LoadBalancer-IP von localkit-broker

<subdomain> ist die MQTT-Subdomain, die in Localkit beim Gerät eingetragen ist (hier localkit).

Dazu gehört eine Firewallregel auf dem Gateway: DNS (Port 53, UDP und TCP) von den Geräten nur zum Gateway selbst. Der W7H hat ab Werk einen öffentlichen DNS-Server als Ausweichserver eingetragen, mit dem er die Umleitung sonst umgehen würde.

Pi-hole bleibt bewusst außen vor: Dort würde die Umleitung für das ganze LAN gelten, die Petkit-App im Heimnetz von der Cloud abschneiden und Localkits eigenen Client für die Petkit-Cloud auf Localkit selbst umlenken, weil die Pods über Pi-hole auflösen.

Home Assistant bekommt die Geräte über den vorhandenen Mosquitto im Namespace home-assistant. Den Geräte-Broker kann Mosquitto nicht ersetzen: Er stellt Nachrichten wie Aliyun ohne Abo zu und fragt dafür Localkit nach den Topics eines Geräts.

Die CA
#

Die Geräte prüfen MQTT gegen eine Root-CA, die fest im Steuerprogramm ctrl steckt. device/patch-ctrl.py ersetzt sie durch localkit-ca. Daraus folgt:

  • Die CA darf sich nicht ändern. rotationPolicy: Never und eine Laufzeit von 20 Jahren verhindern das im Normalbetrieb. Wird das Secret localkit-ca gelöscht oder neu ausgestellt, muss ctrl auf jedem Gerät neu gepatcht und installiert werden.
  • Das Secret localkit-ca gehört in die Sicherung.
  • Die Serverzertifikate erneuert cert-manager selbst, Reloader startet die Pods danach neu.

Upstream liefert eigene Zertifikate samt privater Schlüssel im Repository und im Image aus. Diese App ersetzt sie vollständig.

Sicherheit
#

Der Broker nimmt jede MQTT-Anmeldung an, und Localkit meldet über BYPASS_AUTH jeden Aufruf als Benutzer 1 an. Geschützt wird beides allein durch die NetworkPolicy (Geräte-IPs) und für die Weboberfläche durch Authentik. Jedes neue Gerät braucht eine feste DHCP-Adresse und einen Eintrag in networkPolicy.yaml.

Auf dem Gerät selbst bleiben Telnet mit dem öffentlich bekannten root-Passwort sowie die go2rtc-Ports 1984, 8554 und 8555 ohne Anmeldung offen. Beides lässt sich nur auf der Netzwerkseite abschotten.

Einrichtung
#

  1. Die Secrets stehen per secretGenerator in kustomization.yaml: localkit-app (APP_KEY sowie LOCALKIT_S3_KEY und LOCALKIT_S3_SECRET) und localkit-storage (GARAGE_RPC_SECRET und dieselben S3-Werte als GARAGE_DEFAULT_*). Der im Image eingebaute APP_KEY ist für alle Installationen gleich und wird deshalb überschrieben. Die S3-Werte wirken nur beim ersten Start von Garage; danach müssen sie gleich bleiben.

  2. App ausrollen. Danach einen Benutzer anlegen, der als Benutzer 1 für BYPASS_AUTH dient:

    1kubectl -n localkit exec -it deploy/localkit-main -c main -- php artisan make:filament-user
  3. Gerät wie in device/README.md beschrieben umstellen.

  4. In Localkit beim Gerät die MQTT-Subdomain eintragen, die im UniFi-Gateway umgeleitet wird (siehe DNS-Umleitung).

Rückbau
#

Auf dem Gerät sh /opt/localkit/uninstall.sh ausführen und neu starten (siehe device/README.md), dann die DNS-Einträge und die Firewallregel im UniFi-Gateway entfernen. Das Gerät bootet danach wieder mit der Original-Firmware und spricht mit der Petkit-Cloud.

Fabrice Kirchner
Autor
Fabrice Kirchner
stolzer Vater, Nerd, Admin