Skip to main content

Choose how to expose an application

Inspect 1.36

There are several ways to make a workload reachable from the internet. The right one depends on your protocol and on how many applications you expose. None of these choices is permanent; you can move from one to another later. A switch does require moving DNS records and certificates, so choose deliberately from the start.

The usual setup: one load balancer, one reverse proxy#

For most clusters, use at least one load balancer that points at a reverse proxy running in the cluster (an ingress controller). The load balancer provides one public IP. The reverse proxy sits behind it and routes by hostname, so many domains and many applications share that single IP and that single load balancer. You add an application by writing a routing rule rather than by provisioning another load balancer.

The building blocks#

  • A type: LoadBalancer Service places one Service directly on the internet, on any TCP-based protocol. The Hetzner cloud-controller-manager (CCM) watches the Service and calls the Hetzner API to create a load balancer for it, one load balancer per Service.
  • An ingress controller is the reverse proxy behind one load balancer. It routes many HTTP applications by host and path, and terminates TLS in the cluster. This is the usual setup described above.
  • A NodePort Service opens a port on every node, with no load balancer. It is the low-level component that the other two build on, and it is rarely used on its own.

Which one to use#

You have Use
Several HTTP or HTTPS applications, host or path routing, TLS An ingress controller. Every application shares one load balancer, and certificates reside in the cluster.
One application on a non-HTTP protocol over TCP (raw TCP, gRPC, MQTT) A type: LoadBalancer Service. An ingress controller handles HTTP only.
A single application, with no proxy in the request path A type: LoadBalancer Service. The simplest option, at one load balancer per application.
A UDP service (DNS, QUIC, game or VoIP traffic) Not a Hetzner load balancer; it carries TCP only. See UDP is not supported.
A caller that must see the client's real address Either, plus . By default a pod sees the load balancer's address, not the client's.

UDP is not supported#

Hetzner load balancers carry TCP only (Layer 4 in Kubernetes terms: they forward TCP connections and serve HTTP and HTTPS on top of that TCP). They do not carry UDP. A type: LoadBalancer Service with a UDP port never receives a working load balancer, because there is nothing on the Hetzner side to create.

A UDP service (DNS, QUIC, VoIP, game traffic) therefore reaches the nodes without the managed load balancer:

  • Expose it as a NodePort Service. Cilium forwards UDP on that NodePort on every node's public IP, so clients can reach it there.
  • Point clients at the node addresses directly, or place your own UDP load balancing, anycast, or a floating IP in front. You now own the address and the failover, because no Hetzner load balancer provides them.

This is a trade-off rather than a routine step: without the managed load balancer you also give up its health checks and its single stable IP, so plan how clients locate a healthy node.

Ingress, the Gateway API, and mesh CRDs#

These are all Kubernetes-style APIs for writing routing configuration. Compared with hand-editing an nginx.conf, each provides a schema with validation and RBAC, because the configuration lives in objects you can grant access to. They differ in how much they can express and in how they divide ownership.

Ingress is the original and the simplest: host and path routing in a single object. That single object is also its limitation. Anything beyond basic routing (rewrites, timeouts, header matching, traffic splitting) relies on vendor-specific annotations that each controller interprets differently, so an Ingress does not port cleanly from one controller to another. Because it is one object, ownership is all-or-nothing, so a platform team and an application team cannot each own a separate part of it. Ingress remains stable and functional, but it is no longer actively developed, and the community regards the Gateway API as its successor. See the Kubernetes Ingress docs.

The Gateway API is that successor, GA since 2023. It was designed to address those limitations, so it splits routing into typed resources according to who owns each part: a GatewayClass names an implementation, a cluster operator owns the Gateway (the entry point and its listeners), and each application team owns its own HTTPRoute objects (the host and path rules). The advanced routing that Ingress delegated to annotations is first-class and portable here, so the same configuration moves between implementations instead of being rewritten per controller. See the Gateway API project and the GA announcement.

A mesh brings its own CRDs, applying the same idea within a single product. Istio, for example, configures routing with its own custom resources in the networking.istio.io group: VirtualService and DestinationRule for routing and load balancing, Gateway for the edge, and ServiceEntry for external services. You gain the mesh's full feature set, at the cost of being Istio-specific, though Istio now also implements the Gateway API, so you can drive its ingress with the standard resources instead. See the Istio networking reference.

None of this determines how well the traffic is actually served. The API defines how you write the configuration. The product behind it (nginx, Envoy, Traefik) reads that configuration and performs the work. Choose the API for how your teams work, and the product for how it performs.

The request path#

Even with an ingress controller, the path consists of these same building blocks stacked together:

flowchart LR
  C[Client] --> LB[Hetzner load balancer]
  LB --> NP[NodePort on a node]
  NP --> RP["Reverse proxy pod<br/>(nginx, Envoy, Traefik)"]
  RP --> B[Backend Service]
  LB -. plain type LoadBalancer Service .-> B
  1. A Hetzner load balancer receives the request on your public IP.
  2. It forwards the request to a NodePort on the cluster's nodes.
  3. From there, the Service's routing (externalTrafficPolicy: Cluster or Local, see ) passes it to the reverse proxy pod: nginx, Envoy for Istio, or Traefik.
  4. The reverse proxy reads its configuration and makes one additional hop to the backend Service that serves that hostname.

A plain type: LoadBalancer Service omits the middle stages: the load balancer forwards through the NodePort directly to your application. This is why it uses one load balancer per application, and why the reverse proxy exists in the first place, to consolidate many applications behind one IP.

Note

TLS normally terminates at the reverse proxy, not at the load balancer. The Hetzner load balancer runs at Layer 4 (plain TCP) and forwards the encrypted stream untouched; the proxy holds the certificate and decrypts the traffic. and automate the two tasks required for a hosted name: the certificate and the DNS record. See .

Cost#

Every type: LoadBalancer Service creates its own Hetzner load balancer, and Hetzner bills each one. Ten such Services result in ten load balancers on your invoice. An ingress controller places all your HTTP applications behind one load balancer, so you pay for one and route the remainder with routing objects.

You control each load balancer, including its type and size, its location, the balancing algorithm, the health checks, and Proxy Protocol, through annotations on the Service. See for the full set of options.

Choose your controller, and run more than one#

The load balancer itself is created for you by the Hetzner cloud-controller-manager (CCM): it watches your type: LoadBalancer Services and calls the Hetzner API to create each load balancer and keep it in sync. Syself Autopilot does not install an ingress controller, by design. An ingress controller is an application choice rather than infrastructure, so including one by default would tie you to a version and a feature set chosen for you. You install the one you want as a normal workload and remain free to replace it.

Running more than one controller is a supported configuration, not a workaround. Nothing limits a cluster to a single load balancer or a single controller. Each controller has its own type: LoadBalancer Service, so each has its own load balancer and its own public IP. Because one load balancer is one IP with its own throughput and connection limits, the ability to run several supports a range of configurations: place tenants on separate IPs, keep a legacy application on nginx while new applications move to Traefik, or carry more traffic than one load balancer can handle. See for running several side by side.

Note

ingress-nginx has long been the default choice and remains acceptable on existing clusters, but the project is being retired: best-effort maintenance ended in 2026, so it no longer receives releases or security fixes. For a new cluster, prefer the or a maintained controller such as or Envoy Gateway.

Important

Whatever you expose, the platform drains and replaces nodes during upgrades and self-healing. Run at least two replicas of the exposed workload, spread them across nodes, and assign them a PodDisruptionBudget; otherwise a single node drain takes the application down. See .

Where to go next#