k0s Helm - Kubernetes Package Manager
Status: Active
Last Updated: 2026-01-30
Category: Containers - Kubernetes
Prerequisites: k0s-introduction, k0s-storage
Time: 2-3 hours
Tags: k0s, kubernetes, helm, charts, packages
Summary
Master Helm package manager for Kubernetes to deploy applications with pre-configured charts. Learn Helm basics, chart structure, repositories, releases, values customization, and managing application lifecycle in k0s clusters.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Install and configure Helm
- โ Use Helm repositories
- โ Deploy applications with charts
- โ Customize chart values
- โ Manage releases
- โ Create custom charts
- โ Debug Helm deployments
๐ฆ What is Helm?
The Problem
Manual Kubernetes deployment:
# Deploy application manually
k0s kubectl apply -f deployment.yaml
k0s kubectl apply -f service.yaml
k0s kubectl apply -f ingress.yaml
k0s kubectl apply -f configmap.yaml
k0s kubectl apply -f secret.yaml
# ... 20+ files
Issues:
- ๐ Too many YAML files
- ๐ Hard to upgrade
- โ Difficult to rollback
- ๐๏ธ No parameterization
- ๐ฆ Not portable
The Solution: Helm
Helm is the Kubernetes package manager:
- ๐ฆ Packages (charts) with all resources
- ๐ฏ One-command deployment
- ๐ Easy upgrades and rollbacks
- ๐๏ธ Parameterized values
- ๐ Shared via repositories
Think: APT for Kubernetes
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Helm CLI (helm) โ
โ (runs on your machine) โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Kubernetes API Server โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Helm Release (in ConfigMap) โ โ
โ โ - Release name โ โ
โ โ - Chart version โ โ
โ โ - Values โ โ
โ โ - Deployed resources โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ง Installing Helm
Install Helm CLI
Linux:
# Download script
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Or manual
curl -LO https://get.helm.sh/helm-v3.13.3-linux-amd64.tar.gz
tar -zxvf helm-v3.13.3-linux-amd64.tar.gz
sudo mv linux-amd64/helm /usr/local/bin/helm
macOS:
brew install helm
Windows:
choco install kubernetes-helm
# Or
scoop install helm
Verify:
helm version
# version.BuildInfo{Version:"v3.13.3", GitCommit:"..."}
Configure Helm for k0s
Set kubeconfig:
# Export k0s kubeconfig
sudo k0s kubeconfig admin > ~/.kube/k0s-config
# Use it
export KUBECONFIG=~/.kube/k0s-config
# Or set permanently
echo 'export KUBECONFIG=~/.kube/k0s-config' >> ~/.bashrc
Test connection:
helm list
# Should connect to k0s cluster (empty list initially)
๐ Helm Repositories
Add Repositories
Popular repositories:
# Bitnami (most popular)
helm repo add bitnami https://charts.bitnami.com/bitnami
# Stable (official)
helm repo add stable https://charts.helm.sh/stable
# Prometheus
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# Grafana
helm repo add grafana https://grafana.github.io/helm-charts
# Traefik
helm repo add traefik https://traefik.github.io/charts
# Update indexes
helm repo update
List repositories:
helm repo list
# NAME URL
# bitnami https://charts.bitnami.com/bitnami
# prometheus-community https://prometheus-community.github.io/helm-charts
Search Charts
Search in repositories:
# Search all repos
helm search repo nginx
# NAME CHART VERSION APP VERSION DESCRIPTION
# bitnami/nginx 15.4.4 1.25.3 NGINX Open Source is a web server...
# bitnami/nginx-ingress-controller 10.1.1 1.9.5 NGINX Ingress Controller...
# Search specific repo
helm search repo bitnami/postgres
# Search Helm Hub (all public charts)
helm search hub wordpress
๐ Deploying Applications
Install Chart
Basic installation:
# Install nginx
helm install my-nginx bitnami/nginx
# Output:
# NAME: my-nginx
# LAST DEPLOYED: Thu Jan 30 10:00:00 2026
# NAMESPACE: default
# STATUS: deployed
# REVISION: 1
Check release:
helm list
# NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
# my-nginx default 1 2026-01-30 10:00:00.123456 -0500 EST deployed nginx-15.4.4 1.25.3
Check resources:
k0s kubectl get all -l app.kubernetes.io/instance=my-nginx
# NAME READY STATUS RESTARTS AGE
# pod/my-nginx-123456-abc 1/1 Running 0 1m
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# service/my-nginx LoadBalancer 10.96.123.45 <pending> 80:30123/TCP
Install with Values
Get default values:
helm show values bitnami/nginx > values.yaml
values.yaml (excerpt):
replicaCount: 1
image:
registry: docker.io
repository: bitnami/nginx
tag: 1.25.3
service:
type: LoadBalancer
port: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
Customize values - my-values.yaml:
replicaCount: 3
service:
type: NodePort
nodePorts:
http: 30080
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
ingress:
enabled: true
hostname: nginx.example.com
Install with custom values:
helm install my-nginx bitnami/nginx -f my-values.yaml
Install with Inline Values
Override specific values:
helm install my-nginx bitnami/nginx \
--set replicaCount=3 \
--set service.type=NodePort \
--set service.nodePorts.http=30080
Multiple values:
helm install my-db bitnami/postgresql \
--set auth.username=admin \
--set auth.password=secret123 \
--set auth.database=myapp \
--set primary.persistence.size=10Gi
๐ Managing Releases
List Releases
# Current releases
helm list
# All namespaces
helm list -A
# Specific namespace
helm list -n production
# Include uninstalled
helm list --uninstalled
Upgrade Release
Upgrade to new version:
# Update repo
helm repo update
# Upgrade
helm upgrade my-nginx bitnami/nginx
# Upgrade with new values
helm upgrade my-nginx bitnami/nginx -f my-values.yaml
Upgrade with changes:
# Change replica count
helm upgrade my-nginx bitnami/nginx \
--set replicaCount=5 \
--reuse-values
Rollback Release
View history:
helm history my-nginx
# REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
# 1 Thu Jan 30 10:00:00 2026 superseded nginx-15.4.4 1.25.3 Install complete
# 2 Thu Jan 30 11:00:00 2026 superseded nginx-15.4.5 1.25.4 Upgrade complete
# 3 Thu Jan 30 12:00:00 2026 deployed nginx-15.4.6 1.25.5 Upgrade complete
Rollback:
# Rollback to previous version
helm rollback my-nginx
# Rollback to specific revision
helm rollback my-nginx 2
Uninstall Release
# Uninstall (delete resources)
helm uninstall my-nginx
# Keep history
helm uninstall my-nginx --keep-history
# Can see in history later
helm list --uninstalled
๐ Common Applications
PostgreSQL Database
Install:
helm install my-db bitnami/postgresql \
--set auth.username=admin \
--set auth.password=secret123 \
--set auth.database=myapp \
--set primary.persistence.storageClass=local-path \
--set primary.persistence.size=20Gi
Get connection info:
# Password stored in secret
export POSTGRES_PASSWORD=$(kubectl get secret --namespace default my-db-postgresql -o jsonpath="{.data.postgres-password}" | base64 -d)
# Connect
kubectl run my-db-postgresql-client --rm --tty -i --restart='Never' \
--namespace default \
--image docker.io/bitnami/postgresql:15 \
--env="PGPASSWORD=$POSTGRES_PASSWORD" \
--command -- psql --host my-db-postgresql -U postgres -d postgres -p 5432
Redis Cache
Install:
helm install my-cache bitnami/redis \
--set auth.password=redis123 \
--set master.persistence.storageClass=local-path \
--set master.persistence.size=5Gi
Connect:
export REDIS_PASSWORD=$(kubectl get secret --namespace default my-cache-redis -o jsonpath="{.data.redis-password}" | base64 -d)
kubectl run --namespace default redis-client --restart='Never' --rm -i --tty \
--env REDIS_PASSWORD=$REDIS_PASSWORD \
--image docker.io/bitnami/redis:7.2 \
-- bash
# Inside container
redis-cli -h my-cache-redis-master -a $REDIS_PASSWORD
WordPress
Install:
helm install my-blog bitnami/wordpress \
--set wordpressUsername=admin \
--set wordpressPassword=admin123 \
--set wordpressEmail=admin@example.com \
--set service.type=NodePort \
--set service.nodePorts.http=30180 \
--set mariadb.auth.rootPassword=secret123 \
--set persistence.storageClass=local-path \
--set persistence.size=10Gi
Access:
# Get node IP
NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}")
echo "WordPress URL: http://$NODE_IP:30180"
echo "Username: admin"
echo "Password: admin123"
Grafana
Install:
helm install grafana grafana/grafana \
--set adminPassword=admin123 \
--set service.type=NodePort \
--set service.nodePort=30300 \
--set persistence.enabled=true \
--set persistence.storageClassName=local-path \
--set persistence.size=5Gi
๐จ Creating Custom Charts
Generate Chart
Create chart skeleton:
helm create myapp
# Creates:
# myapp/
# โโโ Chart.yaml
# โโโ values.yaml
# โโโ templates/
# โ โโโ deployment.yaml
# โ โโโ service.yaml
# โ โโโ ingress.yaml
# โ โโโ ...
# โโโ charts/
Chart.yaml
Chart metadata:
apiVersion: v2
name: myapp
description: My application Helm chart
type: application
version: 0.1.0
appVersion: "1.0.0"
keywords:
- web
- nodejs
maintainers:
- name: Your Name
email: you@example.com
values.yaml
Default values:
replicaCount: 1
image:
repository: myapp
pullPolicy: IfNotPresent
tag: "latest"
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
hosts:
- host: myapp.local
paths:
- path: /
pathType: Prefix
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
env:
DATABASE_URL: "postgres://db:5432/myapp"
REDIS_URL: "redis://cache:6379"
templates/deployment.yaml
Use template syntax:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
Install Custom Chart
From directory:
# Test rendering
helm template myapp ./myapp
# Dry run
helm install myapp ./myapp --dry-run --debug
# Install
helm install myapp ./myapp -f myapp-prod-values.yaml
Package chart:
# Create .tgz
helm package myapp
# Output: myapp-0.1.0.tgz
# Install from package
helm install myapp myapp-0.1.0.tgz
๐ Debugging
Get Manifest
See what Helm deployed:
helm get manifest my-nginx
# Shows all Kubernetes YAML
Get Values
Current values:
helm get values my-nginx
# USER-SUPPLIED VALUES:
# replicaCount: 3
# service:
# type: NodePort
# All values (including defaults)
helm get values my-nginx --all
Template Debugging
Render templates locally:
# See what would be deployed
helm template my-nginx bitnami/nginx
# With custom values
helm template my-nginx bitnami/nginx -f my-values.yaml
# Debug mode
helm template my-nginx bitnami/nginx --debug
Dry Run
Test installation:
helm install my-nginx bitnami/nginx --dry-run --debug
Status
Check release status:
helm status my-nginx
# NAME: my-nginx
# LAST DEPLOYED: Thu Jan 30 10:00:00 2026
# NAMESPACE: default
# STATUS: deployed
# REVISION: 1
# TEST SUITE: None
# NOTES:
# ... (chart notes)
๐ก Best Practices
1. Use Values Files
# Good: Values in file
helm install myapp ./myapp -f production.yaml
# Avoid: Too many --set flags
helm install myapp ./myapp \
--set a=1 --set b=2 --set c=3 ... # โ Hard to maintain
2. Pin Chart Versions
# Good: Specific version
helm install myapp bitnami/nginx --version 15.4.4
# Avoid: Latest (can break)
helm install myapp bitnami/nginx # โ Gets latest
3. Namespace Isolation
# Create namespace
k0s kubectl create namespace production
# Install in namespace
helm install myapp bitnami/nginx -n production
4. Use --wait for CI/CD
# Wait for resources to be ready
helm install myapp ./myapp --wait --timeout 5m
# Rollback on failure
helm install myapp ./myapp --wait --atomic
5. Always Test First
# Dry run
helm install myapp ./myapp --dry-run --debug
# Template locally
helm template myapp ./myapp
# Then install
helm install myapp ./myapp
6. Document with NOTES.txt
templates/NOTES.txt:
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To access your application:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "myapp.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running: kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "myapp.fullname" . }}
{{- end }}
๐ What's Next?
Ingress:
- k0s-ingress - HTTP routing with Traefik
Monitoring:
- k0s-monitoring - Prometheus and Grafana
Advanced:
- Helm hooks for migrations
- Subchart dependencies
- Chart testing
๐ Resources
Helm Documentation:
Popular Charts:
๐ Change Log
2026-01-30
- Created Helm guide
- Explained Helm concepts
- Covered installation
- Demonstrated repositories
- Showed application deployment
- Covered release management
- Explained custom charts
- Added debugging techniques
- Included best practices
Next Article: k0s-ingress - Expose applications to the internet!