Kubernetes
Helm & Ingress Controller

Published: November 1, 2026 · 10 min read

Helm and Ingress Controller Explained with a Real Install

Your app runs in the cluster. Reaching it still means a NodePort with an ugly five digit number. Or a LoadBalancer per service, each with a bill.

Ingress fixes that. And Helm is how you install the thing that makes Ingress work.

It builds straight on the Service you already know.

Here is the short version:

  • Helm is the package manager for Kubernetes.
  • Three words: chart (the package), values (your settings), release (one install).
  • An Ingress is only a rule. It routes nothing.
  • An Ingress Controller is a running Pod that does the routing.
  • No controller installed means your Ingress is silently ignored.
  • One controller gives you one entry point for every service.

Helm chart, values and release, the three words of the Kubernetes package manager

Why Helm Exists

Installing ingress-nginx by hand means writing nine objects or more. A namespace, a service account, a ClusterRole and a binding. Then a ConfigMap, a Deployment, a Service, an IngressClass and a webhook. Then maintaining all of it.

Installing ingress-nginx by hand versus one Helm command in Kubernetes

Helm is apt for Kubernetes. Somebody already wrote those files, versioned them and tested them. You install the package and override the handful of values you care about.

Helm wordWhat it is
Chartthe package — templated YAML plus a values.yaml of defaults
Valueswhat you override, with --set or your own file
Releaseone installed instance, with a name and a revision number
Repositorywhere charts are downloaded from

Helm 3 has no Tiller. Nothing is installed in your cluster for Helm itself. It is just a CLI that talks to the API server as you.

Ingress Is Not the Ingress Controller

This is the part that confuses everyone, and it is worth getting straight before you install anything.

A Kubernetes Ingress is a rule while the Ingress Controller is the Pod that routes traffic

IngressIngress Controller
What it isa YAML objecta running Pod
Who writes ityouinstalled once, by Helm
What it doesholds rulesdoes the routing
On its ownnothingneeds rules to be useful
Examplesyour ingress.yamlingress-nginx, Traefik, HAProxy, AWS ALB
🚫

Apply an Ingress with no controller running and Kubernetes accepts it without complaint. No error, no event, no routing. The object just sits in etcd. This is the single most common Ingress problem.

Step 1 — Install Helm

terminal — install helm
# Ubuntu / Debian - the snap package is the quickest
sudo snap install helm --classic

# check it worked
helm version
# version.BuildInfo{Version:"v3.16.3", GitCommit:"...", GoVersion:"go1.23.3"}

# other ways, if snap is not available
# macOS
brew install helm

# the official script
curl -fsSL -o get_helm.sh \
  https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh && ./get_helm.sh
⚠️

--classic is required for the snap. Helm needs to read your kubeconfig outside the snap sandbox, and it will not work without it.

Step 2 — Add the Chart Repository

terminal — add the repo
# 1. tell Helm where the chart lives
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
# "ingress-nginx" has been added to your repositories

# 2. pull the latest index from every repo you added
helm repo update
# Hang tight while we grab the latest from your chart repositories...
# Update Complete. Happy Helming!

# what do I have?
helm repo list
# NAME            URL
# ingress-nginx   https://kubernetes.github.io/ingress-nginx

helm repo add only saves the URL locally. helm repo update is what actually downloads the index of available charts.

Run helm repo update before an install if it has been a while. Otherwise Helm installs the newest chart version it knew about last time.

Step 3 — Install the Ingress Controller

terminal — install ingress-nginx
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  -n ingress-nginx --create-namespace \
  --set controller.service.type=NodePort

# Release "ingress-nginx" does not exist. Installing it now.
# NAME: ingress-nginx
# NAMESPACE: ingress-nginx
# STATUS: deployed
# REVISION: 1

That one line has a lot in it. Here is every part:

what each argument means
helm upgrade --install  ingress-nginx  ingress-nginx/ingress-nginx  ...
#    |        |          |              |
#    |        |          |              +-- <repo name>/<chart name>
#    |        |          +----------------- the RELEASE name you are choosing
#    |        +---------------------------- install it if it is not there yet
#    +------------------------------------- upgrade an existing release

#  -n ingress-nginx          put it in this namespace
#  --create-namespace        make that namespace if it is missing
#  --set controller.service.type=NodePort
#                            override one value from the chart's values.yaml
PartWhy
upgrade --installinstalls the first time, upgrades every time after
ingress-nginx (first)the release name you are choosing
ingress-nginx/ingress-nginxrepo name / chart name
-n ingress-nginxthe namespace to install into
--create-namespacemake that namespace if it does not exist
--set controller.service.type=NodePortoverride one value

