Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

DevOps · CNCF daily

Perses: Dashboards as Code, with an Open Specification and a Kubernetes Operator

CNCF Sandbox. An Apache 2.0 dashboard tool for observability data, with a documented dashboard specification, a CLI and Go and CUE SDKs that validate dashboards in CI, and an operator that manages dashboards as Kubernetes resources. Works with Prometheus, Tempo, Loki and more.

21 min read

CNCF Sandbox

  • Perses
  • Prometheus
  • CUE
  • Go
  • Kubernetes
CNCF project page

Perses is a dashboard tool for observability data: you open it in a browser, choose a data source such as Prometheus, and build panels with queries, variables and layouts. What makes it different from other dashboard tools is the way it treats the dashboard itself. A Perses dashboard is a documented, versioned specification that can be validated statically, written in code with an SDK, applied with a CLI, or declared as a Kubernetes resource, so dashboards go through the same review, test and deploy path as application code. Perses began inside Amadeus, the travel technology company, to solve the problem of managing very large numbers of dashboards in a UI-first tool. It was presented at PromCon EU in 2023, joined the CNCF Sandbox on August 29, 2024, and is licensed under Apache 2.0. This brief explains Perses from the problem it solves to installing, configuring, securing and operating it, with a full dashboard-as-code workflow and a comparison with Grafana, which the Prometheus brief covers, so one read is enough to decide whether it belongs in your platform.

CNCF daily · Sprint 4, post 6 of 15 · Observability

At a glance

Category Observability and Analysis › Observability
CNCF status Sandbox. Accepted August 29, 2024
Originated at Amadeus, with contributors from Red Hat and Chronosphere early on; later releases credit a growing community
Written in Go for the server and CLI, React with TypeScript for the web UI, CUE for the schemas
License Apache 2.0
Data sources Prometheus (metrics), Tempo (traces), Loki (logs) and Pyroscope (profiles); version 0.54.0 added Jaeger, GreptimeDB, Splunk, LogExplorer, Alertmanager and OpenSearch plugins
Latest release 0.54.0 (29 July 2026) was the latest I could confirm; the main-branch documentation already refers to percli 0.55.0
Interfaces Web UI, REST API, percli CLI, Go and CUE SDKs, GitHub Actions, Kubernetes operator
Storage Files on disk or a SQL database
Install options Docker image, binaries and Homebrew, Helm chart, Perses Operator
Also embeddable npm packages that embed panels or whole dashboards in other applications

The problem Perses solves

A dashboard tool is easy to start and hard to run at scale. A team builds dashboards by hand in a UI, exports them as large JSON files, and commits the files. Then problems pile up: the files are huge and unreadable in a code review, there is no static check that a dashboard is valid before it reaches production, copy and paste replaces reuse, the same panel is rebuilt in forty dashboards, and the dashboard in production slowly drifts from the one in Git. At hundreds of dashboards this becomes a platform problem.

Perses starts from the other end. It defines the dashboard as a clean data model first, with a schema for every panel, query, variable and data source, and builds the UI on top of that model. Because the model is explicit, a CLI can validate it without a server, an SDK can generate it from reusable code, and an operator can deliver it through Kubernetes. The open specification also lets other tools read and write the same format, which is why platforms such as Red Hat OpenShift and some commercial observability vendors embed or adopt Perses components.

