LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Perses: The Open Dashboarding Standard

observabilitydashboardskubernetesgitopsprometheuscncfopen-source

Dashboards have always been the odd exception in infrastructure-as-code. Your Terraform is in Git. Your Kubernetes manifests are in Git. Your CI/CD pipelines are in Git. But your Grafana dashboards? Probably clicked together in the UI and exported as opaque JSON blobs that nobody really reviews or versions properly.

Perses was built to fix this. It’s an open-source dashboarding platform — CNCF sandbox since August 2024 — that treats dashboards as first-class code artifacts. Dashboards are defined in YAML or Go, validated by a schema, linted in CI, and deployed through GitOps workflows. In Kubernetes, they’re CRDs. In your repo, they’re files next to the rest of your infrastructure code.

It’s not trying to be a full Grafana replacement. It’s specifically solving the dashboard management problem for teams already deep in the Kubernetes and GitOps ecosystem.


Background

Perses originated at Amadeus, a travel technology company managing a very large number of dashboards at scale. The problem they hit — and that most platform teams hit eventually — is that Grafana dashboards become unmanageable at volume: inconsistent naming, duplicate panels, no review process, no way to enforce standards, no clear ownership.

The project has since attracted contributions from Chronosphere and Red Hat, and was accepted into the CNCF as a sandbox project in August 2024. It’s licensed Apache 2.0, governed by the Linux Foundation, and sits under the CoreDash initiative, which aims to define a standardized dashboard specification across the observability ecosystem.

GitHub stats as of early 2026: 2,100+ stars, 182 forks, 114 releases, actively developed.


The Core Idea: Dashboards as Code

The fundamental shift Perses makes is treating a dashboard the same way you treat a Kubernetes Deployment or a Terraform module: as a file that lives in version control, gets reviewed in pull requests, and is applied through a deployment pipeline.

This means:

  • Git history for dashboards — who changed what, when, and why
  • Code review for dashboard changes — no more “who broke the production dashboard?”
  • CI validation — schema validation catches broken dashboards before they reach production
  • GitOps deployment — ArgoCD or Flux syncs dashboards from Git to running Perses instances
  • Reusable components — build shared panel libraries and reference them across dashboards

None of this is possible with Grafana’s traditional model, where dashboards live in a database and changes happen in the UI.


Architecture

Backend: Go 1.23+, REST API, Cuelang for schema validation. Supports PostgreSQL or file-based storage.

Frontend: React and TypeScript, using remote module federation for dynamic plugin loading.

Plugin system: Each panel type and datasource is a plugin. Plugins contain a Cuelang schema (for validation) and a React component (for rendering). The plugin architecture is open — community plugins follow the same structure as built-in ones.

Kubernetes operator: The perses-operator project (separate from the main repo) watches Perses CRDs and reconciles the desired state. Requires cert-manager for webhook certificate management.


Deploying Perses

Docker (Quickstart)

1
docker run --rm -p 8080:8080 persesdev/perses:latest

The UI is at http://localhost:8080. For anything beyond experimentation, you’ll want a persistent config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# config.yaml
encryption_key: "your-32-char-encryption-key-here"

database:
  file:
    folder: "/var/perses/data"

provisioning:
  folders:
    - "/var/perses/provisioning"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# docker-compose.yaml
services:
  perses:
    image: persesdev/perses:latest
    container_name: perses
    ports:
      - "8080:8080"
    environment:
      - PERSES_ENCRYPTION_KEY=your-encryption-key-here
    volumes:
      - ./config.yaml:/etc/perses/config.yaml
      - perses-data:/var/perses/data
      - ./provisioning:/var/perses/provisioning
    command: ["--config=/etc/perses/config.yaml"]
    restart: unless-stopped

volumes:
  perses-data:

Provisioning folders contain dashboard and datasource YAML files that Perses loads on startup — the equivalent of Grafana’s provisioning directory.

Kubernetes with Helm

