Installing Prometheus on Kubernetes
Install kube-prometheus-stack with Helm and watch all four node-exporters go into CrashLoopBackOff. The cause is one line in the logs and one line of ss output: the DaemonSet uses hostNetwork, and the host already had something on that port. Both fixes are shown, with the output of each.
Observability Guide 36 of 46 Beginner
- Kubernetes1.36.4
- Chartkube-prometheus-stack 88.5.3
- Prometheus Operatorv0.93.1
- Helmv3.18.5
- TimeAbout 22 min
- Reviewed22 August 2026
Written against the versions above. Chart and image versions move quickly. The behaviour in this guide - hostNetwork sharing the node's port space - is a property of the DaemonSet, not of a release.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A running cluster and a working
kubectl. Any of the cluster installation guides gets you there. - Helm installed locally.
helm versionshould answer. - Around 1 GB of spare memory across the cluster. This chart is not small.
-
Nothing is watching yet
A fresh cluster collects almost nothing.
metrics-servermay give youkubectl top, but that is a live snapshot with no history: you cannot ask it what happened twenty minutes ago, and it stores no series you can query.Prometheus is the other half. It scrapes HTTP endpoints on a schedule, keeps the samples as time series, and lets you ask questions across them. Start by confirming there is nothing in the namespace you are about to fill.
bash Example session kubectl --context cka1001 get pods -n monitoringNo resources found in monitoring namespace.kubectl --context cka1001 top nodesNAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)cka1001 130m 6% 1725Mi 52%cka1001-node01 51m 2% 990Mi 30%cka1001-node02 58m 2% 1125Mi 34%cka1001-node03 52m 2% 807Mi 24%Expected resultAn empty namespace, and four nodes with memory to spare. Note the memory percentages - you will want to compare them afterwards.
Success condition
No resources found in monitoring namespace. -
Add the chart
kube-prometheus-stackis the community chart that installs Prometheus, Alertmanager, Grafana,kube-state-metricsandnode-exportertogether, plus the Prometheus Operator that manages them through custom resources.It is worth knowing why that bundle exists. Prometheus on its own scrapes targets you configure by hand. The Operator lets you declare targets as
ServiceMonitorobjects instead, so adding a scrape target becomes a normal Kubernetes apply rather than a config-file edit and reload.bash Example session helm repo add prometheus-community https://prometheus-community.github.io/helm-charts"prometheus-community" already exists with the same configuration, skippinghelm repo update prometheus-communityHang tight while we grab the latest from your chart repositories......Successfully got an update from the "prometheus-community" chart repositoryUpdate Complete. ⎈Happy Helming!⎈helm search repo prometheus-community/kube-prometheus-stack --versions | head -3NAME CHART VERSION APP VERSION DESCRIPTIONprometheus-community/kube-prometheus-stack 88.5.3 v0.93.1 kube-prometheus-stack collects Kubernetes manif...prometheus-community/kube-prometheus-stack 88.5.2 v0.93.1 kube-prometheus-stack collects Kubernetes manif...level=WARN msg="repo is corrupt or missing" repo=traefik error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\traefik-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=grafana error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\grafana-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=rancher-latest error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\rancher-latest-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=argo error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\argo-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=portainer error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\portainer-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=aqua error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\aqua-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=cilium error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\cilium-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=kiali error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\kiali-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=istio error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\istio-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=metallb error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\metallb-index.yaml: The system cannot find the file specified."level=WARN msg="repo is corrupt or missing" repo=jetstack error="open C:\\Users\\Sunil\\AppData\\Local\\Temp\\helm\\repository\\jetstack-index.yaml: The system cannot find the file specified."Error: unable to write table output: write /dev/stdout: The pipe has been ended.Expected resultThe chart version and the app version it ships. Pin these in anything real - the chart moves faster than Kubernetes does.
Success condition
helm search repolists akube-prometheus-stackversion. -
Install it
Two values are set deliberately.
retention=6hkeeps the disk small for a lab; the chart's default is ten days. The memory request keeps Prometheus schedulable on a 4 GB node - leave the default and it may sitPendingon a small cluster, which is a scheduling problem wearing an observability costume.bash Example session kubectl --context cka1001 create namespace monitoringnamespace/monitoring createdhelm --kube-context cka1001 install kps prometheus-community/kube-prometheus-stack --namespace monitoring --set grafana.enabled=true --set prometheus.prometheusSpec.retention=6h --set prometheus.prometheusSpec.resources.requests.memory=400MiNAME: kpsLAST DEPLOYED: Sat Aug 22 02:18:37 2026NAMESPACE: monitoringSTATUS: deployedREVISION: 1DESCRIPTION: Install completeTEST SUITE: NoneNOTES:kube-prometheus-stack has been installed. Check its status by running: kubectl --namespace monitoring get pods -l "release=kps" Get Grafana 'admin' user password by running: kubectl --namespace monitoring get secrets kps-grafana -o jsonpath="{.data.admin-password}" | base64 -d ; echo Access Grafana local instance: export POD_NAME=$(kubectl --namespace monitoring get pod -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=kps" -oname) kubectl --namespace monitoring port-forward $POD_NAME 3000 Get your grafana admin user password by running: kubectl get secret --namespace monitoring -l app.kubernetes.io/component=admin-secret -o jsonpath="{.items[0].data.admin-password}" | base64 --decode ; echo Visit https://github.com/prometheus-operator/kube-prometheus for instructions on how to create & configure Alertmanager and Prometheus instances using the Operator.Expected resultHelm reports the release as deployed. That means the objects were accepted, not that the Pods are healthy - the next step is where that shows.
Success condition
STATUS: deployed. -
What it created, and what broke
Read this output carefully rather than waiting for it to settle. The operator,
kube-state-metricsand Grafana come up. All four node-exporters are inCrashLoopBackOff, and they got there within forty seconds, which rules out anything slow or resource-related.Four out of four is the useful detail. A crash on one node suggests something about that node; a crash on every node suggests something about the workload, or something true of all four hosts.
bash Example session kubectl --context cka1001 -n monitoring get podsNAME READY STATUS RESTARTS AGEalertmanager-kps-kube-prometheus-stack-alertmanager-0 0/2 PodInitializing 0 20skps-grafana-8497974b5-rq8dc 0/3 ContainerCreating 0 40skps-kube-prometheus-stack-operator-7b847d8968-hnsj2 1/1 Running 0 40skps-kube-state-metrics-659896646d-bqwdv 1/1 Running 0 40skps-prometheus-node-exporter-22sl7 0/1 CrashLoopBackOff 2 (17s ago) 40skps-prometheus-node-exporter-78hpk 0/1 CrashLoopBackOff 2 (12s ago) 40skps-prometheus-node-exporter-nxhj5 0/1 CrashLoopBackOff 2 (13s ago) 40skps-prometheus-node-exporter-pvwsz 0/1 CrashLoopBackOff 2 (17s ago) 40sprometheus-kps-kube-prometheus-stack-prometheus-0 0/2 Init:0/1 0 20skubectl --context cka1001 -n monitoring get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEalertmanager-operated ClusterIP None <none> 9093/TCP,9094/TCP,9094/UDP 23skps-grafana ClusterIP 10.111.173.89 <none> 80/TCP 43skps-kube-prometheus-stack-alertmanager ClusterIP 10.104.15.195 <none> 9093/TCP,8080/TCP 43skps-kube-prometheus-stack-operator ClusterIP 10.102.245.192 <none> 443/TCP 43skps-kube-prometheus-stack-prometheus ClusterIP 10.111.140.94 <none> 9090/TCP,8080/TCP 43skps-kube-state-metrics ClusterIP 10.106.192.114 <none> 8080/TCP 43skps-prometheus-node-exporter ClusterIP 10.101.129.169 <none> 9100/TCP 43sprometheus-operated ClusterIP None <none> 9090/TCP 23skubectl --context cka1001 get crd -o name | grep monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/alertmanagerconfigs.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/alertmanagers.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/podmonitors.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/probes.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/prometheusagents.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/prometheuses.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/prometheusrules.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/scrapeconfigs.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/servicemonitors.monitoring.coreos.comcustomresourcedefinition.apiextensions.k8s.io/thanosrulers.monitoring.coreos.comExpected resultA DaemonSet Pod per node, all failing, plus the CRDs the Operator installs -
prometheuses,servicemonitors,prometheusrulesand the rest.Success conditionYou can see
CrashLoopBackOffon everynode-exporterPod. -
The logs say it in one line
Skip the collector list - node-exporter prints every collector it enables at startup, and it is noise here. The last line of each attempt is the whole story:
listen tcp 0.0.0.0:9100: bind: address already in useNot a permission problem, not a missing mount. Something already holds port 9100. And this is a
hostNetworkDaemonSet: the Pod does not get its own network namespace, it uses the node's. So "already in use" means already in use *on the host*, by something that has nothing to do with Kubernetes.bash Example session kubectl --context cka1001 -n monitoring get pods -l app.kubernetes.io/name=prometheus-node-exporterNAME READY STATUS RESTARTS AGEkps-prometheus-node-exporter-22sl7 0/1 CrashLoopBackOff 3 (36s ago) 81skps-prometheus-node-exporter-78hpk 0/1 CrashLoopBackOff 3 (30s ago) 81skps-prometheus-node-exporter-nxhj5 0/1 CrashLoopBackOff 3 (29s ago) 81skps-prometheus-node-exporter-pvwsz 0/1 CrashLoopBackOff 3 (36s ago) 81skubectl --context cka1001 -n monitoring logs -l app.kubernetes.io/name=prometheus-node-exporter --tail=6 --prefix=falsetime=2026-08-21T20:49:50.111Z level=INFO source=node_exporter.go:142 msg=unametime=2026-08-21T20:49:50.111Z level=INFO source=node_exporter.go:142 msg=vmstattime=2026-08-21T20:49:50.111Z level=INFO source=node_exporter.go:142 msg=watchdogtime=2026-08-21T20:49:50.111Z level=INFO source=node_exporter.go:142 msg=xfstime=2026-08-21T20:49:50.111Z level=INFO source=node_exporter.go:142 msg=zfstime=2026-08-21T20:49:50.111Z level=ERROR source=node_exporter.go:248 msg="listen tcp 0.0.0.0:9100: bind: address already in use"time=2026-08-21T20:49:51.803Z level=INFO source=node_exporter.go:142 msg=unametime=2026-08-21T20:49:51.803Z level=INFO source=node_exporter.go:142 msg=vmstatExpected resultThe same
bind: address already in useat the end of each restart attempt.Success conditionYou have found the error line rather than guessing from the Pod status.
-
Ask the host what owns the port
This is the step that turns a guess into an answer.
ss -ltnplists listening TCP sockets with the process that owns each one - and the answer is anode_exporterthat was installed on the host directly, as a systemd unit, long before this chart existed.Two exporters, one port, and the Pod loses because the host process got there first. Nothing about the cluster is broken.
bash Example session sudo -n ss -ltnp | grep -w 9100LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=847,fd=3))systemctl is-active node_exporter prometheus-node-exporter 2>&1 | head -3activeinactivesudo -n systemctl show -p Description,FragmentPath node_exporter | head -3Description=Node ExporterFragmentPath=/etc/systemd/system/node_exporter.serviceExpected resultA pid holding
*:9100, and a systemd unit that explains where it came from.Success conditionYou can name the process on 9100 and the unit file behind it.
-
Move the chart, not the host
There are two honest fixes and the choice matters.
Stop the host exporter if it is redundant - two processes exporting the same metrics is waste, and the DaemonSet is the one Kubernetes can manage, upgrade and reschedule.
Move the chart's exporter to another port if the host one is someone else's: monitored by a system that predates the cluster, or owned by a team who did not agree to this install. That is the assumption here, so 9101 it is.
What you must not do is delete the DaemonSet and call the stack installed. Without node-exporter you have no CPU, memory, disk or network metrics for any node - which is most of what people install Prometheus for.
bash Example session helm --kube-context cka1001 upgrade kps prometheus-community/kube-prometheus-stack --namespace monitoring --reuse-values --set prometheus-node-exporter.service.port=9101 --set prometheus-node-exporter.service.targetPort=9101Release "kps" has been upgraded. Happy Helming!NAME: kpsLAST DEPLOYED: Sat Aug 22 02:21:02 2026NAMESPACE: monitoringSTATUS: deployedREVISION: 2DESCRIPTION: Upgrade completeTEST SUITE: NoneNOTES:kube-prometheus-stack has been installed. Check its status by running: kubectl --namespace monitoring get pods -l "release=kps" Get Grafana 'admin' user password by running: kubectl --namespace monitoring get secrets kps-grafana -o jsonpath="{.data.admin-password}" | base64 -d ; echo Access Grafana local instance: export POD_NAME=$(kubectl --namespace monitoring get pod -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=kps" -oname) kubectl --namespace monitoring port-forward $POD_NAME 3000 Get your grafana admin user password by running: kubectl get secret --namespace monitoring -l app.kubernetes.io/component=admin-secret -o jsonpath="{.items[0].data.admin-password}" | base64 --decode ; echo Visit https://github.com/prometheus-operator/kube-prometheus for instructions on how to create & configure Alertmanager and Prometheus instances using the Operator.kubectl --context cka1001 -n monitoring rollout status ds/kps-prometheus-node-exporter --timeout=120sdaemon set "kps-prometheus-node-exporter" successfully rolled outkubectl --context cka1001 -n monitoring get pods -l app.kubernetes.io/name=prometheus-node-exporterNAME READY STATUS RESTARTS AGEkps-prometheus-node-exporter-66k7t 1/1 Running 0 11skps-prometheus-node-exporter-hhg9v 1/1 Running 0 11skps-prometheus-node-exporter-hvgzr 1/1 Running 0 11skps-prometheus-node-exporter-wmq2w 1/1 Running 0 11ssudo -n ss -ltnp | grep -E ':(9100|9101)'LISTEN 0 4096 *:9101 *:* users:(("node_exporter",pid=137898,fd=4))LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=847,fd=3))Expected resultFour Pods Running, and
ssshowing both exporters: the host one still on 9100, the chart one now on 9101.Success condition
4/4node-exporter Pods Running, and two distinct pids on two ports. -
Or remove the host exporter, if it is yours to remove
The previous step took the cautious option. Here is the other one, with the same rigour, because on this cluster the host exporter really was redundant - a hand-installed systemd unit from September, duplicating exactly what the DaemonSet collects.
systemctl disable --nowstops it and removes the boot symlink. It does not delete the binary or the unit file, sosystemctl enable --now node_exporterputs it back - which is the right level of destructiveness for something you did not install yourself.With 9100 free, the chart can have its default port back. All four Pods roll out, and
ssnow shows the container holding 9100 instead of the host process.Then the check that matters:
count(up{job="node-exporter"} == 1)returns 4. All four nodes are being scraped. Andcount(up == 0)is still 7 - unchanged, because those are the control-plane targets bound to localhost, which have nothing to do with this.Which option to pick is a judgement about ownership, not about correctness. Two exporters on two ports works and wastes a little memory. One exporter on the standard port is cleaner and matches every dashboard's assumptions. What decides it is whether something you do not control was already scraping 9100 - and if you cannot answer that, take the cautious option first.
bash Example session sudo -n systemctl disable --now node_exporter && echo disabled on $(hostname)disabled on cka1001Removed '/etc/systemd/system/multi-user.target.wants/node_exporter.service'.sudo -n ss -ltnp | grep -w 9100 || echo "nothing on 9100 now"nothing on 9100 nowhelm --kube-context cka1001 upgrade kps prometheus-community/kube-prometheus-stack --namespace monitoring --reuse-values --set prometheus-node-exporter.service.port=9100 --set prometheus-node-exporter.service.targetPort=9100Release "kps" has been upgraded. Happy Helming!NAME: kpsLAST DEPLOYED: Sat Aug 22 10:17:58 2026NAMESPACE: monitoringSTATUS: deployedREVISION: 3kubectl --context cka1001 -n monitoring rollout status ds/kps-prometheus-node-exporter --timeout=180sdaemon set "kps-prometheus-node-exporter" successfully rolled outkubectl --context cka1001 -n monitoring get pods -l app.kubernetes.io/name=prometheus-node-exporterNAME READY STATUS RESTARTS AGEkps-prometheus-node-exporter-k2xvn 1/1 Running 0 12skps-prometheus-node-exporter-llzk8 1/1 Running 0 10skps-prometheus-node-exporter-qr2dh 1/1 Running 0 11skps-prometheus-node-exporter-qwf76 1/1 Running 0 9skubectl --context cka1001 -n monitoring exec promq -- curl -sG http://kps-kube-prometheus-stack-prometheus:9090/api/v1/query --data-urlencode 'query=count(up{job="node-exporter"} == 1)'{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1787374165.866,"4"]}]}}Expected resultThe unit disabled, 9100 free, four Pods rolled out on the default port, and four node-exporter targets up.
Success condition
count(up{job="node-exporter"} == 1)returns the number of your nodes.
Troubleshooting
node-exporter Pods stay
CrashLoopBackOffafter changing the port.Why: The chart key is nested under the subchart name. Setting
nodeExporter.service.portdoes nothing; the subchart isprometheus-node-exporter.Fix:Use
--set prometheus-node-exporter.service.port=9101and the matchingtargetPort. Confirm withkubectl -n monitoring get ds kps-prometheus-node-exporter -o jsonpath='{.spec.template.spec.containers[0].args}'.Prometheus itself sits
Pending.Why: Its memory request does not fit any node. The chart default is generous for a small lab cluster.
Fix:
kubectl -n monitoring describe pod prometheus-kps-kube-prometheus-stack-prometheus-0and read the Events. If it saysInsufficient memory, reinstall with a smallerprometheus.prometheusSpec.resources.requests.memory.The helm install times out or leaves objects behind.
Why: The CRDs in this chart are large, and a client-side apply can exceed the 256 KB annotation limit on slower control planes.
Fix:Uninstall with
helm uninstall kps -n monitoring, then install again. The CRDs are deliberately left behind byuninstall; remove them explicitly only if you are done with the Operator.