Why override the service type?

The chart's values.yaml sets this by default:

# -- Type of the external controller service.
type: LoadBalancer

LoadBalancer is right on EKS, GKE or AKS, where the cloud creates a real load balancer for you. On a plain EC2 cluster, bare metal or minikube nothing creates one. The service would sit in <pending> forever.

⚠️

NodePort is the honest choice when there is no cloud load balancer. You reach the cluster at a node IP plus a port instead of a nice address.

Step 4 — Check It Worked

terminal — verify
kubectl get pods -n ingress-nginx
# NAME                                        READY   STATUS    RESTARTS   AGE
# ingress-nginx-controller-7d4b9c6f85-2kq9x   1/1     Running   0          45s

kubectl get svc -n ingress-nginx
# NAME                       TYPE       CLUSTER-IP      PORT(S)
# ingress-nginx-controller   NodePort   10.100.205.4    80:31245/TCP,443:30891/TCP

# just the HTTP node port, nothing else
kubectl get svc -n ingress-nginx ingress-nginx-controller \
  -o jsonpath='{.spec.ports[0].nodePort}'
# 31245

helm list -n ingress-nginx
# NAME            NAMESPACE       REVISION   STATUS      CHART
# ingress-nginx   ingress-nginx   1          deployed    ingress-nginx-4.12.1

That jsonpath command is the useful one. It prints only the HTTP node port, so you can drop it straight into a script or a curl.

Step 5 — Write an Ingress

The controller is running. Now give it rules.

ingress.yaml
# ingress.yaml  -  the routing rule

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hotel-ingress
  namespace: hotel
spec:
  ingressClassName: nginx        # WHICH controller should pick this up
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-svc    # an existing Service
                port:
                  number: 8080

          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-svc
                port:
                  number: 80

Three fields matter most:

FieldWhat it decides
ingressClassNamewhich controller picks this up
hostwhich domain this rule is for
path + pathTypewhich Service gets the request
terminal — apply and test
kubectl apply -f ingress.yaml
# ingress.networking.k8s.io/hotel-ingress created

kubectl get ingress -n hotel
# NAME            CLASS   HOSTS              ADDRESS         PORTS   AGE
# hotel-ingress   nginx   shop.example.com   10.100.205.4    80      20s

# CLASS must say nginx, not <none>
kubectl get ingressclass
# NAME    CONTROLLER             AGE
# nginx   k8s.io/ingress-nginx   4m

# test it through the node port
curl -H "Host: shop.example.com" http://<node-ip>:31245/api/health
# ok
🚫

If CLASS shows <none>, no controller has claimed your Ingress. Check kubectl get ingressclass and make ingressClassName match.

How the Request Actually Travels

How a request reaches a Pod through the Kubernetes ingress controller

  1. The browser asks for shop.example.com/api.
  2. It arrives at the NodePort or the cloud load balancer.
  3. The controller Pod reads its rules and matches host and path.
  4. It forwards to the right Service.
  5. The Service picks one of your Pods.

The saving is at step 2. Without Ingress, every service needs its own LoadBalancer and its own bill. With Ingress you pay for one door and route behind it.

Values Files Beat Long Command Lines

Two or three --set flags are fine. Ten are not.

my-values.yaml
# my-values.yaml  -  better than a long line of --set flags

controller:
  service:
    type: NodePort
  replicaCount: 2
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      cpu: 500m
      memory: 512Mi

# then
# helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
#   -n ingress-nginx --create-namespace -f my-values.yaml

A values file can go in git, be reviewed, and be reused on the next cluster. A long shell command cannot.

Look Before You Install

terminal — inspect first
# what WOULD it create? render locally, change nothing
helm template ingress-nginx ingress-nginx/ingress-nginx \
  --set controller.service.type=NodePort | head -40

# same, but validated against the real cluster
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  -n ingress-nginx --create-namespace --dry-run

# every value the chart accepts
helm show values ingress-nginx/ingress-nginx > all-values.yaml

# what values is my running release actually using?
helm get values ingress-nginx -n ingress-nginx
CommandAnswers
helm templatewhat YAML would this produce?
--dry-runwould the cluster accept it?
helm show valueswhat can I even configure?
helm get valueswhat is my running release using?

