Grafana Grafana

Provision Dashboards from JSON Files

Load dashboards from disk, map folders from directories, and author them without losing changes.

12 min Intermediate

Dashboards are JSON documents, which makes them easy to store in Git. A dashboard provider tells Grafana a folder to watch, and Grafana loads every JSON file it finds there, then keeps checking for changes. This guide covers the provider file, folder mapping, the dashboard JSON itself, and a workflow for authoring dashboards without ending up with UI-only edits.

The provider file

provisioning/dashboards/providers.yaml:

apiVersion: 1

providers:
  - name: default
    orgId: 1
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    allowUiUpdates: false
    options:
      path: /var/lib/grafana/dashboards
      foldersFromFilesStructure: true

What each field does:

  • name is required and must be unique across providers.
  • options.path is the directory Grafana scans for JSON files. It must exist inside the container.
  • updateIntervalSeconds is how often Grafana rescans the path. All dashboards are loaded at startup, and changes are picked up after that without a restart.
  • allowUiUpdates: when false, a provisioned dashboard can’t be saved from the UI, which keeps Git authoritative. When true, UI saves are kept in the database until the source file changes, at which point the file overwrites them.
  • disableDeletion: when true, removing a file does not remove its dashboard from Grafana. Leave it false if Git should be the truth.
  • folder or folderUid place all dashboards in one named folder. Without them, dashboards go to the root.
  • foldersFromFilesStructure: true maps subdirectories of path to Grafana folders. Leave folder and folderUid empty when you use it. Nested folders are not supported, so one level of subdirectory is the limit.

Organize the repository by folder

With foldersFromFilesStructure: true, this layout:

dashboards/
├── infrastructure/
│   ├── node-exporter.json
│   └── kubernetes.json
└── applications/
    └── webapp.json

creates two Grafana folders, infrastructure and applications, with the dashboards inside. Use separate providers instead if you need different settings per group, such as one provider with allowUiUpdates: true for a “scratch” area.

Dashboard JSON essentials

A provisioned file must contain the dashboard model itself, with uid and title at the top level. Do not wrap it in the {"dashboard": {...}, "overwrite": true} envelope that the HTTP API uses, which is a common cause of the “Dashboard title cannot be empty” error.

{
  "uid": "webapp-overview",
  "title": "Webapp Overview",
  "tags": ["webapp", "provisioned"],
  "schemaVersion": 39,
  "version": 1,
  "refresh": "30s",
  "time": { "from": "now-6h", "to": "now" },
  "templating": {
    "list": [
      {
        "name": "namespace",
        "type": "query",
        "datasource": { "type": "prometheus", "uid": "prometheus" },
        "query": "label_values(kube_pod_info, namespace)"
      }
    ]
  },
  "panels": [
    {
      "id": 1,
      "type": "timeseries",
      "title": "Request rate",
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "targets": [
        {
          "refId": "A",
          "expr": "sum(rate(http_requests_total{namespace=\"$namespace\"}[5m]))",
          "datasource": { "type": "prometheus", "uid": "prometheus" }
        }
      ]
    }
  ]
}

Guidelines for dashboards that behave well as code:

  • Set a stable uid. Grafana updates the dashboard with that UID on every load. Avoid reusing a UID across files, or the same title twice in one folder.
  • Leave out the numeric id. It is assigned by the database and differs between instances.
  • Reference data sources by UID, matching the uid you provisioned ({"type": "prometheus", "uid": "prometheus"}). Dashboards exported for sharing may contain placeholders like ${DS_PROMETHEUS}, which you must replace with the real UID, or with a data source template variable.
  • Environment variable interpolation does not work inside dashboard JSON. Use dashboard template variables for per-environment differences.
  • Grafana 13 stores dashboards in a newer schema internally and migrates older JSON when loaded, so existing files keep working.

Author in the UI, then bring the result back to Git

Hand-writing panel JSON is slow, so most teams draft in the UI and export. A workflow that keeps files as the truth:

  1. Run a scratch instance (the compose setup from the earlier guide) where a provider has allowUiUpdates: true.
  2. Build or tweak the dashboard in the UI.
  3. Export it as JSON from the dashboard’s share or export menu. Choose the option that exports for sharing externally if you want data sources replaced by inputs, then set the real UIDs.
  4. Remove the numeric id, set a stable uid, and commit the file into the repository folder.
  5. Open a pull request. The production Grafana picks it up after merge, and allowUiUpdates: false there prevents anyone from diverging.

You can also fetch an existing dashboard through the API:

curl -s -u admin:$GRAFANA_ADMIN_PASSWORD \
  http://localhost:3000/api/dashboards/uid/webapp-overview \
  | jq '.dashboard | del(.id)' > dashboards/applications/webapp.json

Format the JSON consistently (for example jq -S .) so diffs show real changes.

Generate dashboards from code

If many dashboards share a shape, generate the JSON instead of copying it. The Grafana Foundation SDK (Go, TypeScript, Python, PHP, Java) builds dashboards from typed code, and its output is the same JSON you drop into the provider folder. The Foundation SDK guide in this series covers it.

Validate in CI

Catch mistakes before they reach Grafana:

# valid JSON?
jq empty dashboards/**/*.json
# every file has a uid and title?
for f in dashboards/*/*.json; do
  jq -e '.uid and .title' "$f" >/dev/null || echo "missing uid/title: $f"
done
# duplicate uids?
jq -r '.uid' dashboards/*/*.json | sort | uniq -d

Apply and verify

Changes are picked up within updateIntervalSeconds. To force a rescan:

curl -X POST -u admin:$GRAFANA_ADMIN_PASSWORD \
  http://localhost:3000/api/admin/provisioning/dashboards/reload

curl -s -u admin:$GRAFANA_ADMIN_PASSWORD \
  "http://localhost:3000/api/search?type=dash-db" | jq '.[] | {title, uid, folderTitle}'

If a dashboard doesn’t appear, check the startup and rescan log lines from the provisioning logger. The usual causes are a path that doesn’t exist in the container, JSON wrapped in the API envelope, or two files sharing one uid.

Popular Guides