A Kubernetes Ingress is an object that routes HTTP and HTTPS requests from outside the cluster to Services inside it, based on hostname and path. The object itself is only a description of rules: an Ingress Controller such as ingress-nginx or Traefik reads those rules and turns them into a running reverse proxy. Without a controller, an Ingress does nothing at all, and that is exactly where many first attempts fail.
I run Kubernetes clusters in production and wrote the Kubernetes Practical Guide published by Rheinwerk (2024). In almost every cluster I have set up, the Ingress was the point where an application became reachable for real users for the first time, and the point where the first 404 and 503 errors showed up. What I learned along the way is condensed here. You can find all my Kubernetes articles collected on the Kubernetes page.
What the Ingress does and what the Service does
Inside the cluster, the Service gives your Pods a stable address. It knows every replica of a Deployment, spreads requests across them and gets a DNS name that other Pods can call. How the Service types ClusterIP, NodePort and LoadBalancer work is covered in Kubernetes Service Types Explained. What the Service does not answer is how a browser gets from the outside into the cluster.
That is the job of the Ingress. Picture an office building shared by several companies. The reception desk on the ground floor asks each visitor who they want to see and sends them to the right floor. That is the Ingress: it looks at the hostname and path of a request and decides which Service is responsible. Upstairs, the department, meaning the Service, takes over and hands the visitor to a free employee, the Pod.
Technically, the Ingress works on layer 7 of the OSI model, so it understands HTTP. That is why it can route by URL, rewrite paths, terminate TLS and, depending on the controller, authenticate or rate-limit requests. A NodePort Service works on layer 4 and simply forwards whatever arrives on a port. For HTTP applications I therefore recommend the Ingress in practically every case.
|
Service (ClusterIP) |
Service (NodePort) |
Ingress |
| Reachable from |
inside the cluster |
outside via node IP and port |
outside via hostname and path |
| Works on |
layer 4 |
layer 4 |
layer 7 (HTTP) |
| Knows Pods |
yes, via selector |
yes, via selector |
no, it points to Services |
| TLS |
no |
no |
yes, certificate stored as a Secret |
| Also needs |
nothing |
nothing |
an Ingress Controller |
Ingress and Service do not replace each other, they work in sequence: the Ingress forwards to a Service, never directly to a Pod.
The Ingress Controller: ingress-nginx, Traefik or a cloud provider
Kubernetes only ships the Ingress API. The implementation is an Ingress Controller that you or your cluster admins install separately. The controller watches all Ingress objects in the cluster and configures a reverse proxy from them that actually accepts the requests. This split is deliberate: the manifest stays the same whether an Nginx inside the cluster or a load balancer in the cloud sits behind it.
The three variants you will meet in practice:
| Controller |
Runs where |
Typical use |
| ingress-nginx |
as Pods inside the cluster |
on-premise and with every cloud provider, most widely used, many annotations |
| Traefik |
as Pods inside the cluster |
bundled with k3s, configured through its own CRDs or plain Ingress |
| Cloud controller (e.g. AWS Load Balancer Controller) |
creates a load balancer at the provider |
managed Kubernetes in the cloud, the proxy lives outside the cluster |
One source of confusion that cost me time early on: ingress-nginx is the Kubernetes community project. There is also an NGINX Ingress Controller from the vendor F5. Both use Nginx, but they have different annotations and different documentation. Before you search for an annotation, check which of the two you actually installed.
So that a controller knows which Ingress objects belong to it, there is the IngressClass. You set it in the Ingress under spec.ingressClassName. If only one controller runs in the cluster, a default class is usually marked and you can leave the field out. As soon as two controllers run, say one internal and one public, the class is mandatory, otherwise no controller or the wrong one picks up your Ingress. In my k3s clusters on Hetzner I use the bundled Traefik, in clusters with other distributions mostly ingress-nginx. The application manifests are the same in both cases, apart from the annotations.
Ingress rules: host, path and pathType
An Ingress rule consists of an optional host and a list of paths that each point to a Service and a port. If the host is missing, the rule applies to every request that reaches the controller. The following manifest sends the shop of a fictional company to a frontend and the API to a second Deployment:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
spec:
ingressClassName: nginx
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
Two things matter here. First: when several paths match, the longest one wins. A request to /api/orders ends up at the API even though / would match as well. Second: the Ingress passes the full path on to the application. The API therefore receives /api/orders and has to handle it. If it expects /orders, you need a rewrite rule, which is configured through an annotation or a middleware depending on the controller.
The pathType decides how strictly the comparison is done:
| pathType |
Behaviour |
| Prefix |
The requested path must start with the defined path, compared element by element at the slashes |
| Exact |
The path must match exactly, including case; when both match, Exact wins over Prefix |
| ImplementationSpecific |
The controller decides how to interpret the path |
I almost always use Prefix and avoid ImplementationSpecific, because the same manifest can behave differently across controllers with it. Besides the rules you can define a defaultBackend that receives every request no rule matches. In practice that is where a custom error page lives; most controllers otherwise ship a plain 404 page.
Kubernetes Ingress with TLS: HTTPS with cert-manager
Without TLS an Ingress is not fit for production today. The certificate lives in Kubernetes as a Secret of type kubernetes.io/tls, and the Ingress references it under spec.tls. The controller terminates the TLS connection; traffic to the Service behind it then usually runs unencrypted inside the cluster network. How Secrets are structured and why they do not belong in Git is covered in ConfigMap and Secret in Kubernetes.
Renewing certificates by hand is the road to an incident you only run into months later. That is why cert-manager runs in every cluster I operate. The tool watches Ingress objects, obtains a certificate from a certificate authority such as Let's Encrypt, creates the Secret and renews it in time. You need an Issuer or ClusterIssuer once, describing where certificates come from, and after that only an annotation and the TLS block in the Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- shop.example.com
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
With this annotation, cert-manager creates a Certificate object, triggers the Let's Encrypt challenge and writes the result into the Secret shop-tls. For the common HTTP-01 challenge, the host must be reachable from the internet on port 80, because Let's Encrypt fetches a file there. For internal hostnames or wildcard certificates you need the DNS-01 challenge, where cert-manager creates a record at your DNS provider. You can see whether the certificate is there with kubectl get certificate -n shop; if it shows READY False, kubectl describe certificate tells you why, usually a domain that cannot be reached.
Ingress without a cloud: where the traffic arrives
So far this was about rules. The question cloud documentation likes to skip: how does a request from the internet reach the Ingress Controller in the first place? The controller itself is just a Deployment with a Service in front of it, usually of type LoadBalancer. In a cloud, the provider automatically creates a load balancer with a public IP for it, and your DNS points to that IP.
On-premise, that automation does not exist. The LoadBalancer Service then sits at <pending> until you provide an external address yourself: with MetalLB, with a load balancer in front of a NodePort, or with ServiceLB, which ships with k3s. Which option fits when is covered with the Service types in Kubernetes Service Types Explained. For the Ingress only one thing matters: there has to be an address in front of the controller for DNS to point at, and for resilience that address should not hang off a single node.
Which option fits your operation depends heavily on whether you host it yourself or use a provider. That trade-off is discussed in Kubernetes On-Premise or Cloud?.
Common Ingress errors and how to find them
The three error patterns I see most often can be narrowed down with a handful of commands:
kubectl get ingress -n shop
kubectl describe ingress shop -n shop
kubectl get endpoints frontend -n shop
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50
A 404 from the controller usually means no rule matched. The most common reason is a host that does not match the name you requested, for example because you opened the IP address instead of the domain. If kubectl get ingress shows no address in the ADDRESS column, no controller has taken responsibility for the Ingress: check the ingressClassName.
A 503 or 502 means the rule matched, but nothing behind the Service is answering. Then kubectl get endpoints is worth a look: if the list is empty, the selector of the Service does not match the Pod labels, or the Pods are not ready. A misspelled Service name or a wrong port in the Ingress shows up as a warning in describe.
If HTTPS stays red even though the Ingress works, it is almost always the certificate. kubectl describe certificate and the cert-manager events tell you whether the challenge failed. The actual cause is often a DNS record still pointing to the old address, or port 80 blocked by a firewall.
Gateway API: the option beyond Ingress
The Ingress has a well-known weakness: everything beyond host and path lives in annotations, and those differ per controller. The Gateway API solves this with several objects. A Gateway describes the entry point and belongs to the cluster team; an HTTPRoute describes the routing and belongs to the application team. Header matching, traffic splitting and rewrites are part of the standard instead of annotations. The official introduction to the Gateway API shows the objects in detail.
My take: for a new cluster with several teams, the Gateway API is worth a look because the separation of roles is cleaner. For the standard case, one application under one domain with TLS, the Ingress remains perfectly adequate, is supported by every controller and is not going away. So you do not have to migrate anything as long as annotations are not getting in your way.
Frequently asked questions
What is the difference between a Service and an Ingress?
The Service gives Pods a stable address inside the cluster and spreads requests across the replicas. The Ingress routes HTTP requests from outside to Services based on hostname and path. They work together: the Ingress knows nothing about Pods, only about Services.
What is an Ingress Controller?
An Ingress Controller is the software that reads Ingress objects and configures a running reverse proxy from them. Kubernetes does not ship one; common choices are ingress-nginx, Traefik and the controllers of the cloud providers. Without an installed controller an Ingress is created but never served.
ingress-nginx or Traefik: which one should I pick?
For the standard case both are equivalent. ingress-nginx is the most widely used and has an annotation for almost every special case. Traefik is bundled with k3s and can be configured in more detail through its own CRDs. Take what your cluster brings along and switch only when you are missing a specific feature.
Do I need a separate Ingress for every application?
No. One Ingress can route several hosts and paths to different Services, and most controllers merge several Ingress objects for the same host. There is one limit: an Ingress belongs to a namespace and can only reference Services in that namespace. Applications in separate namespaces therefore each need their own Ingress object.
Ingress or Gateway API for a new project?
For one application with a domain and TLS, the Ingress is enough and supported everywhere. If several teams share the same entry point or you need features like traffic splitting without annotations, the Gateway API is the cleaner choice.
Where to go next
Before you touch the Ingress, the Service behind it should be in place and kubectl get endpoints should show real Pod addresses. The basics are in Kubernetes Service Types Explained. After that, a short look at the official documentation on Ingress in Kubernetes is worth it, especially for the pathType examples.
My suggestion for today: take the first manifest above, replace the hostname and Service names with your own and apply it. Then open the domain in your browser, then deliberately the IP address, and watch how the controller reacts in both cases. Once that works, add cert-manager.
In full detail with all examples, from communication between Pods through the Service types to an Ingress with several paths, this is covered in chapter 3 of my Kubernetes Practical Guide (Rheinwerk Computing).