Install, repos & using charts
helm install from a public chart.
A Helm repository works like an app store for Kubernetes (the system that runs your containers, the self-contained bundles of an app plus everything it needs to run, across a fleet of machines). Someone has already packaged a piece of software (an ingress controller, which is the traffic cop that routes outside web requests to the right service inside the cluster, or a database, or a whole monitoring stack) into a chart, stamped it with a version number, and published it to a catalog you can point Helm at. Helm is the package manager for Kubernetes: it takes one of those packages and turns it into running objects in your cluster. Your job in this lesson is the shopper's job. You add a store, browse the shelves, read the label to see what is configurable, install one item as a named release, watch it, and take it back off the shelf when you are done.
You do not need to know yet how a chart is built. You need to install one cleanly, know what it dropped into your cluster, and be able to remove it without leaving debris. That last part matters more than it sounds. A chart you install is code someone else wrote, and it runs with whatever permissions the chart asks for. Treat the install as a security decision from the very first command. That is the habit worth building.
Add a Repo and Find a Chart
A repository is nothing more than a web address serving one file, index.yaml (a plain-text file written in YAML, a human-readable configuration format), that lists every chart and every version the repo hosts. You give that address a short local nickname with helm repo add, and after that you refer to charts as nickname/chartname. It is the same trick apt (the Debian and Ubuntu package tool) or brew (Homebrew on a Mac) use: name a source once, then pull from it by short name for good.
# add two public chart repositories (each is a named index of charts)helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts# re-fetch the local copy of what each repo currently offershelm repo update
"ingress-nginx" has been added to your repositories"prometheus-community" has been added to your repositoriesHang tight while we grab the latest from your chart repositories......Successfully got an update from the "ingress-nginx" chart repository...Successfully got an update from the "prometheus-community" chart repositoryUpdate Complete. ⎈Happy Helming!⎈
Helm keeps that index on disk, so what you search is a cache, not a live view of the repo. Until you run helm repo update, every search you do only sees the versions from your last pull. Hold onto that fact, because it is the root of a reproducibility problem later in this lesson. Use helm search repo to look through the repos you have added, and helm search hub to query Artifact Hub, the public directory that indexes thousands of charts across many repos.
# search the repos you have added; --versions lists every published versionhelm search repo ingress-nginx --versions# search Artifact Hub, the public directory of charts across many reposhelm search hub prometheus --max-col-width=60
NAME CHART VERSION APP VERSION DESCRIPTIONingress-nginx/ingress-nginx 4.11.3 1.11.3 Ingress controller for Kubernetes using NG...ingress-nginx/ingress-nginx 4.11.2 1.11.2 Ingress controller for Kubernetes using NG...ingress-nginx/ingress-nginx 4.11.1 1.11.1 Ingress controller for Kubernetes using NG...ingress-nginx/ingress-nginx 4.11.0 1.11.0 Ingress controller for Kubernetes using NG...URL CHART VERSION APP VERSION DESCRIPTIONhttps://artifacthub.io/packages/helm/prometheus-communi... 27.5.1 v3.1.0 Prometheus collects metrics from configu...https://artifacthub.io/packages/helm/prometheus-communi... 68.4.3 v0.80.1 kube-prometheus-stack collects Kubernetes...
Adding a repo is a trust decision. The moment you run helm repo add, you have told your tooling to fetch, and one day run, whatever that web address serves. If the address came from a random blog post, or a look-alike domain one character off from the real project, you can end up installing a chart that mounts the host filesystem, runs as root, or ships a container image nobody reviewed. Get the URL from the project's own documentation, prefer an https address, and if the repo offers signed charts, plan to check the signature. That check is the last section of this lesson.
Read the Label Before You Install
Reading the label is helm show. Before you commit to anything, helm show chart prints the package's own metadata, and helm show values prints every setting the chart exposes together with its default. Those defaults are the label. They tell you what the chart will do to your cluster if you change nothing.
# print the chart's metadata: name, versions, source, and requirementshelm show chart ingress-nginx/ingress-nginx --version 4.11.3
annotations:artifacthub.io/changes: |- Update Ingress-Nginx version controller-v1.11.3artifacthub.io/prerelease: "false"apiVersion: v2appVersion: 1.11.3description: Ingress controller for Kubernetes using NGINX as a reverse proxy andload balancerhome: https://github.com/kubernetes/ingress-nginxicon: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Nginx_logo.svg/500px-Nginx_logo.svg.pngkeywords:- ingress- nginxkubeVersion: '>=1.21.0-0'maintainers:- name: cpanato- name: Gacko- name: puerco- name: rikatz- name: strongjz- name: tao12345666333name: ingress-nginxsources:- https://github.com/kubernetes/ingress-nginxversion: 4.11.3
The line that matters most here is kubeVersion: '>=1.21.0-0', the chart's own statement that it needs Kubernetes 1.21 or newer, so you learn about a version mismatch now instead of mid-install. Two more lines are worth pulling apart in your head. appVersion (1.11.3) is the software inside the box, the NGINX controller itself. version (4.11.3) is the box, the packaging around it. They move on separate clocks, and mixing them up is how people install a chart update expecting an app update and get neither.
Now dump the values so you can read through what is adjustable. This is where the security-relevant knobs live. For ingress-nginx, the settings that decide how locked-down the container runs sit under controller.image, so pull those out first.
# write every overridable value, with its default, to a file you can reviewhelm show values ingress-nginx/ingress-nginx --version 4.11.3 > default-values.yaml# pull out the controller image's security-relevant settingsgrep -nB2 -A4 'runAsUser: 101' default-values.yaml
46- runAsNonRoot: true47- # www-data -> uid 10148: runAsUser: 10149- allowPrivilegeEscalation: false50- seccompProfile:51- type: RuntimeDefault52- readOnlyRootFilesystem: false
Read that block slowly, because it is the real reason you look before you install. The controller runs as a non-root user (user id 101, the www-data account built into the image), it cannot grant itself more privileges once it is running (allowPrivilegeEscalation is false), and it runs under the RuntimeDefault seccomp profile (seccomp, short for secure computing mode, is a kernel feature that limits which system calls a process is even allowed to make). One default stands out. readOnlyRootFilesystem is false, which means the container can write to its own root filesystem. That is exactly the kind of default you might flip to true in your own values file. You only get to make that call because you looked.
One security setting is missing from that list on purpose: the container's Linux capabilities (the fine-grained slices of root's power that the kernel hands out one at a time, so a process can do a single privileged thing without being all-powerful). This chart does not expose the capability set as a plain value. It bakes the list into its own templates. To see the security context the chart will really apply, render one template to text with helm template --show-only, which prints a single template file as YAML and never contacts the cluster.
# render just the controller Deployment to text and read its security contexthelm template my-ingress ingress-nginx/ingress-nginx --version 4.11.3 \--show-only templates/controller-deployment.yaml \| grep -B1 -A10 'runAsNonRoot: true'
securityContext:runAsNonRoot: truerunAsUser: 101allowPrivilegeEscalation: falseseccompProfile:type: RuntimeDefaultcapabilities:drop:- ALLadd:- NET_BIND_SERVICEreadOnlyRootFilesystem: false
There it is in full. The chart drops every capability (drop: ALL) and then adds exactly one back, NET_BIND_SERVICE, the permission that lets a process bind to low-numbered ports like 80 and 443 without running as root. That is a careful default, and now you can prove it rather than hope for it. If a chart instead ran as user 0 (root) with nothing dropped, this render is where you would catch it before it went live. To change any of it, set controller.containerSecurityContext in your own values file, but know that your block replaces the computed one wholesale, so you have to restate the good parts you want to keep.
Install a Chart as a Release
Now the purchase. helm install takes two things you choose, a release name (your handle for this particular running instance) and a chart reference, and it creates the Kubernetes objects. Pin the exact chart version with --version, drop it into its own namespace (a named partition inside the cluster) with --namespace and --create-namespace, and pass a few quick overrides with --set. Deeper configuration through values files is the next lesson. Before you commit, render everything with nothing sent to the cluster and read what would happen.
# render the release locally and read the top of it; nothing is sent to the clusterhelm install my-ingress ingress-nginx/ingress-nginx \--version 4.11.3 --namespace ingress-nginx \--set controller.replicaCount=2 \--dry-run=client | head -n 14
NAME: my-ingressLAST DEPLOYED: Fri Jul 17 10:31:02 2026NAMESPACE: ingress-nginxSTATUS: pending-installREVISION: 1TEST SUITE: NoneHOOKS:---# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-createSecret.yamlapiVersion: batch/v1kind: Jobmetadata:name: my-ingress-ingress-nginx-admission-createnamespace: ingress-nginx
--dry-run=client renders every template with your overrides applied and prints the exact YAML Helm would send, without sending it. This is your review step. Right after the summary comes the HOOKS section. Hooks are Jobs (short-lived pods that run one task and then exit) that Helm fires at set points during an install, like a setup crew that comes in before the doors open. This chart runs two of them: one creates a TLS certificate (the credential behind an encrypted connection) and one registers the chart's admission webhook, the check Kubernetes runs against new objects before it accepts them. A hook Job runs with real cluster access, so read it as carefully as anything else. Below the hooks sits the MANIFEST section, which lists every object the release creates. Read the ServiceAccount (the identity the pods run as), the ClusterRole (a permission grant that spans the whole cluster), and the securityContext blocks. A careless or hostile chart shows its hand right here: a ClusterRole granting every verb on every resource, a hostPath volume (a mount straight from the node's own disk) pointing at the root of the node, a pod set to privileged: true. Catching that on a dry run costs a minute. Catching it after install can cost a cluster.
# create the release for real, into its own namespacehelm install my-ingress ingress-nginx/ingress-nginx \--namespace ingress-nginx --create-namespace \--version 4.11.3 \--set controller.replicaCount=2
NAME: my-ingressLAST DEPLOYED: Fri Jul 17 10:32:14 2026NAMESPACE: ingress-nginxSTATUS: deployedREVISION: 1TEST SUITE: NoneNOTES:The ingress-nginx controller has been installed.It may take a few minutes for the load balancer IP to be available.You can watch the status by running:kubectl --namespace ingress-nginx get services -o wide -w my-ingress-ingress-nginx-controller
Track, Upgrade, and Remove
Once installed, the release is a first-class object Helm tracks, with its own revision history, like a receipt and a return slip kept in the same envelope. helm list shows what is deployed and which chart version each release runs. helm status returns the release health plus the chart's NOTES text, which usually tells you how to reach the app.
# list releases across every namespace, with chart and app versionshelm list --all-namespaces
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSIONmy-ingress ingress-nginx 1 2026-07-17 10:32:14.882 +0000 UTC deployed ingress-nginx-4.11.3 1.11.3
To change anything, bump the version or tweak a value, do not run install again (it errors if the name is already taken). Run helm upgrade --install instead. That one command installs when the release is absent and upgrades when it already exists, which makes it idempotent: run it once or run it five times against the same cluster and you land in the same place. That property is why it is safe to drop into a pipeline that might hit a fresh cluster or an existing one. Every run adds a revision to the history.
# idempotent deploy: install if absent, upgrade if presenthelm upgrade --install my-ingress ingress-nginx/ingress-nginx \--namespace ingress-nginx --version 4.11.4# see the revision trail Helm keepshelm history my-ingress -n ingress-nginx
Release "my-ingress" has been upgraded. Happy Helming!NAME: my-ingressLAST DEPLOYED: Fri Jul 17 10:45:02 2026NAMESPACE: ingress-nginxSTATUS: deployedREVISION: 2REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION1 Fri Jul 17 10:32:14 2026 superseded ingress-nginx-4.11.3 1.11.3 Install complete2 Fri Jul 17 10:45:02 2026 deployed ingress-nginx-4.11.4 1.11.4 Upgrade complete
That revision trail is your undo button and your audit log at once. If revision 2 misbehaves, helm rollback drops you back to a known-good revision, and Helm records the rollback as a new revision instead of pretending the bad one never happened. When you are done, helm uninstall removes the release and the objects it created. Add --keep-history if you want the trail preserved for the record.
# return to the previous good revision if an upgrade goes wronghelm rollback my-ingress 1 -n ingress-nginx# remove the release and its resources (add --keep-history to keep the trail)helm uninstall my-ingress -n ingress-nginx
Rollback was a success! Happy Helming!release "my-ingress" uninstalled
Verify What You Are Actually Running
Pinning a version tells Helm which package to fetch. It does not tell you the package was not tampered with on the way to you or on the mirror it came from. For that, charts can be signed. A publisher signs the chart with a private key and ships a small companion file, the provenance file, ending in .prov, that carries a checksum and the signature (think of it as a sealed record of what the chart is and who vouched for it). With the publisher's public key sitting in a local keyring (your on-disk file of trusted public keys), helm pull --verify checks that the chart's checksum matches and the signature is genuine before Helm will touch it. It is the same instinct as checking the wax seal on a letter before you trust what is inside.
# fetch a chart and verify its provenance before installing anythinghelm pull ingress-nginx/ingress-nginx --version 4.11.3 \--verify --keyring ~/.gnupg/pubring.gpg
Error: failed to fetch provenance "https://github.com/kubernetes/ingress-nginx/releases/download/helm-chart-4.11.3/ingress-nginx-4.11.3.tgz.prov"
That error is not a bug in your setup. It is the honest state of much of the public ecosystem: plenty of popular repos, ingress-nginx among them, publish no .prov files at all, so --verify has nothing to check and refuses to go further. Knowing that changes how you treat an unsigned chart. When you cannot verify a signature, fall back to inspecting the goods by hand. Pull and unpack the chart, then read what it actually contains before it runs.
# download and unpack the chart so you can read its templates before it runshelm pull ingress-nginx/ingress-nginx --version 4.11.3 --untarls ingress-nginx/templates/controller-*role*.yaml ingress-nginx/templates/controller-serviceaccount.yaml
ingress-nginx/templates/controller-clusterrole.yamlingress-nginx/templates/controller-clusterrolebinding.yamlingress-nginx/templates/controller-role.yamlingress-nginx/templates/controller-rolebinding.yamlingress-nginx/templates/controller-serviceaccount.yaml
Notice the chart grants itself both a namespaced Role and a cluster-wide ClusterRole. Open controller-clusterrole.yaml and controller-serviceaccount.yaml and read what permissions the chart hands itself. You are hunting the same red flags you would in the dry-run output: wildcard verbs on wildcard resources, a cluster-wide role where a namespaced one would do the job, host mounts that reach into the node. Reading someone else's chart the first time you use it is cheap insurance. It is the difference between installing software and installing a workload you have never laid eyes on.
helm install web ingress-nginx/ingress-nginx three weeks apart with no --version flag. The second run brings up a newer controller image and different resource limits. Which explanation fits?helm show chart ingress-nginx/ingress-nginx --version 4.11.3 prints kubeVersion: '>=1.21.0-0'. Your cluster runs 1.19. What should you take from that line?templates/controller-deployment.yaml shows capabilities with drop: ALL and add: NET_BIND_SERVICE, plus readOnlyRootFilesystem: false. You add only controller.containerSecurityContext.readOnlyRootFilesystem: true to your values. What does the next render show?Before you close the terminal, run helm list -A one more time and confirm every release is one you or your team meant to create. A release you do not recognize, sitting in a namespace nobody claims, is a workload running with permissions no one is watching, and that is worth chasing down while it is still a question and not yet an incident.
Try this
Run helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: an unpinned install is not reproducible. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.