CoursesHelmInstall, repos & using charts

Install, repos & using charts

helm install from a public chart.

Intermediate12 min · lesson 2 of 12

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.

terminal
# add two public chart repositories (each is a named index of charts)
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# re-fetch the local copy of what each repo currently offers
helm repo update
output
"ingress-nginx" has been added to your repositories
"prometheus-community" has been added to your repositories
Hang 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 repository
Update 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.

terminal
# search the repos you have added; --versions lists every published version
helm search repo ingress-nginx --versions
# search Artifact Hub, the public directory of charts across many repos
helm search hub prometheus --max-col-width=60
output
NAME CHART VERSION APP VERSION DESCRIPTION
ingress-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 DESCRIPTION
https://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.

terminal
# print the chart's metadata: name, versions, source, and requirements
helm show chart ingress-nginx/ingress-nginx --version 4.11.3
output
annotations:
artifacthub.io/changes: |
- Update Ingress-Nginx version controller-v1.11.3
artifacthub.io/prerelease: "false"
apiVersion: v2
appVersion: 1.11.3
description: Ingress controller for Kubernetes using NGINX as a reverse proxy and
load balancer
home: https://github.com/kubernetes/ingress-nginx
icon: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Nginx_logo.svg/500px-Nginx_logo.svg.png
keywords:
- ingress
- nginx
kubeVersion: '>=1.21.0-0'
maintainers:
- name: cpanato
- name: Gacko
- name: puerco
- name: rikatz
- name: strongjz
- name: tao12345666333
name: ingress-nginx
sources:
- https://github.com/kubernetes/ingress-nginx
version: 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.

terminal
# write every overridable value, with its default, to a file you can review
helm show values ingress-nginx/ingress-nginx --version 4.11.3 > default-values.yaml
# pull out the controller image's security-relevant settings
grep -nB2 -A4 'runAsUser: 101' default-values.yaml
output
46- runAsNonRoot: true
47- # www-data -> uid 101
48: runAsUser: 101
49- allowPrivilegeEscalation: false
50- seccompProfile:
51- type: RuntimeDefault
52- 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.

terminal
# render just the controller Deployment to text and read its security context
helm template my-ingress ingress-nginx/ingress-nginx --version 4.11.3 \
--show-only templates/controller-deployment.yaml \
| grep -B1 -A10 'runAsNonRoot: true'
output
securityContext:
runAsNonRoot: true
runAsUser: 101
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
readOnlyRootFilesystem: 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.

terminal
# render the release locally and read the top of it; nothing is sent to the cluster
helm install my-ingress ingress-nginx/ingress-nginx \
--version 4.11.3 --namespace ingress-nginx \
--set controller.replicaCount=2 \
--dry-run=client | head -n 14
output
NAME: my-ingress
LAST DEPLOYED: Fri Jul 17 10:31:02 2026
NAMESPACE: ingress-nginx
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
---
# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-createSecret.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: my-ingress-ingress-nginx-admission-create
namespace: 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.

terminal
# create the release for real, into its own namespace
helm install my-ingress ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--version 4.11.3 \
--set controller.replicaCount=2
output
NAME: my-ingress
LAST DEPLOYED: Fri Jul 17 10:32:14 2026
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
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
The chart lifecycle, from trust to teardown
1helm repo add + update
trust a source, cache its index
2helm show values
read the label and defaults
3helm install --dry-run=client
render and review the manifest
4helm install --version
create the named release
5helm list / status
confirm it is deployed
6helm upgrade --install
change it idempotently
7helm rollback / uninstall
undo or remove cleanly

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.

terminal
# list releases across every namespace, with chart and app versions
helm list --all-namespaces
output
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
my-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.

terminal
# idempotent deploy: install if absent, upgrade if present
helm upgrade --install my-ingress ingress-nginx/ingress-nginx \
--namespace ingress-nginx --version 4.11.4
# see the revision trail Helm keeps
helm history my-ingress -n ingress-nginx
output
Release "my-ingress" has been upgraded. Happy Helming!
NAME: my-ingress
LAST DEPLOYED: Fri Jul 17 10:45:02 2026
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 2
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Fri Jul 17 10:32:14 2026 superseded ingress-nginx-4.11.3 1.11.3 Install complete
2 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.

terminal
# return to the previous good revision if an upgrade goes wrong
helm 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
output
Rollback was a success! Happy Helming!
release "my-ingress" uninstalled
An unpinned install is not reproducible
Leave off --version and Helm installs whatever chart is newest in your locally cached index, and that cache is only as current as your last helm repo update. Two engineers running the byte-for-byte same helm install a few weeks apart can land on different chart versions, and a newer chart can quietly change the default image tag, the resource requests, or the rendered manifest with nothing changed on your side. That is how a 'we changed nothing' incident actually begins. Pass an explicit --version in anything you expect to reproduce (continuous-integration jobs, runbooks, and GitOps sources, which manage infrastructure through files kept in git), and where the chart allows it, pin the application image tag through your values too, so both the packaging and the software inside it are locked.

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.

terminal
# fetch a chart and verify its provenance before installing anything
helm pull ingress-nginx/ingress-nginx --version 4.11.3 \
--verify --keyring ~/.gnupg/pubring.gpg
output
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.

terminal
# download and unpack the chart so you can read its templates before it runs
helm pull ingress-nginx/ingress-nginx --version 4.11.3 --untar
ls ingress-nginx/templates/controller-*role*.yaml ingress-nginx/templates/controller-serviceaccount.yaml
output
ingress-nginx/templates/controller-clusterrole.yaml
ingress-nginx/templates/controller-clusterrolebinding.yaml
ingress-nginx/templates/controller-role.yaml
ingress-nginx/templates/controller-rolebinding.yaml
ingress-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.

Quick check
01Two teammates run 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?
Correct — With nothing pinned, each run takes whatever the local index calls newest at that moment, and the index had moved on.
Incorrect — A published version is fixed once it ships. A different result means a different version got picked up, not a rewritten one.
Incorrect — The image tag is baked into a particular chart version's values. A new tag is evidence you landed on a new chart.
Incorrect — A stale cache would have held them back, not pushed them forward. Getting something newer points the other way.
02helm 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?
Incorrect — That constraint describes the cluster the chart runs on. A newer Helm binary does not change whether 1.19 satisfies it.
Incorrect — A chart places workloads onto a cluster that already exists. Nothing inside one upgrades Kubernetes itself.
Correct — Reading the metadata turns a failure partway through an install into a planning item you settle before anything is created.
Incorrect — In this same output appVersion is 1.11.3 and version is 4.11.3. The kubeVersion range describes the cluster, not either of those.
03Rendering 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?
Incorrect — The chart assembles that block as one unit, so what you supply lands in place of it rather than alongside it.
Correct — The override is wholesale, which makes keeping the good defaults your job the moment you touch that block at all.
Incorrect — Nothing moves anywhere. The container level block is simply built from what you handed in, and the rest is not carried over.
Incorrect — Helm renders what you give it without complaint, which is exactly why losing the capability drop is so easy to miss.

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.

Related