Key concepts

  • Project. The top-level container and the unit of access control. Dashboards, data sources and variables live in a project. In the Kubernetes operator, a namespace maps to a project.
  • Dashboard. A resource with a spec made of panels, layouts, variables, datasources, a default time range (duration) and a refreshInterval.
  • Panel. One visualization: a plugin such as TimeSeriesChart, StatChart, GaugeChart, Table, Markdown or a trace or log panel, with its display options and one or more queries.
  • Layout. A grid, up to 24 columns wide, that positions panels by reference ($ref) to the panel entries, so a panel is defined once and placed by pointer.
  • Variable. A dashboard input, either a text value or a list whose values come from a query (such as the values of a Prometheus label) or a static list. Panel queries use it as $name.
  • Datasource. A connection to a back end. It can be defined for a dashboard, for a project (Datasource) or for the whole server (GlobalDatasource). The server proxies queries to it, so credentials stay on the server.
  • Plugin. The extension mechanism. Panels, queries, variables and data sources are all plugins, each with a CUE schema that the server and the CLI use for validation. The core plugins live in the perses/plugins repository.
  • Dashboard-as-Code (DaC). Writing dashboards in CUE or Go with the Perses SDKs, then building them to JSON or YAML with percli.
  • percli. The command-line client: apply, get, delete, describe, lint, login, migrate, project, dac and more.
  • Operator. A Kubernetes controller that deploys Perses and syncs PersesDashboard and PersesDatasource resources to it.

Architecture

Perses architecture: clients, meaning a browser, percli and the operator, use the web UI and the Perses API; the API stores resources in files or a SQL database, validates them against plugin schemas, and sends queries through a datasource proxy that holds credentials to Prometheus, Tempo and Loki, and other sources.

One server holds the UI, the API, the storage and the proxy. Blue is a client, purple a Perses component, green storage and teal a data source. Open the zoomable diagram, or download it as SVG or PNG.

Component What it does
Web UI React application served by the server: browse projects and dashboards, edit panels visually, explore queries
Perses API REST API for every resource (projects, dashboards, data sources, variables, secrets, roles, role bindings, users); applies authentication and authorization and validates resources against the plugin schemas
Storage Either files in a folder (YAML or JSON) or a SQL database; file storage is the default and needs no external database
Datasource proxy Forwards panel queries to the data source and adds credentials, so browsers never see them
Plugins and schemas Panels, queries, variables and data sources, each with a CUE schema used for validation and for the migration script
percli and SDKs Static validation, builds, applies and Grafana migration
Operator Deploys Perses and reconciles dashboards and data sources from Kubernetes resources

Why validation is static

Because every plugin has a CUE schema, percli lint can check a dashboard file without contacting any server, and percli lint --online can ask a running Perses to validate it against the plugins that server actually has. That is the basis of the CI step: a dashboard with a misspelt panel option or an unknown query kind fails the pipeline instead of rendering blank in production.

From a reviewed change to a rendered panel

Perses flow in six steps: dashboard code is merged in Git; percli builds it to JSON or YAML; percli lint validates it; percli apply stores it in the Perses API; the web UI loads the dashboard and sends each panel query to the datasource proxy; the proxy adds credentials and runs the PromQL on Prometheus.

  1. Merge. A pull request changes the dashboard code, written in CUE or Go, and is reviewed like any other code.
  2. Build. percli dac build turns the code into JSON or YAML in a built folder.
  3. Lint and apply. percli lint validates the built files, then percli apply -f stores them in the Perses API (or the operator syncs them from Kubernetes resources).
  4. Load. A user opens the dashboard; the UI fetches the specification from the API.
  5. Query. For each panel the UI sends the query to the datasource proxy.
  6. Run. The proxy adds the data source’s credentials and runs the query, for example PromQL on Prometheus, and the result renders in the panel.

Recap: the Git repository is the source of truth, CI proves the dashboard is valid, and the server only stores and serves it.

Perses and Grafana

Perses is often compared with Grafana, which the Prometheus brief covers. They solve the same visualization problem from different starting points:

Perses Grafana
Starting point Code-first: a specification, SDKs and a CLI, with a UI on top UI-first, with provisioning and APIs for automation
License Apache 2.0 AGPLv3
Maturity CNCF Sandbox, young, data model still evolving Mature, with a large ecosystem
Data sources Prometheus, Tempo, Loki, Pyroscope and a growing set of plugins Very large catalogue of data source plugins
Validation Static CUE validation in CI Mostly at import or at render time
Alerting Not an alerting engine; alert rules stay in Prometheus and Alertmanager Built-in alerting
Migration percli migrate converts Grafana JSON on a best-effort basis Not applicable