1
2
3
4
5
6
helm repo add perses https://perses.github.io/helm-charts
helm repo update
helm install perses perses/perses \
  --namespace monitoring \
  --create-namespace \
  -f values.yaml

values.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
perses:
  config:
    encryption_key: "${PERSES_ENCRYPTION_KEY}"
    database:
      sql:
        driver: postgres
        dsn: "${POSTGRES_DSN}"
    provisioning:
      folders:
        - /var/perses/provisioning

  extraEnvVars:
    - name: PERSES_ENCRYPTION_KEY
      valueFrom:
        secretKeyRef:
          name: perses-secrets
          key: encryption_key
    - name: POSTGRES_DSN
      valueFrom:
        secretKeyRef:
          name: perses-secrets
          key: postgres_dsn

  persistence:
    enabled: true
    size: 10Gi

With the Perses Operator

The operator enables fully declarative dashboard management via CRDs:

1
2
3
4
5
6
7
# Install cert-manager first (required for webhook certificates)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# Install the operator
helm install perses-operator perses/perses-operator \
  --namespace perses-operator-system \
  --create-namespace

Once the operator is running, you manage dashboards with Kubernetes objects.


Kubernetes CRDs

With the operator installed, you define dashboards as Kubernetes resources:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: perses.dev/v1alpha1
kind: PersesDashboard
metadata:
  name: node-overview
  namespace: monitoring
  labels:
    app: perses
spec:
  project: infrastructure
  display:
    name: "Node Overview"
  duration: "1h"
  refreshInterval: "30s"
  panels:
    cpu_usage:
      kind: Panel
      spec:
        display:
          name: "CPU Usage"
        plugin:
          kind: TimeSeriesChart
          spec:
            queries:
              - kind: TimeSeriesQuery
                spec:
                  plugin:
                    kind: PrometheusTimeSeriesQuery
                    spec:
                      query: '100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)'

Datasources are also CRDs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: perses.dev/v1alpha1
kind: PersesGlobalDatasource
metadata:
  name: prometheus
spec:
  display:
    name: Prometheus
  plugin:
    kind: PrometheusDatasource
    spec:
      directUrl: http://prometheus.monitoring.svc.cluster.local:9090

PersesGlobalDatasource is cluster-scoped and available to all projects. PersesDatasource is project-scoped.

When you apply these manifests, the operator creates or updates the corresponding resources in the Perses server. Combine this with ArgoCD or Flux watching your Git repository, and you have a fully automated dashboard deployment pipeline.


Datasource Support

Perses currently supports the core Grafana-compatible stack:

Datasource Status Notes
Prometheus / Mimir Full PromQL, metrics explorer, query builder
Tempo Full Traces, Gantt charts, scatter plots, trace ID search
Loki Full Log queries, filtering, log display
Pyroscope Full Continuous profiling, flame graphs
Thanos Full Compatible with Prometheus plugin

The major gaps compared to Grafana: no Elasticsearch, no InfluxDB, no MySQL/PostgreSQL datasources, no cloud-provider-specific datasources (CloudWatch, Azure Monitor, etc.). If your stack is Prometheus + Tempo + Loki + Pyroscope — the standard CNCF observability stack — Perses covers it well. If you need anything outside that, Grafana is still the answer for now.


Dashboard Definition Methods

YAML / JSON Direct

The most straightforward approach: write dashboard YAML directly and commit it to Git. The structure follows Kubernetes conventions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kind: Dashboard
apiVersion: perses.dev/v1alpha1
metadata:
  name: my-service-dashboard
  project: my-team
spec:
  display:
    name: "My Service"
  duration: "6h"
  panels:
    request_rate:
      kind: Panel
      spec:
        display:
          name: "Request Rate"
        plugin:
          kind: TimeSeriesChart
          spec:
            queries:
              - kind: TimeSeriesQuery
                spec:
                  plugin:
                    kind: PrometheusTimeSeriesQuery
                    spec:
                      query: 'rate(http_requests_total[5m])'

