Routing into a cluster looks like a solved problem until the first requirement that annotations handle badly. Then the controller installed by a tutorial two years ago becomes a constraint on the architecture. This compares the four options that are genuinely worth considering, and the decisions around certificates and external addresses that turn out to matter more than the feature lists do.

An ingress controller is the one component almost every cluster needs and almost nobody chooses deliberately. Most teams install whatever the tutorial used, then discover its limits eighteen months later when they need a rate limit, a gRPC route, or a certificate that renews without human intervention.

What the Controller Actually Does

An Ingress or Gateway resource is only a declaration of intent. It says hostnames and paths should route to particular Services. Nothing happens until a controller is running in the cluster to read those resources and configure a real proxy accordingly.

So the choice is really a choice of proxy plus the operator that drives it. That framing makes the comparison tractable: you are picking a reverse proxy you will have to debug at three in the morning, and a control loop that translates Kubernetes objects into its configuration.

kubectl get ingressclass
kubectl -n ingress-nginx get pods
kubectl describe ingress my-app

The third command is the one to reach for when routing does not work. The controller writes its decisions into the resource's events, including why it rejected a rule.

The Four Realistic Choices

ControllerStrengthsWatch out for
ingress-nginxUbiquitous, well documented, huge annotation surface, predictableConfig reloads under heavy churn; annotations are not portable
TraefikClean CRDs, automatic ACME, good middleware model, ships with k3sCRD-based features are Traefik-specific; version upgrades change syntax
HAProxy IngressBest raw throughput and connection handling, mature TCP supportSmaller community, fewer copy-paste answers when stuck
Envoy Gateway / IstioGateway API native, rich traffic policy, mTLS between servicesSubstantially more moving parts; only worth it if you need a mesh

For a single cluster serving HTTP applications, ingress-nginx and Traefik are both correct answers and the decision rarely deserves a week of evaluation. If you are already running k3s, Traefik is installed and working, and replacing it is a deliberate act rather than a default. Our guide to installing k3s on a VPS covers disabling the bundled one if you want something else.

TLS Is Where the Real Difference Shows

Certificate handling separates controllers in practice more than routing features do. There are two patterns and mixing them causes the most confusing failures in this area.

cert-manager plus a Secret. cert-manager obtains certificates, writes them into Kubernetes Secrets, and the controller reads the Secret named in the Ingress resource. This works with every controller, keeps certificate logic in one place, and is what to use if you have more than one ingress path or need certificates for things that are not HTTP.

The controller's own ACME client. Traefik can obtain certificates itself. Fewer components, and genuinely simpler for a small cluster. The catch is storage: the ACME state must persist, and with more than one controller replica you need a distributed store or the replicas will each request certificates independently and hit rate limits.

Pick one pattern per cluster. A cluster where cert-manager owns some hostnames and the controller owns others produces renewal failures that look random and are not.

The LoadBalancer Problem Outside a Cloud

The controller's Service is normally type LoadBalancer, which on a managed cloud provisions an external load balancer automatically. On your own VPS or bare metal nodes there is nothing to provision, so the Service sits in Pending forever and every routing guide stops being applicable.

Three ways out, in ascending order of how much you should like them:

NodePort plus an external proxy. Expose the controller on a high port and put something in front. Works, but you now maintain a proxy outside the cluster and it becomes a single point of failure unless you also make it redundant.

hostNetwork or hostPort on the controller pods. The controller binds 80 and 443 directly on the node. Simple, fast, and fine for a small cluster; the cost is that DNS must point at node addresses and a node replacement becomes a DNS change.

MetalLB in layer 2 mode. Gives you real LoadBalancer Services with a virtual IP that fails over between nodes, so DNS points at one address that survives node loss. This is the closest thing to cloud behaviour and worth the extra component on any cluster you intend to keep.

# MetalLB address pool, adjust to a spare range you control
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: ingress-pool
  namespace: metallb-system
spec:
  addresses:
    - 10.0.10.240-10.0.10.250

Layer 2 mode needs the addresses to be routable on the node network, which is straightforward inside a private cloud and needs coordination with the provider for public addresses. Our guide to running Kubernetes on Proxmox covers the networking side where the cluster sits on your own virtualisation.

Gateway API and Whether to Wait

Gateway API is the successor to Ingress, and it fixes the thing everyone complains about: Ingress ran out of expressiveness years ago, so every controller invented annotations, and annotations do not port between controllers.

Gateway API replaces them with typed resources, and it separates the cluster operator's concern (the Gateway and its listeners) from the application team's concern (the HTTPRoute). On a multi-team cluster that separation alone justifies the migration.

The pragmatic position today: if you are starting fresh and your chosen controller supports it well, use Gateway API. If you have a working Ingress setup, migrate when you next need a feature that annotations handle badly, such as traffic splitting or header-based routing. Running both during a transition is supported and normal.

How to Decide in an Afternoon

Answer four questions and the choice usually makes itself.

Do you need anything other than HTTP and HTTPS? Raw TCP or UDP passthrough narrows the field quickly and favours HAProxy or a Gateway API implementation.

Do you need service-to-service mTLS and traffic policy? If yes, you want a mesh and the ingress choice follows from it. If no, do not install one for the ingress alone.

How many teams write routing rules? More than two, and Gateway API's role separation stops being theoretical.

Who will debug it? The controller with the most answers to your team's likely questions is worth more than a feature nobody uses. This is the honest case for ingress-nginx.

What the Cluster Underneath Has to Provide

Whichever controller you pick, it is the single path to every application in the cluster, which makes its availability the cluster's availability. Run at least two replicas with a pod anti-affinity rule so they land on different nodes, and give the external address a failover mechanism rather than pointing DNS at one node.

On MassiveGRID, managed Kubernetes starts from $0.03474 per hour, roughly $25.37 a month, with resources billed in cloudlets of 128 MiB RAM and 400 MHz CPU so a controller pod costs close to what it actually consumes rather than a whole instance. Where you run the cluster yourself, Proxmox high-availability clustering restarts a lost node's workloads automatically and Ceph storage replicates every block three times across independent NVMe drives, which keeps controller state and cert-manager Secrets available through a node failure. Always-on DDoS mitigation with 10+ Tbps of scrubbing sits in front, which matters more for an ingress endpoint than for anything else in the cluster.

Infrastructure can be ordered across a partner footprint of more than 700 datacenters in 85 metros, 30 countries and six continents, with auto-provisioning in New York, London, Frankfurt and Singapore, so an ingress endpoint can sit close to the users it serves.

Further Reading