How-To

How to Install kube-prometheus-stack for Kubernetes Cluster Monitoring (2026 Guide)

15 min read

Running a cluster without metrics is like driving with the dashboard covered in tape. It works fine until it doesn’t. Then you’re guessing. This guide installs kube-prometheus-stack via Helm, the standard way to get Prometheus and Grafana running on Kubernetes. It also covers the part most tutorials skip: which metrics, dashboards, and alerts actually matter once the pods turn green.

By the end, Prometheus will scrape every node and pod. Grafana will show real cluster health. Alertmanager thresholds will be tuned enough that they won’t spam your on-call channel the first night. That last part matters more than people think; a monitoring stack that pages you for nothing gets muted within a week.

What is kube-prometheus-stack?

kube-prometheus-stack is a Helm chart maintained by the Prometheus community. It bundles the Prometheus Operator, Prometheus, Alertmanager, Grafana, node-exporter, and kube-state-metrics into one installable unit. Hand-wiring each of those components used to eat a full afternoon. Now you run one helm install and get a working, Kubernetes-aware monitoring pipeline with sane defaults for scrape intervals, dashboards, and alert rules.

The chart also installs the Prometheus Operator’s custom resources: ServiceMonitor, PodMonitor, and PrometheusRule. These let you add scrape targets or alert rules as regular Kubernetes YAML. You manage them the same way you manage deployments, with no more hand-editing a monolithic prometheus.yml and reloading it. That’s the real reason this chart became the default starting point. It turns monitoring config into GitOps-friendly manifests, not a snowflake config file that only Dave knows how to touch.

Before You Begin

Make sure you have:

  • A running Kubernetes cluster with kubectl access (managed cloud cluster, k3s, kind, minikube, or bare-metal)
  • Cluster admin permissions (the chart creates CRDs and cluster-scoped RBAC resources)
  • At least 2 vCPUs and 4 GB RAM of spare capacity across your nodes for Prometheus, Grafana, and Alertmanager pods
  • A storage class available for PersistentVolumeClaims if you want metrics to survive pod restarts (recommended for anything beyond a throwaway test cluster)
  • Helm 3.x and kubectl installed locally
  • A terminal (macOS Terminal, iTerm2) and a modern web browser
RequirementDetails
Kubernetes version1.27+ (tested through 1.30)
Helm version3.14.x or later
kubectl versionMatched within one minor version of your cluster
Chart versionkube-prometheus-stack 62.x (2026 release line)
Minimum cluster resources2 vCPU / 4 GB RAM free (small cluster); scale up for production

> This tutorial assumes you already have a cluster to point at. If you need one first, spin up a local k3s or kind cluster. The install steps below work the same once kubectl get nodes returns results.

Step-by-Step Guide

Step 1: Install kubectl and Helm on macOS

Open Terminal and install both tools with Homebrew.

macOS Spotlight search open with "Terminal" typed, about to launch Terminal.app from Applications > Utilities
# Install the Kubernetes CLI
brew install kubectl

# Install the Helm package manager for Kubernetes
brew install helm

Verify both installed correctly:

kubectl version --client
helm version

Expected output looks like:

Client Version: v1.30.3
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
version.BuildInfo{Version:”v3.15.2″, GitCommit:”…”, GitTreeState:”clean”, GoVersion:”go1.22.4″}

Step 2: Confirm cluster access

Point kubectl at your cluster’s kubeconfig (already set up from your cloud provider, k3s, kind, or Rancher Desktop). Confirm it responds.

kubectl get nodes

NAME STATUS ROLES AGE VERSION
node-01 Ready control-plane 14d v1.30.2
node-02 Ready worker 14d v1.30.2
node-03 Ready worker 14d v1.30.2

If this hangs or returns a connection refused error, fix that before continuing. Helm can’t install anything if kubectl can’t reach the API server.

Step 3: Add the Prometheus community Helm repo

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

“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 “prometheus-community” chart repository
Update Complete. ⎈Happy Helming!⎈

Step 4: Create a values.yaml with sane defaults before installing

Don’t install with defaults and tune later. Set retention, storage, and the Grafana admin password up front. If you go back to fix storage after Prometheus already wrote data to an ephemeral volume, you lose your metrics history and start over. Create a file called values.yaml:

# values.yaml
grafana:
  adminPassword: "YOUR_GRAFANA_ADMIN_PASSWORD"
  persistence:
    enabled: true
    size: 5Gi

prometheus:
  prometheusSpec:
    retention: 15d
    scrapeInterval: 30s
    resources:
      requests:
        cpu: 250m
        memory: 1Gi
      limits:
        memory: 2Gi
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: "" # WARNING: an empty string can disable dynamic provisioning; omit this field to use the cluster default, or set an explicit class like "gp3" or "local-path"
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 50Gi