Go SDK

For programmatic dashboard generation with full type safety:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "github.com/perses/perses/go-sdk/dashboard"
    "github.com/perses/perses/go-sdk/panel"
    timeseries "github.com/perses/perses/go-sdk/panel/time-series"
    prometheus "github.com/perses/perses/go-sdk/prometheus/query"
)

func main() {
    dashboard.New("my-service-dashboard",
        dashboard.ProjectName("my-team"),
        dashboard.Name("My Service"),
        dashboard.AddPanel("request_rate",
            panel.New("Request Rate",
                timeseries.Chart(
                    timeseries.WithQuery(
                        prometheus.New("rate(http_requests_total[5m])"),
                    ),
                ),
            ),
        ),
    ).Build()
}

The Go SDK enables building dashboard libraries — shared panel definitions that multiple teams import and reuse. Run percli dac build to generate the final dashboard YAML from your Go code.


The percli CLI

percli is the command-line tool for working with Perses dashboards. Install it:

1
2
3
4
# Download the latest release
curl -Lo percli https://github.com/perses/perses/releases/latest/download/percli_linux_amd64
chmod +x percli
mv percli /usr/local/bin/

Validate a dashboard locally:

1
percli lint dashboard.yaml

Validate against a running server (full plugin schema validation):

1
percli lint --online --project my-team dashboard.yaml

Apply a dashboard to Perses:

1
percli apply --project my-team -f dashboard.yaml

Migrate from Grafana:

1
percli migrate -f grafana-dashboard.json --online -o json > perses-dashboard.json

Custom lint rules enforce organizational standards:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# lint-rules.yaml
rules:
  - name: require-datasource-variable
    condition:
      panelPlugin:
        kind: TimeSeriesChart
    require:
      - spec.queries[*].spec.plugin.spec.datasource
  - name: naming-convention
    condition:
      kind: Dashboard
    pattern:
      metadata.name: "^[a-z][a-z0-9-]*$"
1
percli lint --custom-rule.path ./lint-rules.yaml -f ./dashboards/

This is where Perses delivers value that Grafana can’t match: you can programmatically enforce that every dashboard has a datasource variable, follows naming conventions, includes required metadata, or any other standard your team needs. Run it in CI on every pull request.


CI/CD Integration

A typical GitHub Actions workflow for dashboard-as-code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
name: Validate Dashboards

on:
  pull_request:
    paths:
      - 'dashboards/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install percli
        run: |
          curl -Lo percli https://github.com/perses/perses/releases/latest/download/percli_linux_amd64
          chmod +x percli
          sudo mv percli /usr/local/bin/

      - name: Lint dashboards
        run: |
          percli lint \
            --custom-rule.path ./ci/lint-rules.yaml \
            ./dashboards/

      - name: Validate schema
        run: |
          percli validate -f ./dashboards/

  deploy:
    if: github.ref == 'refs/heads/main'
    needs: validate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Apply dashboards
        env:
          PERSES_URL: ${{ secrets.PERSES_URL }}
          PERSES_TOKEN: ${{ secrets.PERSES_TOKEN }}
        run: |
          percli login $PERSES_URL --token $PERSES_TOKEN
          percli apply --project infrastructure -f ./dashboards/

Pull requests that modify dashboards are automatically validated. Merges to main automatically deploy. The same workflow that governs your Terraform and Kubernetes manifests now governs your dashboards.


Migrating from Grafana

Migration is available but best-effort. The CLI converts Grafana dashboard JSON to Perses format:

1
2
3
4
# Single dashboard
percli migrate -f grafana-dashboard.json --online -o json > perses-dashboard.json

# Or import via the UI: Home → Add Dashboard → Import

The --online flag ensures migration uses the latest plugin schema versions from your running Perses instance.

What migrates well:

  • Dashboard structure and layout
  • Time series panels
  • Stat and gauge panels
  • PromQL queries