Treat the migration honestly: it converts what it can map, and panels or variables that Perses has no plugin for are replaced with placeholders that you rebuild by hand. Choose Perses when you want dashboards under strict version control, a permissive licence, a Kubernetes-native workflow, or a specification you can embed in other tools. Stay with Grafana when you rely on its plugin catalogue, its alerting, or a team that builds dashboards visually and does not want a code workflow.

Install and first run

Docker and binaries

docker run --name perses -d -p 127.0.0.1:8080:8080 persesdev/perses

Open http://localhost:8080. Pin an exact image tag in anything real. Homebrew provides both pieces: brew install perses/tap/perses for the server and web UI and brew install perses/tap/percli for the CLI. Release archives on GitHub include SBOM files for the Go binaries and the source tree.

With no configuration, Perses uses file storage and does not require login, which is fine for a local trial and wrong for anything shared. Mount a configuration file and a data folder:

docker run -d --name perses -p 8080:8080 \
  -v $PWD/config.yaml:/etc/perses/config.yaml \
  -v $PWD/data:/perses/data \
  persesdev/perses --config=/etc/perses/config.yaml

Helm

helm repo add perses https://perses.github.io/helm-charts
helm repo update
helm install perses perses/perses \
  --namespace perses --create-namespace \
  --set config.database.file.folder=/perses/data \
  --set config.security.enable_auth=true

The chart passes config.* values into the server configuration, so everything in the configuration reference below can be set through values.yaml. Give the data folder a persistent volume if you use file storage.

The Perses Operator

The operator is the Kubernetes-native option and gives you the PersesDashboard and PersesDatasource resources. It needs cert-manager for its webhook certificates:

helm install perses-operator perses/perses-operator

It watches these custom resources:

  • Perses declares a Perses server and the operator builds the Deployment (SQL storage) or StatefulSet (file storage with a persistent volume), the Service and the configuration.
  • PersesDashboard declares a dashboard to sync to matching Perses instances. The resource’s namespace maps to a Perses project.
  • PersesDatasource declares a project-scoped data source in the same way.

The operator can also configure server TLS, client mTLS and datasource proxy TLS, supports basic auth, OAuth and native Kubernetes authentication, and exposes its own Prometheus metrics and alert rules. Its API is still versioned v1alpha (a v1alpha2 form appears in recent material), so treat the schema as evolving and use kubectl explain on your cluster.

Configuration reference

The server reads a YAML file (--config), and environment variables with the PERSES_ prefix can override keys, for example PERSES_DATABASE_FILE_FOLDER. The options that matter:

Key What it does
database.file.folder, .extension (yaml or json), .case_sensitive File storage location and format
database.sql.* SQL storage: address, database name, user, password or password_file, tls_config, timeouts
security.readonly Rejects every create, update and delete call, for a server that only serves provisioned content
security.enable_auth Requires a valid token on most endpoints
security.encryption_key or encryption_key_file A 32-byte key (AES-256) that encrypts stored secrets such as data source passwords. If you do not set one a built-in default is used, so set your own
security.authentication.access_token_ttl, refresh_token_ttl Token lifetimes, 15 minutes and 24 hours by default
security.authentication.disable_sign_up Hides sign-up and disables user creation through the API
security.authentication.providers.enable_native Built-in username and password login
security.authentication.providers.oidc, .oauth External login providers: issuer or URLs, client_id, client_secret, redirect_uri, scopes
security.authorization.guest_permissions Permissions granted to logged-in users by default
provisioning.folders, provisioning.interval Folders of resources loaded at start and refreshed on an interval
schemas.*_path, schemas.interval Where plugin schemas are read from
important_dashboards, information Dashboards pinned on the home page and a Markdown banner
deactivate_front Stops serving the web UI, for an API-only server

The key list comes from the published configuration model of a recent but not the latest release, so check the configuration documentation for the release you install, especially for newer authorization and plugin settings.

Dashboards as code: a worked example

This example builds a dashboard with a variable, a panel and a data source, then validates and applies it.

