Provision Dashboards from JSON Files
Load dashboards from disk, map folders from directories, and author them without losing changes.
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:
nameis required and must be unique across providers.options.pathis the directory Grafana scans for JSON files. It must exist inside the container.updateIntervalSecondsis how often Grafana rescans the path. All dashboards are loaded at startup, and changes are picked up after that without a restart.allowUiUpdates: whenfalse, a provisioned dashboard can’t be saved from the UI, which keeps Git authoritative. Whentrue, UI saves are kept in the database until the source file changes, at which point the file overwrites them.disableDeletion: whentrue, removing a file does not remove its dashboard from Grafana. Leave itfalseif Git should be the truth.folderorfolderUidplace all dashboards in one named folder. Without them, dashboards go to the root.foldersFromFilesStructure: truemaps subdirectories ofpathto Grafana folders. LeavefolderandfolderUidempty 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
uidyou 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:
- Run a scratch instance (the compose setup from the earlier guide) where a provider has
allowUiUpdates: true. - Build or tweak the dashboard in the UI.
- 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.
- Remove the numeric
id, set a stableuid, and commit the file into the repository folder. - Open a pull request. The production Grafana picks it up after merge, and
allowUiUpdates: falsethere 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
-
Operate Grafana as Code: The Big Picture What can live in files, what needs an API, and how the pieces fit together. -
Configure the Grafana Server with grafana.ini and Environment Variables Know where config files live, how overrides are ordered, and how to keep settings in Git. -
Provision Alert Rules, Contact Points, and Notification Policies Define the whole alerting pipeline in YAML so it is reviewed and versioned like code. -
Deploy Grafana on Kubernetes with Helm and Provisioning Values Run Grafana from a Helm chart where data sources, dashboards, and settings all come from values files. -
Reload, Verify, and Troubleshoot Provisioning from the Command Line Find out what Grafana loaded, reload without a restart, and fix the usual provisioning errors.