alertmanager:
  alertmanagerSpec:
    storage:
      volumeClaimTemplate:
        spec:
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 5Gi

An empty storageClassName: "" does not mean “use the cluster’s default storage class.” Depending on your cluster’s configuration, an explicit empty string can actually disable dynamic provisioning instead. If you want the default storage class to be used, omit the storageClassName field entirely (or leave it unset) so Kubernetes can apply defaulting. If you have more than one storage class, set storageClassName explicitly to the one you want (kubectl get storageclass lists what’s available). Skip storageSpec entirely and Prometheus stores data in ephemeral pod storage. Every restart wipes your history, which defeats the whole point of having history.

Step 5: Install the chart

helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  -f values.yaml

--create-namespace creates the monitoring namespace if it doesn’t exist. -f values.yaml applies your overrides on top of the chart defaults.

NAME: monitoring
LAST DEPLOYED: Fri Aug 7 10:14:22 2026
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
NOTES:
kube-prometheus-stack has been installed. Check its status by running:
kubectl –namespace monitoring get pods -l “release=monitoring”

Get Grafana ‘admin’ user password by running:
kubectl –namespace monitoring get secrets monitoring-grafana -o jsonpath=”{.data.admin-password}” | base64 -d ; echo

Terminal window showing successful helm install output with STATUS: deployed and the NOTES section listing Grafana access instructions

Step 6: Verify all pods are running

Give it 60–90 seconds for images to pull, then check:

kubectl get pods -n monitoring

NAME READY STATUS RESTARTS AGE
monitoring-kube-prometheus-operator-7d4f8b6c8f-xk2lp 1/1 Running 0 2m
monitoring-kube-state-metrics-6b8f9d5c4-p9nzt 1/1 Running 0 2m
monitoring-grafana-5f7c9b8d6-vqm2j 3/3 Running 0 2m
monitoring-prometheus-node-exporter-4jz6n 1/1 Running 0 2m
monitoring-prometheus-node-exporter-8x2wr 1/1 Running 0 2m
prometheus-monitoring-kube-prometheus-prometheus-0 2/2 Running 0 90s
alertmanager-monitoring-kube-prometheus-alertmanager-0 2/2 Running 0 90s

Terminal output of kubectl get pods -n monitoring showing all kube-prometheus-stack pods in Running state with 0 restarts

Seven pods, all Running, zero restarts. That’s what you want to see. If any pod is stuck in Pending, it’s usually a resource or PVC issue. Check with kubectl describe pod POD_NAME -n monitoring.

Step 7: Access Grafana and confirm login

Retrieve the admin password (skip this if you set grafana.adminPassword explicitly in values.yaml):

kubectl get secret monitoring-grafana -n monitoring -o jsonpath="{.data.admin-password}" | base64 --decode
echo

Port-forward the Grafana service to your local machine:

kubectl port-forward svc/monitoring-grafana 3000:80 -n monitoring

Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

Open http://localhost:3000 in your browser. Log in with username admin and the password from above.

macOS

Run the port-forward command above in a Terminal tab and leave it running. It stays in the foreground until you press Ctrl+C. Open Safari or Chrome to http://localhost:3000 in a second tab.

Web

The Grafana UI is identical no matter which OS you’re forwarding from. Everything from here happens in the browser.

Grafana login page for the local self-hosted instance, showing the username and password fields and Grafana logo

Step 8: Verify Prometheus is actually scraping targets

This is the step people skip, and it matters. A green pod doesn’t mean Prometheus is collecting data. Port-forward the Prometheus service in a new terminal tab:

kubectl port-forward svc/monitoring-kube-prometheus-prometheus 9090:9090 -n monitoring

Open http://localhost:9090/targets in your browser.

Prometheus web UI Targets page showing scrape targets for kubelet, node-exporter, kube-state-metrics, and apiserver all in UP state

Every target should show State: UP with a recent Last Scrape time. Targets stuck in DOWN? Jump to the troubleshooting section below. It’s almost always an RBAC or network policy issue blocking Prometheus from reaching the target.

Step 9: Open the default Grafana dashboards

Back in Grafana, go to Dashboards in the left sidebar. The chart pre-loads several dashboards under folders like Kubernetes / Compute Resources / Cluster and Kubernetes / Networking. Open Kubernetes / Compute Resources / Cluster first. It’s the closest thing to a single pane of glass for overall cluster health.

Grafana's default Kubernetes cluster overview dashboard showing CPU utilization, memory utilization, and pod count panels across the cluster with a namespace filter dropdown

Then open the Node Exporter / Nodes dashboard for per-node detail: CPU, memory, disk, and network broken out per host.

Grafana node-level dashboard showing per-node CPU load, memory usage, and disk usage panels with a node selector at the top

Step 10: Import additional dashboards beyond the defaults