Set up a CUE repository

mkdir dashboards && cd dashboards
cue mod init example.com/dashboards
percli dac setup            # downloads the Perses CUE SDK and plugins

(For Go use go mod init and percli dac setup --language go.) The current documentation requires percli 0.55.0 or later and a recent cue. A dashboard in CUE:

package mydac

import (
	dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
	panelGroupsBuilder "github.com/perses/perses/cue/dac-utils/panelgroups"
	varGroupBuilder "github.com/perses/perses/cue/dac-utils/variable/group"
	labelValuesVarBuilder "github.com/perses/plugins/prometheus/sdk/cue/variable/labelvalues"
	panelBuilder "github.com/perses/plugins/prometheus/sdk/cue/panel"
	timeseriesChart "github.com/perses/plugins/timeserieschart/schemas:model"
	promQuery "github.com/perses/plugins/prometheus/schemas/prometheus-time-series-query:model"
	promDs "github.com/perses/plugins/prometheus/schemas/datasource:model"
)

dashboardBuilder & {
	#name:    "ServiceOverview"
	#project: "platform"

	#variables: {varGroupBuilder & {
		#input: [
			labelValuesVarBuilder & {
				#name:           "namespace"
				#metric:         "up"
				#label:          "namespace"
				#datasourceName: "prom"
			},
		]
	}}.variables

	#panelGroups: panelGroupsBuilder & {
		#input: [{
			#title: "Traffic"
			#cols:  2
			#panels: [
				panelBuilder & {
					spec: {
						display: name: "Request rate"
						plugin: timeseriesChart
						queries: [{
							kind: "TimeSeriesQuery"
							spec: plugin: promQuery & {
								spec: query: "sum(rate(http_requests_total{namespace=\"$namespace\"}[5m]))"
							}
						}]
					}
				},
			]
		}]
	}

	#datasources: prom: {
		default: true
		plugin: promDs & {
			spec: close({directUrl: "http://prometheus.monitoring.svc:9090"})
		}
	}
}

The same dashboard can be written in Go with the go-sdk (dashboard.New, AddVariable, AddPanelGroup, AddDatasource), and a Go program must not print to standard output, because dac build captures it. Reuse is the point: put panel definitions in shared modules, and build a hundred service dashboards from one template.

Build, validate and apply

percli dac build -f dashboard.cue -o json   # writes built/ServiceOverview.json
percli dac build -d dashboards              # all files in a folder, one dashboard per file
percli dac watch dashboards                 # rebuild on save while you work

percli lint -f built/ServiceOverview.json
percli lint -f built/ServiceOverview.json --online   # also validate against a running server

percli login http://perses.example.com --username admin
percli project platform
percli apply -f built/ServiceOverview.json
percli get dashboard

The dashboard as plain YAML

You can also write or generate the resource directly. This is the shape percli apply accepts and what the API stores:

kind: Dashboard
metadata:
  name: service-overview
  project: platform
spec:
  display:
    name: Service overview
  duration: 1h
  refreshInterval: 30s
  variables:
    - kind: ListVariable
      spec:
        name: namespace
        plugin:
          kind: PrometheusLabelValuesVariable
          spec:
            labelName: namespace
            matchers: ['up{job="app"}']
  panels:
    requests:
      kind: Panel
      spec:
        display:
          name: Request rate
        plugin:
          kind: TimeSeriesChart
          spec: {}
        queries:
          - kind: TimeSeriesQuery
            spec:
              plugin:
                kind: PrometheusTimeSeriesQuery
                spec:
                  query: sum(rate(http_requests_total{namespace="$namespace"}[5m]))
  layouts:
    - kind: Grid
      spec:
        items:
          - x: 0
            "y": 0
            width: 12
            height: 8
            content:
              $ref: "#/spec/panels/requests"

Note how the layout points at the panel by $ref, which is why a panel can be defined once and placed anywhere.

The data source

A global data source gives every project one Prometheus, with the server proxying the calls:

kind: GlobalDatasource
metadata:
  name: prom
