← Back to Blog

Kubernetes NGINX Ingress Controller: Setup, TLS, and Routing

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 kubectl configured
  • Helm 3 installed (install docs)
  • A domain name pointing to your cluster's external IP or load balancer
  • cert-manager installed 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-weight for gradual rollouts
  • ModSecurity WAF by enabling the modsecurity-snippet annotation
  • NGINX Ingress Controller configuration at the official repository docs

References

Need Help Implementing This?

I help teams design and build scalable cloud infrastructure, DevOps pipelines, and production-grade systems.

Book a Free Consultation