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(Repositorypyrox/localkit, Abschnitt “About this fork” in dessen README), der Broker ausghcr.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 inImagePullBackOff). Den Broker gibt es nur als Branch-Tagmain, 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
mainwieder als einen Container ausghcr.io/dwyschka/localkit(Tagmainmit aktuellem Digest) mit nginx und s6 betreiben, wie vor diesem Umbau (Git-Historie vonvalues.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: Neverund eine Laufzeit von 20 Jahren verhindern das im Normalbetrieb. Wird das Secretlocalkit-cagelöscht oder neu ausgestellt, mussctrlauf jedem Gerät neu gepatcht und installiert werden. - Das Secret
localkit-cagehö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 #
-
Die Secrets stehen per
secretGeneratorinkustomization.yaml:localkit-app(APP_KEYsowieLOCALKIT_S3_KEYundLOCALKIT_S3_SECRET) undlocalkit-storage(GARAGE_RPC_SECRETund dieselben S3-Werte alsGARAGE_DEFAULT_*). Der im Image eingebauteAPP_KEYist 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. -
App ausrollen. Danach einen Benutzer anlegen, der als Benutzer 1 für
BYPASS_AUTHdient:1kubectl -n localkit exec -it deploy/localkit-main -c main -- php artisan make:filament-user -
Gerät wie in
device/README.mdbeschrieben umstellen. -
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.