Kubernetes NGINX Ingress Controller: Setup, TLS, and Routing
Your Kubernetes cluster runs, pods are healthy, services are up. But there is no way for external traffic to reach them. You could expose each service with a NodePort, but that means managing separate ports, dealing with SSL termination outside the cluster, and losing path-based routing entirely.
An Ingress controller solves this. It sits at the edge of your cluster, accepts HTTP and HTTPS traffic, and routes it to the right service based on hostname or path. NGINX is the most common choice, and for good reason: it is fast, well-documented, and has been battle-tested in production for years.
This guide walks through installing the NGINX Ingress Controller with Helm, creating your first Ingress resource, and setting up TLS with cert-manager so your services get automatic HTTPS certificates.
Prerequisites
- A running Kubernetes cluster (1.25+) with
kubectlconfigured - Helm 3 installed (install docs)
- A domain name pointing to your cluster's external IP or load balancer
cert-managerinstalled if you want automatic TLS (covered below)
Check your cluster is reachable:
kubectl cluster-info
kubectl get nodes
Step 1: Install NGINX Ingress Controller
The official Helm chart lives in the ingress-nginx repository. Add it and install:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx --create-namespace --set controller.service.type=LoadBalancer
Verify the pods are running:
kubectl -n ingress-nginx get pods
You should see output like:
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-6f4b8c7c9b-abc12 1/1 Running 0 30s
Get the external IP of the load balancer:
kubectl -n ingress-nginx get svc ingress-nginx-controller
The EXTERNAL-IP field shows where your cluster accepts traffic. Point your DNS A record to this IP.
Step 2: Deploy a Test Application
Before configuring routing, you need something to route to. Deploy a simple app:
kubectl create deployment hello --image=nginx:alpine --port=80
kubectl expose deployment hello --port=80 --type=ClusterIP
This creates a Service called hello that is only reachable from inside the cluster. The Ingress controller will change that.
Step 3: Create an Ingress Resource
Create a file called hello-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: hello.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello
port:
number: 80
Apply it:
kubectl apply -f hello-ingress.yaml
Now traffic to hello.example.com reaches your hello deployment. The NGINX controller watches for Ingress resources and updates its nginx.conf automatically.
Verify the Ingress was created:
kubectl get ingress hello-ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
hello-ingress nginx hello.example.com 34.120.x.x 80 10s
Step 4: Path-Based Routing
Most real setups route multiple paths to different services. Deploy a second app:
kubectl create deployment api --image=nginx:alpine --port=80
kubectl expose deployment api --port=80 --type=ClusterIP
Update your Ingress to handle both paths:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /web(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: hello
port:
number: 80
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api
port:
number: 80
The rewrite-target: /$2 annotation strips the prefix before forwarding. A request to example.com/api/users reaches the api service at /users, not /api/users.
Apply the updated config:
kubectl apply -f hello-ingress.yaml
Test with curl:
curl -H "Host: example.com" http://<EXTERNAL-IP>/web/
curl -H "Host: example.com" http://<EXTERNAL-IP>/api/status
Step 5: Install cert-manager for Automatic TLS
cert-manager watches for Certificate resources and talks to Let's Encrypt to issue and renew TLS certificates automatically.
Install cert-manager with Helm:
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true
Verify the pods:
kubectl -n cert-manager get pods
You should see three pods running: cert-manager, cert-manager-cainjector, and cert-manager-webhook.
Step 6: Create a ClusterIssuer
A ClusterIssuer tells cert-manager which ACME server to use and how to verify domain ownership. Create cluster-issuer.yaml:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
ingressClassName: nginx
Apply it:
kubectl apply -f cluster-issuer.yaml
Check the issuer status:
kubectl get clusterissuer letsencrypt-prod -o wide
The READY column should say True after a few seconds.
Step 7: Add TLS to Your Ingress
Update your Ingress resource with a TLS section:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- example.com
secretName: example-com-tls
rules:
- host: example.com
http:
paths:
- path: /web(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: hello
port:
number: 80
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api
port:
number: 80
Apply:
kubectl apply -f hello-ingress.yaml
cert-manager will detect the cert-manager.io/cluster-issuer annotation, request a certificate from Let's Encrypt, and store it in the example-com-tls Secret. This takes 30 to 60 seconds.
Monitor the certificate:
kubectl get certificate
NAME READY SECRET AGE
example-com-tls True example-com-tls 45s
Once READY is True, HTTPS works. Try:
curl -v https://example.com/web/
Common Issues and Fixes
502 Bad Gateway. The Ingress controller cannot reach your backend. Check that the Service name and port match what you defined. Use kubectl describe ingress hello-ingress to see the configured backends. Also verify the Service is running: kubectl get svc hello.
Certificate stuck in Pending. cert-manager cannot complete the HTTP-01 challenge. Make sure port 80 is open on your load balancer and that DNS points to the correct IP. Check cert-manager logs: kubectl -n cert-manager logs deploy/cert-manager.
Rewrite breaks your app. The rewrite-target annotation changes the path before forwarding. If your app expects the original path, remove the annotation. If you need both the original and rewritten path, use the nginx.ingress.kubernetes.io/configuration-snippet annotation to set custom headers.
Ingress class not found. If kubectl get ingress shows no class, add --set controller.ingressClassResource.name=nginx to your Helm install command, or check that you set ingressClassName: nginx in your Ingress spec.
Where to Go Next
- rate-limit annotations (
nginx.ingress.kubernetes.io/limit-rps) to protect backends from traffic spikes - canary deployments with
nginx.ingress.kubernetes.io/canary-weightfor gradual rollouts - ModSecurity WAF by enabling the
modsecurity-snippetannotation - NGINX Ingress Controller configuration at the official repository docs