Deploy Prometheus, Grafana, and Alertmanager on Civo Kubernetes with Helmfile
This tutorial covers installation, persistent storage, monitoring, alerting, and production-ready configuration with kube-prometheus-stack.
This tutorial shows how to deploy Prometheus, Grafana, and Alertmanager on a Civo Kubernetes cluster using Helmfile and the kube-prometheus-stack chart.
Civo manages the Kubernetes control plane separately from your worker nodes and uses K3s as its Kubernetes distribution. Because of that, some default scrape targets and alert rules in kube-prometheus-stack need adjustment for a clean installation. This guide also configures persistent storage for Prometheus and Grafana using Civo's civo-volume storage class, including retention limits that fit offline volume expansion.
By the end of this tutorial, you will have:
- Prometheus collecting metrics from worker nodes and Kubernetes workloads
- Grafana with persistent storage and preconfigured Kubernetes dashboards
- Alertmanager ready to route notifications
- A version-controlled Helmfile that can reproduce the deployment
- Civo-specific configuration that avoids unavailable control-plane targets and storage rollout failures
Architecture
kube-prometheus-stack is a single Helm chart. One Helmfile release installs Prometheus, Grafana, Alertmanager, the Prometheus Operator, node-exporter, and kube-state-metrics together — including the Operator CRDs (such as ServiceMonitor and PrometheusRule) that the Operator uses to reconcile scrape targets and alert rules. You do not install those components one by one.
Helmfile keeps the chart version, repository, and values in Git. Instead of remembering a long helm install command, you apply a declarative file and the cluster matches that file.

