Run on Kubernetes¶
A kustomize base for an indah app, with one overlay per ingress, written for k3s first. The files are in
deploy/recipes/k8s/ in the repository.
The pieces¶
| File | What it sets |
|---|---|
base/deployment.yaml |
readiness on /ready, liveness on /health, a 5 s preStop, a 30 s grace period, a non-root user, INDAH_NODE from the pod name |
base/configmap.yaml |
INDAH_TRUSTED_PROXIES, INDAH_NODE_HEADER, INDAH_DRAIN_SECONDS |
base/service.yaml, base/pdb.yaml |
a ClusterIP Service and a PodDisruptionBudget (maxUnavailable: 1) |
overlays/traefik/ |
the sticky cookie, for k3s's default ingress |
overlays/ingress-nginx/ |
cookie affinity for ingress-nginx |
overlays/plain/ |
any ingress, no affinity |
Deploy on k3s¶
deploy/recipes/build-image.sh my-registry/indah-app:1 # or your own image; see the Dockerfile
# edit overlays/traefik/ingress.yaml (your host) and base/kustomization.yaml (image, replicas)
kubectl apply -k deploy/recipes/k8s/overlays/traefik
kubectl -n indah create secret generic indah-secrets \
--from-literal=INDAH_DOCTOR_TOKEN="$(openssl rand -hex 24)"
kubectl -n indah rollout restart deploy/indah-app # pick the secret up
The Secret is not in the manifests, so it never lands in git. INDAH_SECRET (for secure=True) goes in
the same Secret.
Several replicas need affinity¶
A tab's state lives in one pod, and its stream and its events must reach that pod. Without help, a Service sends each request to any pod: with two replicas and no affinity, only 10 of 20 test clicks took effect. The other 10 reached a pod that did not hold the tab, and indah answered them with HTTP 409 so the tab reconnects. The fix at the ingress is a cookie: the ingress sets it on the first response, the browser sends it back on the stream and on every event, and the ingress keeps sending that browser to the same pod.
On Traefik (k3s's default), the cookie is configured by annotations on the Service:
metadata:
annotations:
traefik.ingress.kubernetes.io/service.sticky.cookie: "true"
traefik.ingress.kubernetes.io/service.sticky.cookie.name: "indah-affinity"
traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true"
traefik.ingress.kubernetes.io/service.sticky.cookie.samesite: "lax"
traefik.ingress.kubernetes.io/service.sticky.cookie.maxage: "86400"
# over HTTPS also: traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true"
(The Traefik documentation lists these under the Kubernetes Ingress provider, for Traefik v3.7. k3s v1.35
ships Traefik 3.6, where they were tested.) With a Traefik IngressRoute instead of an Ingress, the cookie
is sticky.cookie on the service entry; that form was not tested here.
Affinity is best effort. A pod restart, a rollout or a scale-down ends the sessions that were on it: the tab reconnects to another pod and starts a fresh session there, so keep state you care about in your own database. indah drains first (below), which makes a rollout a reconnect, not an error.
ingress-nginx is retired
The Kubernetes project retired ingress-nginx in March 2026,
so it gets no more fixes. The overlay is here for clusters that still run it; it follows the
ingress-nginx cookie affinity documentation
(affinity: cookie, affinity-mode: persistent) and was not tested.
Rolling out without losing tabs¶
The Deployment has maxUnavailable: 0, a preStop sleep of 5 s so the ingress stops sending new tabs to the
old pod, then SIGTERM, then INDAH_DRAIN_SECONDS (10 s) of draining, inside a 30 s grace period. Keep
terminationGracePeriodSeconds above preStop plus the drain. Readiness is /ready, which turns 503 as
the drain starts. See Rolling out without losing tabs.
The client address¶
INDAH_TRUSTED_PROXIES: "k8s" trusts every private range plus the CGNAT range, which is broad: any pod
could then set X-Forwarded-For and be believed. When you know your ingress controller's pods' range
(k3s default: 10.42.0.0/16), name that instead. To keep the visitor's real address on k3s, its built-in
load balancer must not rewrite it: set externalTrafficPolicy: Local on the Traefik Service (k3s takes
that from a HelmChartConfig for traefik in kube-system). Check it with the deploy doctor
(/_indah/doctor, enabled by INDAH_DOCTOR_TOKEN): it warns when the resolved client is an address inside
trusted_proxies, which means the real visitor is hidden.
What was checked¶
On k3s v1.35.5 with Traefik 3.6.13 in a throwaway k3d cluster, two replicas, on 2026-10-24
(deploy/recipes/k8s/verify-k3d.sh repeats all of it; indah built from the repository checkout, since the
settings above are newer than the 0.7.0 release):
| Check | Result |
|---|---|
| Traefik overlay, browser-like client | cookie indah-affinity set on the page load; 20 of 20 events answered by the stream's pod, 20 of 20 patches reached the tab |
| Plain overlay, no cookie | 10 of 20 events reached the tab; the other 10 got HTTP 409 |
| Deploy doctor behind Traefik | the peer is the Traefik pod (10.42.0.x); a client-sent X-Forwarded-For: 6.6.6.6 never reached the app |
| Rolling restart, open tab | the tab received reconnect after the preStop sleep, as the drain began |
Not checked: ingress-nginx, a Traefik IngressRoute, the real visitor address on a real k3s
(k3d's Docker networking hides it: Traefik saw 10.42.0.1, so externalTrafficPolicy: Local was not
tested), the Gateway API, and a Cloudflare Tunnel in front of the cluster.