Kubernetes controller that publishes ingress-backed .local hostnames through Avahi on every node. It is intended for home and lab networks where ingress hostnames like llm.local or vault.local should resolve to a node-local LAN address via mDNS.
- runs as a DaemonSet so every node advertises the same ingress-backed
.localnames - watches
networking.k8s.io/v1Ingress objects with read-only RBAC - filters
spec.rules[].hostvalues to names ending in.localby default - supports opt-out or opt-in annotation policy through
mdns.shipstuff.io/enabled - reuses host Avahi over host D-Bus when available
- falls back to a bundled
dbus-daemonplusavahi-daemonwhen host Avahi is unavailable - publishes each alias independently with
avahi-publish-address -R, so one collision does not block the full set - exposes
/healthz,/readyz,/metrics, and/stateon port8080
Use a Kubernetes context permitted to create the chart's ServiceAccount, DaemonSet, ClusterRole, and ClusterRoleBinding, and manage Helm release secrets in the target namespace. Nodes need a multicast-capable LAN interface and an ingress endpoint reachable at the advertised node address. The default chart uses host networking, host D-Bus access, and a root container with capabilities; namespace admission policies must allow these settings.
From this repo:
helm upgrade --install mdns-controller ./helm/mdns-controller \
--namespace kube-systemFrom the published OCI chart after a v* tag has been pushed and CI has published it:
helm upgrade --install mdns-controller oci://ghcr.io/shipstuff/charts/mdns-controller \
--version 0.1.1 \
--namespace kube-systemFor the servertimeai/home-lab defaults:
helm upgrade --install mdns-controller oci://ghcr.io/shipstuff/charts/mdns-controller \
--version 0.1.1 \
--namespace kube-system \
-f examples/values-servertimeai.yamlThe chart and image are public. If your Helm client reports a GHCR authentication
error, check for stale registry credentials; authenticated access can use
helm registry login ghcr.io with a token allowed to read the package.
An operator can bootstrap the release and grant a separate deployment identity permission to upgrade it. The chart's read-only Ingress RBAC is for the running controller, not for Helm or the CI runner. Apply deployment permissions outside CI; the runner should not be able to modify its own grants.
Upgrading this chart requires access to Helm release secrets and the existing DaemonSet, ServiceAccount, ClusterRole, and ClusterRoleBinding. Named-resource permissions can restrict updates, but cannot authorize creation of missing resources; rerun operator bootstrap if those resources are deleted. Helm's revisioned secret names also mean standard RBAC cannot restrict secret access by release label. Choose the release namespace and CI trust boundary accordingly.
Use the existing Helm release name and namespace when replacing an in-tree copy
of this chart. Check helm list -A and kubectl get daemonsets -A first. Remove
the old deployment workflow and any separate legacy publisher after confirming
the new controller is publishing, so both do not advertise the same aliases.
Do not remove host Avahi: the controller intentionally reuses it. Kubernetes
NodeLocal DNSCache (nodelocaldns) is unrelated and should also remain.
Check rollout with kubectl -n kube-system rollout status daemonset/mdns-controller.
A NotReady node can block full rollout even when the reachable nodes are healthy;
inspect node readiness and controller logs before diagnosing a chart failure.
Common chart values:
| Value | Default | Purpose |
|---|---|---|
image.repository |
ghcr.io/shipstuff/mdns-controller |
Controller image repository. |
image.tag |
0.1.1 |
Controller image tag. |
namespaceOverride |
kube-system |
Namespace used by rendered resources. |
hostAvahi.enabled |
true |
Reuse host Avahi through /run/dbus when available. |
bundledAvahi.enabled |
true |
Start bundled D-Bus and Avahi fallback when needed. |
ingress.hostSuffix |
.local |
Host suffix eligible for mDNS publication. |
ingress.annotationKey |
mdns.shipstuff.io/enabled |
Annotation key controlling inclusion. |
ingress.defaultEnabled |
true |
Include matching hosts unless annotated false; set false for opt-in. |
network.interface |
empty | Force the LAN interface used for advertisement. |
network.address |
empty | Force the IPv4 address registered for aliases. |
network.extraExcludeInterfaces |
empty | Extra comma-separated Avahi deny-interface patterns. |
health.reconcileInterval |
15s |
Kubernetes-to-Avahi reconcile interval. |
health.staleAfter |
60s |
Readiness staleness threshold after last successful reconcile. |
tolerations |
[{operator: Exists}] |
Default permits all taints; override to respect custom maintenance taints. |
Cordoning a node does not exclude a DaemonSet. To use a custom NoSchedule
maintenance taint, replace the default blanket toleration with only the taints
you intend to tolerate (for example, the control-plane NoSchedule taint).
Kubernetes adds standard DaemonSet node-health tolerations automatically. An
offline node cannot confirm termination of an old publisher even after its pod
is removed from desired placement.
When ingress.defaultEnabled=true, an ingress is included unless it has:
metadata:
annotations:
mdns.shipstuff.io/enabled: "false"When ingress.defaultEnabled=false, an ingress is excluded unless it has:
metadata:
annotations:
mdns.shipstuff.io/enabled: "true"mDNS is multicast, not authoritative DNS. It is limited to the local broadcast domain unless your network explicitly reflects mDNS between segments. .local names can also collide with other Bonjour/Avahi responders. The controller treats collisions as per-host publication failures and continues publishing the rest of the desired aliases.
Each node publishes aliases to its own LAN-facing IPv4 address. This is useful for home-network ingress, but it is not deterministic load balancing.
docker build -t ghcr.io/shipstuff/mdns-controller:dev .The Dockerfile builds the Go controller statically, then packages it with Avahi, D-Bus, iproute2, and the entrypoint script.
Release flow matches the windrose-self-hosted pattern:
scripts/release.sh 0.1.2
git push --follow-tags origin mainThe release script updates the chart version, chart appVersion, and default image tag, commits the bump, and creates an annotated vX.Y.Z tag. The tag push publishes:
ghcr.io/shipstuff/mdns-controller:X.Y.Zoci://ghcr.io/shipstuff/charts/mdns-controller:X.Y.Z