Feature · ~5 min · Kubernetes & Docker

Your homelab,
discovered

You already know what's running on your cluster. Your tunnel tool doesn't — so you look up an address, copy it wrong, and debug a 502. Now the agent reads the inventory itself and hands you a list to pick from.

Exposing a service starts with an address you have to go and find

Every tunnel needs a target. On a laptop that's localhost:3000 and nobody thinks about it. On a Kubernetes cluster it's http://grafana.monitoring.svc.cluster.local:3000 — four parts, each of which you have to be right about, none of which you remember.

So you go and look. kubectl get svc -A, squint, find the port, notice it's named http-web and not the number you assumed, type it into the dashboard, get a 502, and start again. On Docker it's the same story with different spelling: is it the published host port, the container port, the compose service name, or the IP that changes every time the container is recreated?

None of this is hard. It's just friction, repeated once per service, and it's the reason the fifth thing you meant to expose never gets exposed.

The agent brings the list to you

An HLE agent now inventories what is running around it and reports the list to the dashboard. You pick something from it, and it becomes a tunnel. No address to look up, no port to get wrong.

Run it on the machine itself to see exactly what the dashboard will show:

hle agent services
              Discovered services (k8s, docker)
  Name     Where       Address                          Suggested label
  grafana  monitoring  http://grafana.monitoring.svc…   grafana
  gitea    git         http://gitea.git.svc.cluster…    gitea
  plex     media       http://172.18.0.4:32400          plex

The address is always from the agent's point of view. A cluster-internal DNS name is exactly right there, and is exactly what gets stored — the agent is inside the cluster, so that name resolves for it even though it means nothing on your laptop.

Two providers ship today, and they self-detect. An agent on a plain VM reports nothing and the dashboard hides the section rather than showing you an empty table with an explanation you didn't ask for.

Discovery is read-only. It lists. It never creates, patches or deletes anything in your cluster or on your Docker host — there is no code path that writes.

Kubernetes, in one Helm command

The chart that ships the HLE operator now ships the agent too. If you only want the agent, turn the operator off:

git clone https://github.com/hle-world/hle-operator
cd hle-operator

helm install hle ./chart/hle-operator \
  --namespace hle --create-namespace \
  --set operator.enabled=false \
  --set agent.enabled=true \
  --set agent.token.value=hlea_your_token_here

Get the token from Connections → Agents → New Agent. It's shown once. Rather keep it out of your shell history? Create the Secret yourself and pass --set agent.token.existingSecret=hle-agent instead.

The pod dials out. There's no Service, no Ingress, no published port, and no inbound path into the cluster — which was the point of using HLE in the first place.

The permission it gets is one ClusterRole, and this is the whole of it:

rules:
  - apiGroups: [""]
    resources: ["services", "endpoints"]
    verbs: ["get", "list", "watch"]

No secrets. No pods. No write verb anywhere. And if you'd rather not grant even that, set agent.discovery.enabled=false — the pod then stops mounting a ServiceAccount token altogether, and you type addresses in by hand as before.

kube-system, kube-public and kube-node-lease never appear in the list. Neither do ports named metrics, telemetry, health or probe. Cluster plumbing isn't what you're looking for, and in kube-system's case it's actively dangerous to put on the internet.

Docker, from the socket

The same agent, on a Docker host, reads running containers and their ports:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro

Where a container has a network alias, that's preferred over its raw IP — so the endpoint keeps working after the container is recreated with a different address, which is the failure mode you'd otherwise hit a week later and blame on HLE.

The Docker socket is root-equivalent on the host. Anything that can write to it can start a privileged container. The agent only ever reads — containers list and inspect — and the mount should be :ro. But mounting it at all is a real trust decision, so it stays opt-in and off by default. If that trade isn't worth it to you, don't mount it: everything else still works.

From list to live tunnel

In the dashboard, open Connections → Agents → Discovered. Pick a service. The discovered address becomes the target and the suggested label becomes the subdomain — both editable before you save.

From there it's an ordinary endpoint. It appears in your tunnel list, obeys the same access controls as everything else, gets the same traffic chart, and the agent starts serving it within seconds. Nothing about the resulting tunnel remembers that it was discovered rather than typed.

The operator isn't going anywhere

Discovery is the imperative path: see what exists, click, done. That's the opposite of what you want if your cluster is reconciled from a Git repository, and the operator is still the answer there — HLETunnel CRDs, Ingress adoption, and a commit for every change.

They coexist deliberately. The same chart installs either or both, and neither reconciles the other's tunnels. A common shape: the operator for production services that belong in Git, the agent for the long tail you're still deciding about — where the discovery list is the fastest way to find out what that tail contains.

Point an agent at your cluster and look

Free tier, no card. Discovery is read-only and off unless you grant it.