Upgrades and Rollback

Helm keeps every Kubernetes release revision so a bad upgrade can be rolled back

Every helm upgrade creates a new revision. The old ones are kept.

helm history ingress-nginx -n ingress-nginx
# REVISION  STATUS       CHART                  DESCRIPTION
# 1         superseded   ingress-nginx-4.11.2   Install complete
# 2         superseded   ingress-nginx-4.12.0   Upgrade complete
# 3         deployed     ingress-nginx-4.12.1   Upgrade complete
 
helm rollback ingress-nginx 1 -n ingress-nginx
# Rollback was a success! Happy Helming!

A rollback is itself a new revision. Going back from 3 to 1 creates revision 4, so the history is never lost.

Helm Commands Quick Reference

commands
# repos
helm repo add <name> <url>
helm repo update
helm repo list

# install and upgrade
helm upgrade --install <release> <repo>/<chart> -n <ns> --create-namespace
helm upgrade --install <release> <repo>/<chart> -n <ns> -f my-values.yaml
helm upgrade --install <release> <repo>/<chart> -n <ns> --set key=value

# look
helm list -n <ns>
helm list -A                        # every namespace
helm status <release> -n <ns>
helm get values <release> -n <ns>
helm show values <repo>/<chart>

# undo
helm history <release> -n <ns>
helm rollback <release> <revision> -n <ns>

# before you commit to anything
helm template <release> <repo>/<chart>
helm upgrade --install <release> <repo>/<chart> --dry-run

# remove
helm uninstall <release> -n <ns>
CommandWhat it does
helm repo add <name> <url>remember a chart repository
helm repo updaterefresh the chart index
helm upgrade --installinstall, or upgrade if already there
helm list -Aevery release, every namespace
helm show values <chart>everything you may override
helm get values <release>what this release is actually using
helm history <release>every revision
helm rollback <release> <n>go back to a revision
helm uninstall <release>remove it

What's Next

Helm turns a twenty file install into one repeatable command. An Ingress Controller turns many load balancers into one. The official Helm documentation (opens in a new tab) and the ingress-nginx install guide (opens in a new tab) go deeper.

← Back to Connection Pooling


Frequently Asked Questions

What is Helm in Kubernetes?

Helm is the package manager for Kubernetes. The same idea as apt or npm, except the package is a set of Kubernetes objects. Instead of writing twenty YAML files yourself, you install a chart somebody already wrote and tested. You then override only the values you care about.

What is the difference between a Helm chart and a release?

A chart is the package, a folder of templated YAML plus a values file of defaults. A release is one installed instance of that chart, with a name you chose and a revision number. Install the same chart twice with different names and you get two separate releases.

What is the difference between Ingress and an Ingress Controller?

An Ingress is a YAML object holding routing rules. It says which host and path goes to which Service. It routes nothing by itself. An Ingress Controller is a Pod actually running software like nginx. It watches for Ingress objects and turns them into real configuration. Without a controller installed, your Ingress object sits there doing nothing and gives no error.

How do you install an ingress controller with Helm?

Three commands. Add the repository with helm repo add ingress-nginx and the chart URL. Refresh it with helm repo update. Then run helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx with a namespace. Add --set controller.service.type=NodePort if your cluster has no cloud load balancer.

Why use helm upgrade --install instead of helm install?

Because it works whether or not the release already exists. helm install fails if the release is there, and helm upgrade fails if it is not. The combined form installs on the first run and upgrades on every run after. That makes it safe to put in a script or a pipeline.

What is the default service type in the ingress-nginx chart?

LoadBalancer. The chart values file sets controller.service.type to LoadBalancer. That is right on AWS, Google Cloud or Azure, where a real load balancer can be created for you. On bare metal, minikube or a plain EC2 cluster nothing creates one. You override it to NodePort instead.

How do you find the NodePort of the ingress controller?

Run kubectl get svc in the ingress-nginx namespace, against the ingress-nginx-controller service. Use a jsonpath of spec.ports index zero and nodePort. That prints just the HTTP port number, usually something in the 30000 to 32767 range. You then reach the cluster at any node IP plus that port.

Why is my Ingress showing CLASS none?

Because the ingressClassName field is missing from your Ingress, or it does not match an IngressClass that exists. Run kubectl get ingressclass to see what is available. With ingress-nginx the class is usually called nginx. Without a matching class no controller claims the Ingress, so nothing routes.