The bundled dashboards cover the basics. Two community dashboards earn a permanent spot in your bookmarks: Node Exporter Full (dashboard ID 1860) for deeper hardware-level detail, and Kubernetes Cluster (Prometheus) (dashboard ID 7249) for a workload-centric view that’s easier to read than the chart’s default.

In Grafana, go to Dashboards → New → Import. Enter the dashboard ID, click Load, select your Prometheus data source (usually named Prometheus) when prompted, and click Import.

Grafana dashboard import screen with a dashboard ID entered in the "Import via grafana.com" field and a Load button visible

Step 11: Check Alertmanager and configure a real notification receiver

Port-forward Alertmanager:

kubectl port-forward svc/monitoring-kube-prometheus-alertmanager 9093:9093 -n monitoring

Open http://localhost:9093 to see any currently firing alerts.

Alertmanager web UI showing the active alerts list and a Silence button for managing acknowledged alerts

Out of the box, Alertmanager has no external receiver configured. Alerts fire but go nowhere. That’s about as useful as a smoke alarm with no battery. Add a Slack (or webhook) receiver by extending your values.yaml:

# values.yaml (append)
alertmanager:
  config:
    global:
      resolve_timeout: 5m
    route:
      receiver: "slack-notifications"
      group_by: ["alertname", "namespace"]
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h
    receivers:
      - name: "slack-notifications"
        slack_configs:
          - api_url: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
            channel: "#cluster-alerts"
            send_resolved: true

Apply the change:

helm upgrade monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  -f values.yaml

Which Metrics Actually Matter

The default dashboards surface dozens of panels. Most of them are useful context, not signal. If you’re triaging cluster health day to day, these four categories are where the real answers live.

Node resource pressure. Watch node_memory_MemAvailable_bytes relative to total, and node_filesystem_avail_bytes for disk. Kubernetes evicts pods under memory pressure well before a node hits 0% free. A node above 85–90% memory use for more than a few minutes is worth alerting on.

Pod restart loops. kube_pod_container_status_restarts_total from kube-state-metrics is the single most useful metric for catching CrashLoopBackOff and OOMKilled pods. It flags problems before someone files a ticket. A pod that restarted once during a deploy is normal. A pod restarting five times in ten minutes is not.

PVC capacity. kubelet_volume_stats_available_bytes versus kubelet_volume_stats_capacity_bytes tells you how full a PersistentVolumeClaim is. This metric prevents a 2 a.m. page from a stateful workload, like databases or log stores, that silently filled its disk. Alert at 80% used with room to act. Don’t wait for 95%; by then it’s already too late.

API server latency. apiserver_request_duration_seconds (via its histogram buckets) tells you if the control plane is struggling. Rising p99 latency on LIST/WATCH requests is often the earliest sign of etcd or control-plane resource exhaustion. It shows up long before pods start failing to schedule.

Everything else, including total request counts, container CPU throttling on non-critical batch jobs, and per-pod network byte counts, is useful for deep debugging. Treat it as reference data, not something worth an alert or a dashboard headline panel.

Configuration

Sizing retention and storage

Retention is a direct trade-off between historical visibility and disk cost. Prometheus‘s on-disk size scales roughly linearly with retention period, scrape interval, and the number of active time series.

Cluster sizeSuggested retentionSuggested Prometheus PVC
Homelab / small (1–10 nodes)7–15d20–50 GB
Mid-size (10–50 nodes)15–30d100–200 GB
Large (50+ nodes)15d local + long-term via Thanos/Mimir200 GB+ per Prometheus shard

For anything beyond a homelab, don’t extend local Prometheus retention past 30 days. Pair it with Thanos or Grafana Mimir for long-term storage instead. Local Prometheus disk grows fast, and long retention on a single instance makes queries slower without much benefit.

Scrape interval trade-offs

The chart defaults to a 30-second scrape interval. Dropping to 15s doubles your sample count. It also roughly doubles disk usage and Prometheus CPU load, for marginally better graph resolution. Going up to 60s halves storage but can hide short-lived spikes. A pod that OOMKills and restarts inside a 60-second window might never show up clearly on a memory graph. For most clusters, 30s is the right default. Drop to 15s only for specific high-value ServiceMonitors, like your API gateway, not globally.

Avoiding cardinality explosions

Cardinality is the number of unique label combinations for a metric. A metric with a pod label is fine at typical scale. A metric with a user_id or full url_path label is not, because every unique value creates a new time series. This is the single most common way teams accidentally take down their own Prometheus. It’s usually self-inflicted, not a Prometheus bug.

Check current series count:

kubectl exec -n monitoring prometheus-monitoring-kube-prometheus-prometheus-0 -c prometheus -- \
  wget -qO- http://localhost:9090/api/v1/status/tsdb

Look at seriesCountByMetricName in the response. Anything with an unexpectedly high count is worth investigating. Fix it by dropping the offending label at scrape time with metric_relabel_configs on the relevant ServiceMonitor:

