> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Grafana

> Deploy Grafana OSS on Control Plane using the Template Catalog. Dashboards and alerting over your own datasources, backed by a highly available PostgreSQL app database, with datasource provisioning as code and multi-replica alerting HA coordinated by Redis Sentinel.

## Overview

Grafana is the open-source (AGPL-3.0) dashboarding and alerting front end for time-series and SQL data. This template deploys Grafana OSS as a stateless app tier backed by a highly available PostgreSQL app database — every dashboard, user, alert rule, and saved datasource lives in that database, so the Grafana workload itself holds no state and can run multiple replicas.

This template exists to visualize **data you own**: the catalog's [Prometheus](/template-catalog/templates/prometheus), [Thanos](/template-catalog/templates/thanos), and [Grafana Mimir](/template-catalog/templates/mimir) installs, your own SQL databases, and external systems. It is not an observability stack in itself — it ships no collectors, no exporters, and no data.

<Info>
  **This template ships zero dashboards and zero datasources.** A fresh install has an empty dashboard list and an empty datasource list — you connect your own datasources (in the UI or [as code](#provisioning-datasources)) and build or import your own dashboards. Control Plane's console already provides built-in workload-metrics dashboards for every workload; this template is for the data those dashboards do not cover.
</Info>

### Architecture

* **Grafana** — A stateless standard workload serving the UI and HTTP API on port `3000`. The `replicas` knob controls how many instances run; all replicas share the same PostgreSQL app database.
* **PostgreSQL (HA, default)** — The [postgres-highly-available](/template-catalog/templates/postgres-highly-available) template as a subchart: 3× Patroni PostgreSQL, 3× etcd, and a HAProxy leader-routing endpoint Grafana connects through.
* **PostgreSQL (dev/lightweight, optional)** — The single-instance [postgres](/template-catalog/templates/postgres) template instead, for lighter non-HA deployments.
* **Redis Sentinel (optional)** — The [redis](/template-catalog/templates/redis) template as a subchart in Sentinel mode. Grafana's alerting HA uses it to coordinate alert evaluation across replicas, so exactly one notification is sent per alert. Required when `replicas` is 2 or more.
* **Hardened defaults** — User signup, anonymous access, and upstream telemetry are all disabled. When public access is on, Grafana's `root_url` is derived automatically from the canonical endpoint.

### What Gets Created

* **Standard Grafana Workload** — `replicas` stateless instances serving the UI and API on port `3000`.
* **Database Workloads** — HA mode: a stateful Patroni PostgreSQL workload, a stateful etcd workload, and a standard HAProxy leader-routing workload. Single mode: one stateful PostgreSQL workload.
* **Redis Workloads** *(optional)* — A Redis workload and a Sentinel workload (3 replicas each by default) when `redis.enabled` is true.
* **Volume Sets** — The database subchart's persistent volumes (10 GiB per replica by default), plus the etcd volumes in HA mode. Grafana itself has none.
* **Secrets** — The database credentials created by the database subchart, and — only when `datasources.definitions` is set — an opaque secret holding the rendered datasource provisioning file. The admin password and the datasource-encryption key are **prerequisite secrets you create yourself**; the chart references them by name and never creates, modifies, or deletes them.
* **Identity & Policy** — A least-privilege policy granting the Grafana identity `reveal` on exactly the secrets it uses, including the prerequisite admin-password, encryption-key, datasource-credential, and SMTP secrets you name.
* **Cron Backup Workload** *(optional)* — When database backups are enabled.

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC.
</Note>

## Prerequisites

**Two [opaque secrets](/guides/create-secret/opaque) must exist before you install.** Grafana's admin account is a human-facing login, and `publicAccess.enabled` defaults to `true`, so that account is reachable from the internet — a value in `values.yaml` would sit in the Helm release for the life of the install, so neither credential is a value. You create both secrets with encoding `plain` and name them in `admin.passwordSecretName` and `admin.secretKeySecretName`.

<Steps>
  <Step title="Create the admin password secret">
    Choose your own strong password for the `admin` login and store it as the secret's payload:

    ```bash theme={null}
    printf '%s' 'YOUR-STRONG-PASSWORD' | cpln secret create-opaque --name my-grafana-admin-password --encoding plain -f -
    ```

    Set `admin.passwordSecretName` to the name you used.
  </Step>

  <Step title="Create the encryption key secret">
    Generate a random key — Grafana encrypts every stored datasource credential with it — and back the key up somewhere safe outside Control Plane:

    ```bash theme={null}
    printf '%s' "$(openssl rand -hex 32)" | cpln secret create-opaque --name my-grafana-secret-key --encoding plain -f -
    ```

    Set `admin.secretKeySecretName` to the name you used.
  </Step>
</Steps>

<Warning>
  **A missing prerequisite secret wedges the install rather than failing it.** `cpln helm install` still exits 0 and reports success, but the Grafana workload never starts: it sits at zero replicas with the message `The secret <name> no longer exists. Workload updates are paused until the secret is added or the reference to the secret removed.` Create both secrets first, and after installing confirm with `cpln workload get-deployments {release}-grafana --gvc {gvc}` rather than trusting the Helm output.
</Warning>

The two secrets have **different lifecycles** — see [Admin Credentials](#admin-credentials) before you delete or rotate either one.

Optional features each need a secret or a bucket created **before** you install:

| Feature                              | What you must create first                                                                                                           |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| Credentialed provisioned datasources | One [dictionary secret](/guides/create-secret/dictionary) per entry in `datasources.credentialSecrets`, holding the credential keys  |
| Authenticated SMTP                   | An [opaque secret](/guides/create-secret/opaque) with encoding `plain` holding the SMTP password, named in `smtp.passwordSecretName` |
| Database backups                     | A bucket and access setup on AWS S3, Google Cloud Storage, or an S3-compatible server — see [Backing Up](#backing-up)                |

Change the app database password (`postgres.credentials.password`) before installing as well — it ships with a `change-me` placeholder default.

Once both secrets exist, install the template using your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Choosing a Database Mode

Exactly one of the two database modes must be enabled — the chart enforces this at render and fails the install with a clear message otherwise.

|                   | `postgresHA` (default)                                  | `postgres`                             |
| ----------------- | ------------------------------------------------------- | -------------------------------------- |
| What runs         | 3× Patroni PostgreSQL, 3× etcd, HAProxy leader endpoint | One single-replica PostgreSQL workload |
| Database failover | Automatic (Patroni leader election)                     | None                                   |
| Footprint         | 8 replicas across 3 workloads                           | 1 workload                             |
| Best for          | Production                                              | Development and lightweight installs   |

In HA mode the full stack converges in roughly 6 minutes on a fresh install: etcd comes up first, then the PostgreSQL cluster and its HAProxy endpoint, then Grafana runs its schema migrations. Single mode is faster.

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
image: grafana/grafana:13.1.1

replicas: 1 # 1 = single instance; >=2 = HA tier over shared Postgres (requires redis.enabled: true)

resources:
  maxCpu: 1000m
  maxMemory: 1Gi
  minCpu: 500m
  minMemory: 512Mi

# BOTH secrets must EXIST BEFORE INSTALL — opaque, encoding: plain.
# The admin login is on the public internet when publicAccess.enabled is true,
# so its password never transits values or the Helm release.
admin:
  user: admin # initial admin login name (not sensitive)
  # The password applies only when the admin account is FIRST created; on later
  # boots Grafana ignores it (change it in the UI instead).
  applyPassword: true # Set to false after your first login to stop referencing the password secret, which can then be deleted
  passwordSecretName: my-grafana-admin-password # opaque secret holding the first-boot admin password; read only while applyPassword is true
  # DIFFERENT LIFECYCLE — this one is permanent. It is read on EVERY boot to
  # decrypt datasource credentials stored in the DB (AES-256), so it has no
  # applyPassword-style toggle: never delete it, and never rotate it (that
  # breaks every saved datasource secret).
  secretKeySecretName: my-grafana-secret-key # opaque secret holding the encryption key

# Grafana datasource provisioning entries, passed through verbatim. Point them at
# YOUR datasources (prometheus/thanos/mimir templates, your DBs, external APIs).
datasources:
  definitions: []
  # - name: Prometheus
  #   type: prometheus
  #   access: proxy
  #   url: http://RELEASE-prometheus.GVC.cpln.local:9095 # prometheus template
  #   isDefault: true
  # - name: Thanos
  #   type: prometheus
  #   access: proxy
  #   url: http://RELEASE-thanos.GVC.cpln.local:10902 # thanos Query template
  # - name: AppDB
  #   type: postgres
  #   url: my-db-host:5432
  #   user: grafana_reader
  #   jsonData: { database: appdb, sslmode: disable }
  #   secureJsonData:
  #     password: $PG_PASSWORD # interpolated from credentialSecrets below

  # The credentials Grafana authenticates TO each datasource with — not user
  # access to Grafana. `definitions` renders into a plaintext provisioning file,
  # so put the password in a pre-created dictionary secret and write $KEY above.
  # Every $KEY needs an entry here, and the secrets MUST EXIST BEFORE INSTALL.
  credentialSecrets: []
  # - name: my-grafana-ds-credentials
  #   keys: [PG_PASSWORD]

smtp: # alert notification emails
  enabled: false
  host: smtp.example.com:587 # host:port
  user: "" # empty = unauthenticated SMTP
  passwordSecretName: "" # opaque secret (encoding: plain) with the SMTP password; create BEFORE install
  fromAddress: grafana@example.com
  fromName: Grafana

publicAccess:
  enabled: true # UI on the canonical *.cpln.app HTTPS endpoint

internalAccess:
  type: same-gvc # options: none, same-gvc, same-org, workload-list
  workloads: [] # used with workload-list
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME

postgresHA: # default: highly available PostgreSQL app database
  enabled: true
  postgres:
    username: grafana
    password: change-me-grafana-db-password # change before installing
    database: grafana
  replicas: 3
  volumeset:
    capacity: 10 # initial capacity in GiB per replica (minimum is 10)

  backup: # optional database backups — see Backing Up
    enabled: false
    mode: logical # logical or wal-g
    resources:
      cpu: 100m
      memory: 128Mi
    logical:
      image: ghcr.io/controlplane-com/backup-images/postgres-backup:17.1.0
      schedule: "0 2 * * *"
    walg:
      intervalSeconds: 21600
    provider: aws # options: aws, gcp, minio
    aws:
      bucket: grafana-pg-backup-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: grafana-pg-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: grafana-pg-backup-bucket
      cloudAccountName: my-gcs-cloud-account
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: grafana-pg-backup-bucket
      accessKey: my-minio-username
      secretKey: my-minio-password
      prefix: postgres/backups

postgres: # dev/lightweight: single-instance PostgreSQL (disable postgresHA first)
  enabled: false
  credentials: # the chart writes these into the secret named below — nothing for you to create
    username: grafana
    password: change-me-grafana-db-password # change before installing
    database: grafana
  config:
    credentialsSecretName: my-grafana-db-credentials # secret names are org-wide — give each release its own
  volumeset:
    capacity: 10 # initial capacity in GiB (minimum is 10)

  backup: # optional database backups — see Backing Up
    enabled: false
    image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0
    schedule: "0 2 * * *"
    resources:
      cpu: 100m
      memory: 128Mi
    provider: aws # options: aws, gcp, minio
    aws:
      bucket: grafana-pg-backup-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: grafana-pg-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: grafana-pg-backup-bucket
      cloudAccountName: my-gcs-cloud-account
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: grafana-pg-backup-bucket
      credentialsSecretName: my-grafana-minio-credentials # dictionary secret holding accessKey + secretKey
      prefix: postgres/backups

redis: # Sentinel-mode Redis — alerting-HA coordination; required when replicas >= 2
  enabled: false
  redis:
    image: redis:7.4
    replicas: 3
    resources:
      cpu: 200m
      memory: 256Mi
      minCpu: 80m
      minMemory: 128Mi
  sentinel:
    image: redis:7.4
    replicas: 3
    resources:
      cpu: 200m
      memory: 256Mi
      minCpu: 80m
      minMemory: 128Mi
```

### Grafana Instance

* `image` — The Grafana OSS container image.
* `replicas` — Number of Grafana instances. `1` is a single instance; `2` or more forms an HA tier over the shared app database and **requires `redis.enabled: true`** — see [High Availability](#high-availability).
* `resources` — CPU and memory for the Grafana container: `minCpu`/`minMemory` are the reservation, `maxCpu`/`maxMemory` the limit.
* `admin.user` — The admin login name. Not sensitive, so it stays a plain value. Applied when the account is first created.
* `admin.applyPassword` / `admin.passwordSecretName` / `admin.secretKeySecretName` — The two prerequisite secrets and the toggle that retires one of them. See [Admin Credentials](#admin-credentials).

Signup (`GF_USERS_ALLOW_SIGN_UP`), anonymous access, and upstream analytics/update checks are disabled by the template and are not configurable.

### Admin Credentials

Both credentials live in [opaque secrets](/guides/create-secret/opaque) you create before installing (see [Prerequisites](#prerequisites)) — neither ever passes through Helm values. **They sit next to each other in `values.yaml` but their lifecycles are opposites**, and deleting the wrong one is destructive:

|                              | `admin.passwordSecretName`                                | `admin.secretKeySecretName`                                         |
| ---------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------- |
| What it holds                | The `admin` login password                                | The key encrypting stored datasource credentials                    |
| When Grafana reads it        | Only when the admin account is **first created**          | On **every boot**                                                   |
| Environment variable         | `GF_SECURITY_ADMIN_PASSWORD`                              | `GF_SECURITY_SECRET_KEY`                                            |
| Can you stop referencing it? | Yes — set `admin.applyPassword: false`                    | **No.** There is no toggle                                          |
| Can you delete the secret?   | Yes, once `applyPassword` is `false`                      | **Never**                                                           |
| Can you rotate it?           | Yes — but change the password in the Grafana UI, not here | **Never** — every saved datasource credential becomes undecryptable |

<Warning>
  The encryption-key secret is permanent. Deleting it wedges the workload, and changing its payload makes every datasource credential already saved in the app database undecryptable. Back the key up outside Control Plane instead of rotating it.
</Warning>

#### Retiring the Admin Password Secret

After the account exists, the password secret is inert — Grafana ignores `GF_SECURITY_ADMIN_PASSWORD` on later boots, and the password can only be changed in the UI (or with `grafana-cli admin reset-admin-password`). So you can stop referencing it entirely:

<Steps>
  <Step title="Log in once">
    Sign in as `admin.user` with the password from the secret and confirm you can reach the UI.
  </Step>

  <Step title="Set applyPassword to false and upgrade">
    With `admin.applyPassword: false`, the chart drops the password secret from both places it appeared — the container environment and the `reveal` policy grant. The encryption-key reference is retained.
  </Step>

  <Step title="Delete the secret">
    Nothing references it any more, so the secret can be deleted. Your existing admin login keeps working; the account lives in the app database.
  </Step>
</Steps>

<Warning>
  **After that upgrade, expect logins to return `401` and `/api/health` to return `503` for a minute or two** while the bundled app database restarts alongside the change. This is the database being briefly unreachable, not the toggle breaking authentication or resetting your password — do not roll back, and do not recreate the password secret. Wait for `/api/health` to report `200` and log in again.
</Warning>

### Access

* `publicAccess.enabled` — Serve the UI and API on the canonical `*.cpln.app` HTTPS endpoint (default). Everything behind the endpoint is gated by Grafana's own login, and Grafana's `root_url` is set to that endpoint automatically. Set to `false` for an internal-only instance — external requests are then refused at the edge while in-GVC callers still reach it per `internalAccess`.
* `internalAccess.type` — Internal firewall scope of the Grafana workload:

| Type            | Description                                                            |
| --------------- | ---------------------------------------------------------------------- |
| `none`          | No internal access.                                                    |
| `same-gvc`      | Allow access from all workloads in the same GVC (default).             |
| `same-org`      | Allow access from all workloads in the same organization.              |
| `workload-list` | Allow access only from workloads listed in `internalAccess.workloads`. |

Firewall changes applied by an upgrade take up to about 30 seconds to propagate.

### App Database

Enable exactly one of `postgresHA` (production, default) or `postgres` (dev/lightweight) — see [Choosing a Database Mode](#choosing-a-database-mode). In both modes, **change the database password before installing** (`postgres.credentials.password`). Grafana is wired to the active database automatically: the HAProxy leader endpoint in HA mode, or the single instance directly in dev mode. In HA mode `postgresHA.proxy.enabled` must stay `true` — that HAProxy endpoint is Grafana's stable database address, and disabling it is rejected at render.

If you run more than one release of this template in the same organization, give each its own `postgres.config.credentialsSecretName`. Secret names are organization-wide, so a second release left on the default name is **refused at install** and creates nothing — the first release is unaffected.

<Warning>
  **Template versions before `1.2.1` did not compact the etcd cluster inside the bundled highly available database**, so etcd's backend grows with time alone and goes read-only once it reaches its 2 GiB quota — after roughly 110 days — taking PostgreSQL failover with it. Only installs running the HA database are affected (`postgresHA.enabled`, the default here); see [etcd History Compaction](/template-catalog/templates/postgres-highly-available#etcd-history-compaction) for the mechanism and the symptoms. Upgrade to `1.2.1` or later to turn compaction on: that stops further growth but cannot shrink a backend that has already grown, and a cluster that has already raised a `NOSPACE` alarm needs operator recovery rather than an upgrade.
</Warning>

### High Availability

Grafana holds no local state, so scaling the app tier is a matter of raising `replicas`. Alert evaluation is the one thing that must be coordinated: without coordination every replica would evaluate the same rule and send its own notification.

```yaml theme={null}
replicas: 2   # any value >= 2
redis:
  enabled: true   # required when replicas >= 2
```

* Coordination uses Grafana's **Redis-Sentinel alerting HA mode** — the Sentinel workload deployed by the `redis` subchart, not gossip-based peer clustering. Each replica registers itself under its own peer name, and exactly one notification is delivered per firing alert.
* The chart refuses to render `replicas >= 2` without `redis.enabled: true`.
* Redis and Sentinel ship without authentication — the same-GVC internal firewall is the boundary. Enabling Redis or Sentinel auth is rejected at render in this version.
* Multi-replica behavior verified in testing: a full rolling restart and a killed replica both served every request without a single non-200 response, and a killed replica was replaced in about 98 seconds.

### SMTP

Grafana sends alert notification emails through the SMTP server you configure. Leave `smtp.enabled: false` if you use webhook, chat, or other contact points instead.

```yaml theme={null}
smtp:
  enabled: true
  host: smtp.example.com:587   # host:port
  user: mailer                 # omit for unauthenticated SMTP
  passwordSecretName: my-smtp-password   # pre-created opaque secret (encoding: plain)
  fromAddress: grafana@example.com
  fromName: Grafana
```

`smtp.passwordSecretName` is required whenever `smtp.user` is set. The password stays in the [opaque secret](/guides/create-secret/opaque) — it never passes through Helm values — and the chart grants the Grafana identity `reveal` on exactly that secret.

## Provisioning Datasources

You can add datasources in the UI at any time. `datasources.definitions` additionally lets you declare them as code: each entry is a standard [Grafana datasource provisioning](https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources) entry, rendered verbatim into a provisioning file that the workload mounts at startup.

<Note>
  Provisioned datasources are read-only in the Grafana UI. To change one, change the value and upgrade the release.
</Note>

For catalog services running in the same GVC, use their internal endpoints:

```yaml theme={null}
datasources:
  definitions:
    - name: Prometheus
      type: prometheus
      access: proxy
      url: http://RELEASE_NAME-prometheus.GVC_NAME.cpln.local:9095   # prometheus template
      isDefault: true
    - name: Thanos
      type: prometheus
      access: proxy
      url: http://RELEASE_NAME-thanos.GVC_NAME.cpln.local:10902      # thanos Query template
```

### Datasource Credentials

Credentials for provisioned datasources never go into Helm values. Put them in a [dictionary secret](/guides/create-secret/dictionary) you create before installing, list the secret under `datasources.credentialSecrets`, and reference each key as `$KEY` in the provisioning entry. Every listed key is exposed to the container as an environment variable, Grafana interpolates it when it loads the provisioning file, and the chart grants the workload `reveal` on exactly that secret.

<Steps>
  <Step title="Create the dictionary secret">
    Create a [dictionary secret](/guides/create-secret/dictionary) — for example `my-grafana-ds-credentials` with the key `PG_PASSWORD` — **before** installing.
  </Step>

  <Step title="List it under credentialSecrets">
    Add the secret name and the keys you want exposed to `datasources.credentialSecrets`.
  </Step>

  <Step title="Reference the key in the definition">
    Use `$PG_PASSWORD` wherever the credential belongs in the provisioning entry.
  </Step>
</Steps>

```yaml theme={null}
datasources:
  definitions:
    - name: AppDB
      type: postgres
      url: my-db-host:5432
      user: grafana_reader
      jsonData: { database: appdb, sslmode: disable }
      secureJsonData:
        password: $PG_PASSWORD
  credentialSecrets:
    - name: my-grafana-ds-credentials
      keys: [PG_PASSWORD]
```

The provisioning file itself only ever contains the literal `$PG_PASSWORD` placeholder — the value stays in the secret.

### Control Plane Metrics as a Datasource

Control Plane's own metrics store is Prometheus-compatible and holds more than the console's built-in dashboards display, including [custom metrics](/reference/workload/custom-metrics) your workloads expose, cost-relevant series such as `egress`, `cross_zone_traffic`, and `volume_set_*`, and cron and stability counters. Adding it as a datasource complements the built-in workload dashboards rather than replacing them — reach for it when you need custom application metrics, cost views, alerting you own and route yourself, or a single pane that mixes platform metrics with your other datasources.

Follow the [centralized metrics management guide](/guides/centralized-metrics-management) for the endpoint details, then wire the token in as a datasource credential:

<Steps>
  <Step title="Create a service account with readMetrics">
    Create a service account granted the `readMetrics` permission on the org and generate a key for it.
  </Step>

  <Step title="Store the key in a dictionary secret">
    Put the key in a [dictionary secret](/guides/create-secret/dictionary) — for example under the key `CPLN_METRICS_TOKEN` — and list the secret under `datasources.credentialSecrets`.
  </Step>

  <Step title="Add the datasource definition">
    Point a `prometheus`-type datasource at `https://metrics.cpln.io/metrics/org/YOUR_ORG` and send the token as an `Authorization` header.
  </Step>
</Steps>

```yaml theme={null}
datasources:
  definitions:
    - name: Control Plane metrics
      type: prometheus
      access: proxy
      url: https://metrics.cpln.io/metrics/org/YOUR_ORG
      jsonData:
        httpHeaderName1: Authorization
      secureJsonData:
        httpHeaderValue1: Bearer $CPLN_METRICS_TOKEN
  credentialSecrets:
    - name: my-grafana-ds-credentials
      keys: [CPLN_METRICS_TOKEN]
```

<Warning>
  A workload's built-in `CPLN_TOKEN` does **not** authenticate against the metrics endpoint. Use a service-account key with the `readMetrics` permission.
</Warning>

## Connecting

| What                                 | Value                                                                                                                              |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| UI / API (public)                    | `https://<canonical>.cpln.app` — `status.canonicalEndpoint` of `{release}-grafana`                                                 |
| Internal (same GVC)                  | `http://{release}-grafana.{gvc}.cpln.local:3000`                                                                                   |
| Login                                | `admin.user` / the payload of the `admin.passwordSecretName` secret — or whatever you have since changed the password to in the UI |
| App database (internal, HA mode)     | `{release}-postgres-ha-proxy.{gvc}.cpln.local:5432`, credentials in the `{release}-postgres-config` secret                         |
| App database (internal, single mode) | `{release}-postgres.{gvc}.cpln.local:5432`, credentials in the secret named by `postgres.config.credentialsSecretName`             |

Health and readiness are served at `/api/health`, which reports the Grafana version and the app-database status:

```bash theme={null}
curl -s https://<canonical>.cpln.app/api/health
```

The [Grafana HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/) is available on the same endpoint for scripted dashboard, datasource, and alert-rule management.

## Backing Up

Database backups are optional and disabled by default. They cover the app database — the dashboards, users, alert rules, and saved datasources that make up your Grafana instance. Enable them with `postgresHA.backup.enabled` or `postgres.backup.enabled` (matching your database mode), and complete the storage setup for your provider **before** installing. The values below are shown under `backup.*` — set them within the enabled database block.

<Tabs>
  <Tab title="AWS S3">
    <Steps>
      <Step title="Create a bucket">
        Create an S3 bucket. Set `backup.aws.bucket` and `backup.aws.region` to match.
      </Step>

      <Step title="Set up a Cloud Account">
        If you do not have one, [create a Cloud Account](https://docs.controlplane.com/guides/create-cloud-account) for your AWS account. Set `backup.aws.cloudAccountName` to its name.
      </Step>

      <Step title="Create a bucket-scoped IAM policy">
        Create an AWS IAM policy with the JSON below (replace `YOUR_BUCKET`), then set `backup.aws.policyName` to the policy's name:

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [{
            "Effect": "Allow",
            "Action": [
              "s3:ListBucket",
              "s3:GetBucketLocation",
              "s3:GetObject",
              "s3:GetObjectVersion",
              "s3:PutObject",
              "s3:DeleteObject",
              "s3:DeleteObjectVersion",
              "s3:AbortMultipartUpload"
            ],
            "Resource": [
              "arn:aws:s3:::YOUR_BUCKET",
              "arn:aws:s3:::YOUR_BUCKET/*"
            ]
          }]
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Google Cloud Storage">
    <Steps>
      <Step title="Create a bucket">
        Create a GCS bucket. Set `backup.gcp.bucket` to its name.
      </Step>

      <Step title="Set up a Cloud Account">
        If you do not have one, [create a Cloud Account](https://docs.controlplane.com/guides/create-cloud-account) for your GCP project. Set `backup.gcp.cloudAccountName` to its name — the backup identity reaches the bucket keylessly, with no stored credentials.
      </Step>
    </Steps>

    <Warning>
      Grant the `Storage Admin` role to the GCP service account created for the Cloud Account (`roles/storage.objectAdmin` scoped to the bucket also works).
    </Warning>
  </Tab>

  <Tab title="S3-compatible (MinIO, R2, Wasabi)">
    <Steps>
      <Step title="Create a bucket">
        Create your bucket on the server. Set `backup.minio.bucket` to its name.
      </Step>

      <Step title="Set the endpoint">
        Set `backup.minio.endpoint` to the S3 API address including port. For the [MinIO](/template-catalog/templates/minio) template deployed in the same GVC, this is `http://WORKLOAD_NAME:9000`.
      </Step>

      <Step title="Set credentials">
        The two backing stores take these differently. In HA mode (`postgresHA`), set `backup.minio.accessKey` and `backup.minio.secretKey` to credentials with access to the bucket. In single-instance mode (`postgres`), those two values were removed: create a [dictionary secret](/guides/create-secret/dictionary) holding the keys `accessKey` and `secretKey`, and set `backup.minio.credentialsSecretName` to its name — see [MinIO backup prerequisites](/template-catalog/templates/postgres#minio) for the exact command.
      </Step>
    </Steps>
  </Tab>
</Tabs>

In HA mode, `backup.mode` selects `logical` (scheduled `pg_dump` via a cron workload) or `wal-g` (continuous WAL archiving). The single-instance mode takes scheduled logical dumps.

## Important Notes

* **Create the admin-password and encryption-key secrets before installing**, and change the database password. A missing prerequisite secret does not fail the install — `helm install` reports success and the workload then sits at zero replicas waiting on the secret.
* **The encryption-key secret is permanent: never delete it, never rotate it.** It is read on every boot to decrypt stored datasource credentials; changing its payload makes every saved datasource credential undecryptable. Back the key up outside Control Plane.
* **After your first login you can set `admin.applyPassword: false` and delete the password secret** — see [Retiring the Admin Password Secret](#retiring-the-admin-password-secret). Expect `401` logins and a `503` from `/api/health` for a minute or two after that upgrade while the app database restarts; this is not the toggle breaking authentication.
* **This template ships no dashboards and no datasources.** It is the pane for data you own; Control Plane's console already provides built-in workload-metrics dashboards, so there is no reason to recreate those here.
* **Scaling requires Redis** — set `replicas >= 2` together with `redis.enabled: true`. The chart refuses to render multi-replica without it, because Sentinel-coordinated alerting is what keeps one firing alert from producing one notification per replica.
* **Dashboards, users, and alert rules live in the app database** and survive Grafana restarts, redeployments, and replica replacement. **Uninstall deletes the database volume sets** — enable backups if the data matters.
* **Prerequisite secrets you create are not owned by the release** — the admin-password, encryption-key, datasource-credential, and SMTP secrets all survive `helm uninstall` and must be deleted manually if you no longer need them.
* **With `publicAccess.enabled: false`, Grafana's `root_url` is not derived** from a canonical endpoint, so absolute links in alert emails from an internal-only instance point at localhost. Keep public access on if you rely on links in email notifications.
* **Grafana Live push updates are per-instance** in multi-replica mode; dashboard auto-refresh and alerting are unaffected.
* **This template ships Grafana OSS only** — Enterprise features such as fine-grained RBAC, reporting, and query caching are not available.

## External References

<CardGroup cols={2}>
  <Card title="Grafana Documentation" icon="book" href="https://grafana.com/docs/grafana/latest/">
    Official Grafana documentation
  </Card>

  <Card title="Provisioning Datasources" icon="database" href="https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources">
    Reference for the datasource provisioning entries used by this template
  </Card>

  <Card title="Alerting High Availability" icon="bell" href="https://grafana.com/docs/grafana/latest/alerting/set-up/configure-high-availability/">
    How Grafana coordinates alert evaluation across replicas
  </Card>

  <Card title="Configuration Reference" icon="gear" href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/">
    Every Grafana setting and its environment-variable name
  </Card>

  <Card title="Grafana HTTP API" icon="code" href="https://grafana.com/docs/grafana/latest/developers/http_api/">
    Manage dashboards, datasources, and alert rules programmatically
  </Card>

  <Card title="Grafana Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/grafana">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