What doesn’t migrate:

  • Alerting rules — Perses has no native alerting engine
  • Users, organizations, and RBAC configurations
  • Grafana-specific plugins with no Perses equivalent
  • Template variables with complex logic

v0.50.0 (January 2025) rewrote the migration engine entirely, fixing major memory leak issues that caused OOMKill on large dashboard sets. If you tried migrating before that and gave up, it’s worth trying again.

The practical migration strategy: use percli to get 80% of the way there, then manually fix the remaining panels. For dashboards with heavy use of Grafana-specific plugins, the migration won’t produce anything useful — those need to be rebuilt.


Comparison with Grafana

Perses Grafana
License Apache 2.0 AGPLv3
Dashboard-as-code Native — it’s the whole point Via Grafonnet/Terraform provider (workaround)
Kubernetes CRDs Yes (via operator) No
Schema validation Strong (Cuelang) None
CI/CD linting Built-in (percli) External tooling required
GitOps First-class Possible but awkward
Datasources ~5 core 100+
Plugin ecosystem Small, growing Large, mature
Alerting None built-in Comprehensive
CNCF Sandbox No
Maturity Early (v0.5x) Mature (v11+)

The honest summary: Perses is better than Grafana for the specific workflow of managing dashboards through code review and GitOps. Grafana is better for almost everything else — datasource breadth, plugin ecosystem, alerting, UI polish, and maturity.

These aren’t necessarily in competition. Some teams run both: Grafana for the broad dashboard needs and ad-hoc exploration, Perses for the dashboards that are important enough to warrant GitOps treatment.


What’s Production-Ready

Stable and ready:

  • Core dashboard CRUD and API
  • Prometheus/Mimir, Tempo, Loki, Pyroscope datasources
  • Kubernetes CRDs and operator
  • percli CLI (lint, validate, apply, migrate)
  • Dashboard provisioning from files
  • Apache 2.0 licensed, CNCF governed

Still maturing:

  • Plugin ecosystem (small compared to Grafana)
  • RBAC (authentication works, fine-grained authorization still developing)
  • Alerting (not present — use Prometheus Alertmanager separately)
  • Reporting and scheduled exports

At v0.53.x, Perses is appropriate for production use on the Prometheus/Loki/Tempo stack if GitOps dashboard management is a priority. It’s not a complete Grafana replacement for teams with diverse datasource requirements.


When to Use Perses

Perses fits well when:

  • Your team already uses GitOps (ArgoCD, Flux) and wants dashboards in the same workflow
  • You’re managing dashboards at scale (dozens to hundreds) and need consistency enforcement
  • Your primary observability stack is Prometheus + Loki + Tempo + Pyroscope
  • You want code review and CI validation for dashboard changes
  • You’re building a platform and want dashboards as Kubernetes resources

Stick with Grafana when:

  • You need datasources beyond the CNCF stack (Elasticsearch, CloudWatch, etc.)
  • You need built-in alerting
  • Your team creates dashboards primarily through the UI
  • You need the mature plugin ecosystem
  • You want commercial support

The Bottom Line

Perses solves a real problem: dashboard management at scale without GitOps is painful, and Grafana’s JSON model wasn’t designed for code review workflows. The dashboard-as-code approach with schema validation and percli CI integration is a genuine improvement over Grafana’s provisioning mechanism.

It’s early — CNCF sandbox, v0.5x, limited datasources, no alerting. But if your stack is Prometheus + Loki + Tempo and you’re already managing infrastructure as code, Perses fits naturally into that workflow. The Kubernetes-native CRDs and operator make it the only dashboarding platform where deploying a new dashboard looks the same as deploying a new Deployment or ConfigMap.

Worth watching: the CoreDash initiative under the Linux Foundation aims to standardize the dashboard specification format across projects. If that gains traction, Perses’s data model could become the foundation for interoperability between observability platforms.

Comments