Prerequisites
To follow this tutorial, you need:
- A Civo Kubernetes cluster
- kubectl installed locally
- Helm 3+ installed locally (this tutorial was verified with Helm 4.1.4; Helm 3 remains fully supported)
- Helmfile installed locally
- The helm-diff plugin for
helmfile diff(helm plugin install https://github.com/databus23/helm-diff)
If you do not already have a cluster, create one by following the Civo documentation for Creating a Kubernetes cluster. You can use the Dashboard or the Civo CLI. For a monitoring demonstration, start with at least two or three worker nodes.
Step 1: Confirm your cluster context
Confirm you are pointed at the intended Civo cluster:
kubectl config current-contextkubectl get nodes
You should see worker nodes similar to (versions will vary by cluster):
NAME STATUS ROLES AGE VERSIONk3s-monitoring-demo-...-node-pool-a1b2 Ready <none> 2m v1.34.3+k3s1k3s-monitoring-demo-...-node-pool-c3d4 Ready <none> 2m v1.34.3+k3s1k3s-monitoring-demo-...-node-pool-e5f6 Ready <none> 2m v1.34.3+k3s1
Notice that ROLES shows <none>. On Civo, the control plane is managed and does not appear as a node in your cluster. That fact drives several values changes later in this guide.
Also confirm the default storage class:
kubectl get storageclass
Expected output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSIONcivo-volume (default) csi.civo.com Delete WaitForFirstConsumer true
civo-volume is Civo's native storage class and supports volume expansion. Per Civo's documentation at the time of writing, expansion is an offline operation (the volume must be unmounted first). Future CSI driver releases may change this behavior, so verify against the current Civo documentation when planning storage growth.
Step 2: Create the Helmfile project
By the end of this step you will have:
monitoring/|-- helmfile.yaml`-- values/`-- prometheus-stack.yaml
Create a working directory:
mkdir -p monitoring/valuescd monitoring
Create helmfile.yaml:
# helmfile.yamlrepositories:- name: prometheus-communityurl: https://prometheus-community.github.io/helm-chartshelmDefaults:kubeContext: YOUR_CLUSTER_CONTEXT # replace with your kubectl context namecreateNamespace: truewait: truetimeout: 600releases:- name: prometheus-stacknamespace: monitoringchart: prometheus-community/kube-prometheus-stackversion: 86.1.0values:- values/prometheus-stack.yaml
Two settings are especially important:
kubeContext— Specifies the Kubernetes context Helm uses for this deployment. ReplaceYOUR_CLUSTER_CONTEXTwith the output ofkubectl config current-contextso applies always hit the intended Civo cluster.version— Pinning the chart version keeps staging and production aligned. This tutorial was tested with86.1.0; leave the version unset and you get whatever is newest on the day you run the command. Before pinning a newer version, checkhelm search repo ... --versionsand the chart CHANGELOG — minor bumps in this chart occasionally change default value paths.
Confirm available chart versions if you need to choose a different pin:
helm repo add prometheus-community https://prometheus-community.github.io/helm-chartshelm repo updatehelm search repo prometheus-community/kube-prometheus-stack --versions | head
On Windows, use an equivalent command to display only the first few results.
Install the helm-diff plugin. It is required for helmfile diff. Some Helmfile versions also use it during apply to preview changes. Prefer this over helmfile init, which can prompt for confirmation:
helm plugin install https://github.com/databus23/helm-diff
If the plugin is already installed, helm plugin list will show diff.
Step 3: Configure the stack for Civo
Create values/prometheus-stack.yaml:
# values/prometheus-stack.yaml# Managed control plane scrapes are not useful on Civo; kube-proxy is# typically disabled here too (no useful separate scrape target on K3s).kubeScheduler:enabled: falsekubeControllerManager:enabled: falsekubeEtcd:enabled: falsekubeProxy:enabled: falsedefaultRules:disabled:KubeMemoryOvercommit: trueprometheus:prometheusSpec:retention: 15dretentionSize: 18GBresources:requests:cpu: 250mmemory: 1300MistorageSpec:volumeClaimTemplate:spec:storageClassName: civo-volumeaccessModes: ["ReadWriteOnce"]resources:requests:storage: 20Gigrafana:deploymentStrategy:type: Recreatepersistence:enabled: truetype: pvcstorageClassName: civo-volumesize: 5Giresources:requests:cpu: 100mmemory: 256Mi
Why these control-plane targets are disabled
On Civo, the control plane is managed outside your worker nodes, so scheduler, controller-manager, and etcd are not useful scrape targets from the customer cluster. K3s typically still runs kube-proxy, but it usually does not expose a separate, useful scrape endpoint the way a classic kubeadm install would.
Without disabling these components, several default alerts fire when those metrics are missing — often through expressions such as absent(...) — which can leave critical alerts such as KubeSchedulerDown and KubeControllerManagerDown firing permanently. Setting enabled: false prevents those ServiceMonitors and associated default monitoring resources from being installed — don't just silence the alerts, or you'll inherit the same noise on every new cluster.
KubeMemoryOvercommit is disabled for a similar chart-default reason: on small multi-node Civo clusters that also host the monitoring stack, total memory requests often trip the alert immediately even when the cluster is healthy. Behavior can vary by node size and workload mix, so revisit this override if your cluster is larger or you rely on overcommit warnings.
Why storage class and retention are set explicitly
civo-volume is already the default StorageClass, but pinning it prevents an unexpected change if the cluster default is modified later.
The chart's default retention is time-based and may not include a size limit appropriate for your PVC. Per Civo docs at the time of writing, volume expansion is offline: the volume must be unmounted before it can grow, which usually means stopping Prometheus. Set both retention and retentionSize, and keep retentionSize slightly under the PVC size so Prometheus can compact before the volume fills.
Why Grafana uses Recreate
civo-volume is ReadWriteOnce. A rolling update tries to start the new Grafana pod before the old pod releases the volume. That commonly results in a multi-attach error and a stuck rollout.
Recreate stops the old pod first. Grafana is briefly unavailable during upgrades, which is acceptable for this setup. Apply the same pattern to any Deployment that mounts a single RWO PVC on Civo.
Step 4: Preview and deploy
Always preview changes first:
helmfile diff
Then apply:
helmfile apply
If helmfile apply fails partway, run helmfile diff to see what still differs from the files, fix the values, and apply again. If the diff looks correct but pods or volumes are still stuck, check kubectl -n monitoring get events --sort-by=.lastTimestamp for PVC attach failures, admission webhook errors, or other cluster-side issues. To undo a bad values change, revert the Git commit (or edit the values file back) and run helmfile apply so the cluster matches the known-good files again.
The first installation can take several minutes while CRDs are installed and persistent volumes attach.
Verify the release:
helm list -n monitoringkubectl -n monitoring get podskubectl -n monitoring get pvckubectl -n monitoring get svc
Expected pods include Prometheus, Alertmanager, Grafana, the operator, kube-state-metrics, and one node-exporter pod per node. PersistentVolumeClaims for Prometheus and Grafana should reach Bound.
Alertmanager runs without a PVC in this guide. Add persistent storage if you need silences and notification state to survive restarts.
Example pod output after a successful apply:
NAME READY STATUS RESTARTS AGEalertmanager-prometheus-stack-kube-prom-alertmanager-0 2/2 Running 0 2mprometheus-prometheus-stack-kube-prom-prometheus-0 2/2 Running 0 2mprometheus-stack-grafana-... 3/3 Running 0 2mprometheus-stack-kube-prom-operator-... 1/1 Running 0 2mprometheus-stack-kube-state-metrics-... 1/1 Running 0 2mprometheus-stack-prometheus-node-exporter-... 1/1 Running 0 2m
Pod names and Grafana READY counts can vary slightly by chart version (sidecars). What matters is that the core components are Running and the PVCs are Bound.
If pods stay Pending, check the PVCs — civo-volume uses WaitForFirstConsumer, so a claim can remain Pending until a Pod is scheduled.
Pods in the monitoring namespace

PersistentVolumeClaims bound on civo-volume

Step 5: Verify Prometheus
Port-forward the Prometheus service:
kubectl -n monitoring port-forward svc/prometheus-stack-kube-prom-prometheus 9090:9090
If your release name differs, list services with kubectl -n monitoring get svc and use the Prometheus service name from that output.
Open http://localhost:9090/targets.
Confirm that:
- Listed targets for your stack are UP (Grafana, Alertmanager, Prometheus, operator, kube-state-metrics, node-exporter, kubelet, CoreDNS, apiserver where available)
- There are no scheduler, controller-manager, etcd, or kube-proxy target groups
On a fresh Civo cluster, the targets page should look clean.
Prometheus targets for the monitoring stack:
Then open http://localhost:9090/alerts.
The Watchdog alert should be firing. That is expected. It is an always-on heartbeat used to prove Alertmanager delivery still works. You may also see InfoInhibitor firing; that is a companion alert used to reduce noisy info-level notifications.
If KubeSchedulerDown or KubeControllerManagerDown is firing, the values file was not applied. Run helmfile diff and confirm the kubeScheduler and kubeControllerManager blocks are present.
Prometheus alerts showing Watchdog:

Run a basic PromQL check in Prometheus → Graph:
up
You should see a vector of healthy scrape targets.
Step 6: Access Grafana
Port-forward Grafana:
kubectl -n monitoring port-forward svc/prometheus-stack-grafana 3000:80
Grafana uses username admin. The password is stored in a Secret as base64. Decode it before logging in — do not paste the encoded string into the login form.
kubectl -n monitoring get secret prometheus-stack-grafana \-o jsonpath='{.data.admin-password}' | base64 -decho
Open http://localhost:3000 and sign in as admin with the decoded password.
The Prometheus datasource and standard Kubernetes dashboards are provisioned by the chart. Open Kubernetes / Compute Resources / Cluster and confirm node and pod metrics are populated.
Grafana Kubernetes Compute Resources Cluster dashboard:

Port-forward is appropriate while you validate the stack. Exposing Grafana publicly requires an Ingress, TLS, and authentication policy. Leave that for a follow-up change rather than enabling an open LoadBalancer during setup.
Step 7 (optional): Route alerts to Slack
This section is optional and was not part of the chart version verification above. Use it when you want Alertmanager notifications in Slack.
Create the Slack webhook as a Kubernetes Secret. Do not commit the webhook URL into the values file.
kubectl -n monitoring create secret generic alertmanager-slack \--from-literal=webhook-url='https://hooks.slack.com/services/XXX/YYY/ZZZ'
Append this configuration to values/prometheus-stack.yaml:
alertmanager:alertmanagerSpec:secrets:- alertmanager-slackconfig:route:group_by: ['alertname', 'namespace']group_wait: 30sgroup_interval: 5mrepeat_interval: 4hreceiver: slackroutes:- matchers: ['alertname =~ "Watchdog|InfoInhibitor"']receiver: 'null'receivers:- name: 'null'- name: slackslack_configs:- api_url_file: /etc/alertmanager/secrets/alertmanager-slack/webhook-urlchannel: '#alerts'send_resolved: truetitle: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}'
api_url_file mounts the webhook from the Secret instead of embedding it in Git.
Apply the change:
helmfile apply
Two details to keep in mind:
- Alertmanager matchers are fully anchored.
severity =~ "warn"does not matchwarning. - Routing
Watchdogtonullmeans your deadman switch is not monitored externally. For production, sendWatchdogto an outside heartbeat service such as Dead Man's Snitch or Healthchecks.io so you are notified if Alertmanager itself stops delivering.
Your repository should now match the project layout from Step 2. Changing retention, resources, or alert routing is an edit plus helmfile apply. Recreating the stack on a new cluster is a context switch plus helmfile apply.
Cleanup
To remove the stack:
helmfile destroy
PersistentVolumeClaims may remain after the release is deleted, depending on reclaim behavior and chart settings. Deleting the Helm release also does not necessarily delete the underlying Civo volume, depending on the StorageClass reclaim policy. Delete unused PVCs deliberately, and remember that detached Civo volumes continue to incur charges until removed.
Key takeaways
- Deploy the complete kube-prometheus-stack with Helmfile.
- Configure Prometheus and Grafana with persistent storage on Civo.
- Disable unsupported managed control plane scrape targets.
- Use
Recreatefor Grafana to avoid ReadWriteOnce volume conflicts. - Manage monitoring as code with a version-controlled Helmfile.
Frequently Asked Questions
Summary
This Helmfile setup makes kube-prometheus-stack work cleanly on Civo with three Civo specific fixes.
Disable managed control plane scrape targets (scheduler, controller-manager, etcd, kube-proxy) so Prometheus does not alert on metrics you cannot scrape from the worker side.
Set Prometheus retention and retentionSize so the TSDB disk has a predictable ceiling and growth stays aligned with civo-volume behavior.
Switch Grafana to deploymentStrategy.type: Recreate so upgrades work with civo-volume (ReadWriteOnce) without multi attach issues.
With those tweaks, deploying and updating the stack is a repeatable apply from Git instead of a one off Helm command.
Share this article