spec:
  default: true
  plugin:
    kind: PrometheusDatasource
    spec:
      proxy:
        kind: HTTPProxy
        spec:
          url: http://prometheus.monitoring.svc:9090

If the Prometheus needs credentials, put them in the proxy settings as a secret reference rather than in the URL, so they are stored encrypted and never sent to the browser.

Migrating from Grafana

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

--online lets the server’s plugin migration scripts translate panels and queries. Review the result: unmapped panels become placeholders. The practical approach is to migrate your most-used dashboards, rebuild the rest as code, and keep Grafana running for the long tail until you have decided.

Production reference design on Kubernetes

  1. Run the operator, not hand-written manifests. Install cert-manager and the Perses Operator, declare a Perses resource with SQL storage if you want several replicas, or file storage with a persistent volume for a single instance.
  2. Keep dashboards in Git as code. One dashboard per file in a repository, CUE or Go modules for shared panels and variables, a CI job that runs percli dac build, percli lint and, with a test server, percli lint --online. The perses/cli-actions GitHub workflow wraps the build, validate and apply steps.
  3. Deliver by GitOps. Generate PersesDashboard and PersesDatasource resources in CI and let Argo CD or Flux apply them; the operator syncs them to Perses. Where you are not using the operator, run percli apply from the pipeline against a service account.
  4. One project per team or application. Namespaces map to projects with the operator, so team boundaries follow Kubernetes boundaries, and roles and role bindings grant access per project.
  5. Authentication on, sign-up off. Enable authentication with an OIDC provider, disable sign-up, set your own encryption key from a Secret, and set a short token lifetime.
  6. Treat the UI as read-mostly. Let the UI be for exploration and editing drafts; promote changes by exporting them back to code. For a server that only serves provisioned dashboards, set security.readonly: true.
  7. Pin versions and watch security releases. Release 0.54.0 fixed two security issues (below), so subscribe to the project’s advisories and upgrade promptly.

Security

  • Turn authentication on. By default Perses does not block anything, so enable security.enable_auth, use OIDC or OAuth for people, and a dedicated service account for percli in CI.
  • Control access with projects and roles. Authorization uses a Kubernetes-inspired model of roles and role bindings scoped to projects, with global equivalents. Review guest_permissions so a logged-in user does not get more than intended.
  • Protect stored secrets. Data source passwords and tokens are encrypted with the configured 32-byte key. If you rely on the built-in default key, anyone with the code and a copy of the data can decrypt them, so set your own.
  • Keep credentials in the proxy. Browsers talk only to Perses; the datasource proxy holds the data source credentials. That makes the proxy’s authorization important: release 0.54.0 fixed a missing-authorization flaw in the datasource proxy that could disclose secrets across scopes, and a path traversal caused by an unvalidated project parameter. Both are the kind of defect to expect in a young project, and both are reasons to run a current release and to restrict network access.
  • Use TLS. Terminate TLS in front of Perses or configure it in the operator, and use TLS to data sources and to the SQL database.
  • Supply chain. Releases ship SBOMs for the binaries and the source; scan the image like any other.

Monitoring and day-2 operations

  • Server health. Watch pod status, restarts, request errors and latency at the ingress, and the logs for provisioning and plugin-loading errors. The operator provides built-in Prometheus metrics and alerting rules compatible with the Prometheus Operator.
  • Storage. For file storage, back up the data folder; for SQL, back up the database. With the operator and Git as the source of truth, a restore is “redeploy and reapply”, but secrets and users still need backing up.
  • Drift. If people edit dashboards in the UI, those edits can differ from Git. Decide the rule: provisioned and operator-managed resources are changed only in Git, and the UI is for drafts. Confirm how your version treats UI edits to operator-managed resources before relying on it.
  • Plugins and image size. Each release adds plugins and the project has said the default image may stop bundling some of them, and that plugins can be enabled or disabled by configuration, so read the release notes before upgrading.
  • Upgrades. Perses is pre-1.0, so the data model and the operator’s API can change between releases. Upgrade the server, percli and your SDK versions together, run your CI lint against the new version first, and keep the changelog open.
  • Capacity. The server is light; load comes from the data sources. The queries behind the panels, their time ranges and refresh intervals decide the load on Prometheus, so use variables and sensible defaults.

