Helm charts declared in k0s config: Flannel, Traefik, cert-manager, Prometheus
Published: 2026-04-10
One of k0s's underused features is declaring Helm chart installations directly in /etc/k0s/k0s.yaml. Instead of running helm install after the cluster starts, you list the charts under spec.extensions.helm and k0s reconciles them automatically on startup.
How it works
The k0s controller includes a Helm controller. When it reads the cluster config, it creates Chart custom resources in kube-system for each entry under extensions.helm.charts. The Helm controller installs or upgrades the chart — same as FluxCD's HelmRelease controller, but without needing FluxCD.
yamlspec:
extensions:
helm:
concurrencyLevel: 5
repositories:
- name: flannel
url: https://flannel-io.github.io/flannel
- name: traefik
url: https://traefik.github.io/charts
- name: jetstack
url: https://charts.jetstack.io
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
charts:
- name: flannel
chartname: flannel/flannel
namespace: kube-flannel
order: 1
- name: traefik
chartname: traefik/traefik
namespace: traefik
order: 2
- name: cert-manager
chartname: jetstack/cert-manager
namespace: cert-manager
order: 3
- name: kube-prometheus-stack
chartname: prometheus-community/kube-prometheus-stack
namespace: monitoring
order: 4
order controls installation sequence:
1— Flannel (CNI). Nothing reachesRunningwithout a working network.2— Traefik. Needs network.3— cert-manager. Its webhook must be ready before anyCertificateobjects are created.4— Prometheus. Needs everything above.
Flannel
Flannel is the CNI. Backend is vxlan — wraps pod traffic in UDP datagrams, works without special kernel routing. Pod CIDR: 10.244.0.0/16.
yamlvalues: |
podCidr: "10.244.0.0/16"
flannel_backend_type: "vxlan"
No Calico or Cilium. Single node, no node-to-node routing to manage.
Traefik
yamlvalues: |
hostNetwork: true
service:
type: ClusterIP
ports:
web:
redirectTo:
port: websecure
ingressRoute:
dashboard:
enabled: false
providers:
kubernetesCRD:
enabled: true
kubernetesIngress:
enabled: true
hostNetwork: true lets Traefik bind directly to host ports 80 and 443 — no cloud LoadBalancer needed. ClusterIP means no NodePort. HTTP immediately redirects to HTTPS.
Dashboard is disabled — it's not needed and exposes internal routing information.
cert-manager
yamlvalues: |
installCRDs: true
global:
leaderElection:
namespace: cert-manager
installCRDs: true — Certificate, ClusterIssuer, and related CRDs are installed as part of the Helm chart. Without this, you'd need to apply CRDs separately before the chart, breaking the single-config approach.
After cert-manager is ready, create a ClusterIssuer for Let's Encrypt:
yamlapiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: traefik
Prometheus (kube-prometheus-stack)
yamlvalues: |
prometheus:
prometheusSpec:
retention: 7d
storageSpec:
emptyDir:
medium: ""
sizeLimit: 5Gi
alertmanager:
enabled: false
prometheus-pushgateway:
enabled: false
grafana:
enabled: true
persistentVolume: false / emptyDir — on a VPS with limited disk, a PVC for Prometheus isn't worth it. If the pod restarts, you lose recent history. Acceptable for personal monitoring.
alertmanager: false and pushgateway: false save memory. kube-state-metrics and node-exporter are enabled — pod/node metrics visible in Grafana and Beszel.
Checking chart status
bashk0s kubectl get charts.helm.k0sproject.io -A
Example output:
NAMESPACE NAME VERSION STATUS ...
kube-system flannel 0.26.0 deployed
kube-system traefik 32.1.0 deployed
kube-system cert-manager v1.16.0 deployed
kube-system kube-prometheus-stack 68.0.0 deployed
Status: deployed, pending-install, failed. First place to look when a chart fails.
Get events for a specific chart:
bashk0s kubectl describe chart.helm.k0sproject.io cert-manager -n kube-system
Upgrading charts
Change the version: field in /etc/k0s/k0s.yaml:
yaml- name: traefik
chartname: traefik/traefik
version: "33.0.0"
Then restart k0scontroller:
bashk0s stop
k0s start
Or reload the config without full restart:
bashk0s config update /etc/k0s/k0s.yaml
Limitations
- No
helm diffbefore apply — k0s applies blindly on startup - No rollback UI — manual
helm rollbackin the chart's namespace - No support for OCI registry charts (as of k0s v1.30)
- Values must be inline YAML in
k0s.yaml— no separate values files
For complex charts with many values, managing inline YAML gets unwieldy. At that point, move to FluxCD HelmReleases.
What can go wrong
Chart fails to install and k0s keeps retrying.
k0s applies chart specs on every startup. If a chart errors, check logs: journalctl -u k0scontroller | grep -i helm. Fix the values in k0s.yaml and restart: systemctl restart k0scontroller.
Values YAML in k0s.yaml breaks the entire config.
k0s.yaml is parsed as YAML; an indentation error in the inline values block will prevent k0s from starting. Always validate with k0s config validate before restarting the controller.
Chart version drift.
k0s installs charts from a HelmRepository on first start. If you don't pin the version: field, a cluster rebuild gets a different chart version than the original. Always pin chart versions.
Summary
- Charts declared in
spec.extensions.helmink0s.yamlare installed automatically on cluster startup — no separatehelm installstep - This replaces the need for FluxCD for the initial cluster bootstrap layer (Flannel, Traefik, cert-manager, Prometheus)
- Limitations: no
helm diff, no rollback UI, no OCI registry support (as of k0s v1.30) - Pin chart versions explicitly; leaving version unpinned means a cluster rebuild gets whatever is current in the repo at that moment
- For charts with many values or complex per-environment overrides, move to FluxCD HelmReleases instead