# service-monitor-example.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app
  namespace: monitoring
  labels:
    release: monitoring
spec:
  selector:
    matchLabels:
      app: my-app
  endpoints:
    - port: metrics
      interval: 30s
      metricRelabelings:
        - sourceLabels: [__name__]
          regex: "http_requests_total"
          targetLabel: url_path
          replacement: ""
          action: replace

Prometheus itself exposes prometheus_tsdb_symbol_table_size_bytes and scrape_samples_scraped. Watch these on your own Prometheus instance to catch cardinality growth before it OOMKills the pod monitoring everything else. That’s a genuinely annoying way to lose visibility right when you need it most.

Setting alert thresholds that don’t spam the on-call channel

The chart’s default PrometheusRule set is tuned for general use, not your cluster’s baseline. Two changes fix most alert fatigue: add a for: duration so alerts only fire after a condition persists, and group related alerts in Alertmanager instead of sending one notification per pod.

# prometheus-rule-example.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: node-memory-pressure
  namespace: monitoring
  labels:
    release: monitoring
spec:
  groups:
    - name: node-resources
      rules:
        - alert: NodeMemoryHighUtilization
          expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.90
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Node {{ $labels.instance }} memory usage above 90%"
            description: "Memory has been above 90% for 10 minutes. Investigate before pods start getting evicted."

for: 10m means a brief CPU or memory spike during a deploy won’t page anyone. Only sustained pressure will. Combine that with the group_by and group_interval settings from Step 11 to batch related alerts instead of firing one notification per affected pod.

Tips and Troubleshooting

Prometheus pod gets OOMKilled

Why it happens: Default resource limits are sized for small clusters. On larger clusters, or with high-cardinality metrics, Prometheus‘s memory footprint outgrows the chart defaults.

Fix: Raise prometheus.prometheusSpec.resources.limits.memory in values.yaml (start at 2Gi, go to 4Gi+ for larger clusters) and run helm upgrade. Watch prometheus_tsdb_head_series to correlate memory growth with series count.

All monitoring data disappears after a pod restart

Why it happens: No PersistentVolumeClaim was configured, so Prometheus and Grafana were writing to ephemeral pod storage. That storage gets wiped on restart or rescheduling.

Fix: Set prometheus.prometheusSpec.storageSpec and grafana.persistence.enabled: true as shown in Step 4, then helm upgrade. Confirm the PVC bound correctly with kubectl get pvc -n monitoring.

Prometheus targets show DOWN on the /targets page

Why it happens: Usually an RBAC issue (the Prometheus Operator’s ClusterRole doesn’t have permission to scrape a namespace) or a NetworkPolicy blocking traffic from the monitoring namespace to the target pod’s metrics port.

Fix: Check kubectl describe servicemonitor SERVICE_MONITOR_NAME -n monitoring for selector mismatches. Confirm any NetworkPolicy in the target namespace allows ingress from the monitoring namespace on the metrics port.

New application metrics don’t show up in Prometheus

Why it happens: No ServiceMonitor exists for the app, or its labels don’t match what prometheus.prometheusSpec.serviceMonitorSelector expects.

Fix: Create a ServiceMonitor like the example in the cardinality section above. Make sure it carries the release: monitoring label (or whatever your Helm release name is). The chart’s default selector filters on that label.

Can’t log into Grafana

Why it happens: The admin password was auto-generated at install and never retrieved, or grafana.adminPassword wasn’t set explicitly.

Fix: Pull the current password from the secret:

kubectl get secret monitoring-grafana -n monitoring -o jsonpath="{.data.admin-password}" | base64 --decode
echo

Or set grafana.adminPassword explicitly in values.yaml and run helm upgrade to reset it.

Wrapping Up

You now have a full Kubernetes monitoring pipeline. Prometheus scrapes every node and pod, Grafana shows dashboards for cluster and node health, and Alertmanager is wired to a real notification channel with thresholds that won’t page you for a two-minute CPU blip. That’s the point of using the Helm chart instead of wiring components by hand, so you spend your time tuning thresholds and dashboards, not writing scrape configs from scratch.

The defaults are a solid starting point, not a finished product. Budget an afternoon after the initial install to right-size retention, add PVC and API-latency alerts, and trim the rules you don’t need. That tuning pass separates a dashboard nobody looks at from a monitoring stack that actually catches problems before your users do.

StepActionApplies To
1–3Install kubectl, Helm, verify cluster accessmacOS
4–6Configure values.yaml, install chart, verify podsmacOS
7–8Access Grafana, verify Prometheus targetsWeb
9–10Review default dashboards, import Node Exporter Full and Kubernetes Cluster dashboardsWeb
11Configure Alertmanager receiver and thresholdsmacOS + Web

Resources