Usage cases

  • Dashboards for many services from one template. Generate per-team or per-service dashboards from shared CUE or Go modules, validated in CI.
  • GitOps observability. Deliver dashboards and data sources with Argo CD or Flux as Kubernetes resources.
  • Embedding visualization. Put Perses panels or dashboards inside another product’s UI with the npm packages.
  • A common specification. Use the open dashboard format as an interchange format between tools.
  • Prometheus, traces and logs in one place. View metrics, Tempo traces and Loki logs together.

In the Sprint 4 final project, a pharmaceutical plant’s machine and platform metrics are shown on Perses dashboards defined as code.

Version and project status

Perses is a CNCF Sandbox project, developed in the public perses/perses repository, with the plugins in perses/plugins, the operator in perses/perses-operator and the charts in perses/helm-charts. It is pre-1.0 (0.x versions), releases arrive every few months with frequent betas, and the dashboard model has been stabilising since it was first released. Version 0.52 added logs and profiling support (Loki, Pyroscope and new panels); 0.54.0 added more plugins, deprecated the @perses-dev/core npm package in favour of @perses-dev/spec and @perses-dev/client, and carried two security fixes. The operator reached 0.4.0 in April 2026 and a Helm chart for it is newly released, so expect rough edges there. The project’s roadmap page lists what is next.

Adoption

The CNCF’s announcement material notes that Red Hat and Chronosphere embedded Perses in their products and joined Amadeus as contributors, and Red Hat OpenShift uses Perses for part of its tracing UI. GreptimeDB embeds Perses in its built-in dashboard, and Dash0 uses the Perses dashboard format. These are vendor and integration relationships rather than a list of production end users, so ask the project about reference users for your sector.

Alternatives

Solution Model Best suited to
Grafana UI-first dashboards with a large plugin catalogue and alerting Teams that need the broadest data source coverage or Grafana’s alerting and ecosystem
Grafana with provisioning, Jsonnet or Terraform Dashboard-as-code on top of Grafana Teams that want code-managed dashboards but keep Grafana
Prometheus’ built-in expression browser Query and graph UI Ad-hoc queries, not shared dashboards
Kibana, OpenSearch Dashboards Dashboards over a search back end Log and event data in Elasticsearch or OpenSearch
Managed observability platforms Hosted dashboards Teams that prefer a vendor to run the stack

Strengths and limitations

Strengths Limitations
Dashboards are a clean, validated specification, so review and CI work well A young project: pre-1.0, with a data model and operator API that are still changing
Go and CUE SDKs allow reuse across hundreds of dashboards Fewer data sources and panel types than Grafana
Operator and CLI make GitOps delivery straightforward No alerting engine, so alert rules stay in Prometheus and Alertmanager
Apache 2.0 and an open specification, with embeddable components Grafana migration is best-effort; some panels need to be rebuilt by hand
Server proxy keeps data source credentials off the browser Authentication is off by default, and two permission flaws were fixed in 0.54.0

Recommendation

Trial. Perses is a strong fit when your problem is managing many dashboards as code, you query Prometheus, and you want a permissive licence and a Kubernetes-native workflow. Begin with one project, one data source and a handful of dashboards written in CUE or Go, wire the lint step into CI, turn on authentication from the start and run a current release. Keep Grafana alongside it while you migrate and while you depend on its alerting or its wider plugin set, and re-check the plugin list for every data source you need before you commit.

References: Perses documentation · Perses on GitHub · Dashboard-as-Code guide · percli reference · Perses Operator · Perses plugins · Helm charts · Perses on Docker Hub · CNCF project page · Why Perses joined the CNCF · CNCF Landscape entry

Spotted something wrong or out of date?Suggest a correction. I review every suggestion before changing the page.