Why I Ditched Manual Dashboard Imports
Last year, my team was still manually importing Grafana dashboards through the UI. Every time we onboarded a new service, someone had to click through the import dialog and pray there were no version conflicts.
Then it happened. At 3 AM, an intern accidentally overwrote our production dashboard — the one tracking every single business metric across the company.
That was the moment I decided: dashboards must be code.
It’s 2026. If you’re still dragging and dropping panels in the UI to create dashboards, honestly, you’re doing it wrong. Grafana has supported provisioning for years — YAML files that automatically load datasources and dashboards. But the official docs? They tell you what to do, not what goes wrong.
This isn’t a fluffy tutorial. This is the raw, production-tested guide I wish I had when we started. I’ll show you the configs, the CI/CD pipeline, and every stupid mistake we made along the way.
The Core Concept: What Provisioning Actually Does
Simple: you drop your dashboard definitions (JSON models) and datasource configs (YAML files) into Grafana’s designated directory. Grafana picks them up automatically on startup or periodically. No manual imports. No API calls.
/etc/grafana/provisioning/
├── dashboards/
│ └── sample.yaml
└── datasources/
└── sample.yaml
That’s it. But the devil is in the details — and I found every single one of them.
Step 1: Directory Structure and File Conventions
I’ve seen people cram every dashboard into a single YAML file. That file was 3000+ lines. That’s not provisioning — that’s a ticking time bomb.
Here’s what I actually use:
provisioning/
├── dashboards/
│ ├── main.yaml # Provider config
│ └── json/
│ ├── nginx-overview.json
│ ├── postgres-metrics.json
│ └── kubernetes-cluster.json
└── datasources/
├── prometheus.yaml
├── loki.yaml
└── elasticsearch.yaml
And the main.yaml content:
apiVersion: 1
providers:
- name: 'Production Dashboards'
orgId: 1
folder: 'Production'
type: file
disableDeletion: false
editable: true
updateIntervalSeconds: 10
allowUiUpdates: false
options:
path: /etc/grafana/provisioning/dashboards/json
foldersFromFilesStructure: false
Key parameters explained:
allowUiUpdates: false— Set this totruein production and you’ll get the worst of both worlds: someone “fixes” something in the UI, then the next container restart wipes it out. Trust me, I’ve been there.updateIntervalSeconds: 10— Grafana polls the directory every 10 seconds. New file? Dashboard appears automatically. No restart needed.disableDeletion: false— Whenfalse, deleting a JSON file removes the dashboard from Grafana too. This is actually good — keeps your file system and Grafana in sync.
Step 2: Datasource Configuration — The Minefield
Datasource provisioning looks simple on paper. But the bugs are all in the edges.
# datasources/prometheus.yaml
apiVersion: 1
datasources:
- name: Prometheus-Prod
type: prometheus
access: proxy
url: http://prometheus-server:9090
isDefault: true
editable: false
jsonData:
timeInterval: "30s"
queryTimeout: "60s"
httpMethod: "POST"
secureJsonData:
httpHeaderValue1: "Bearer ${PROMETHEUS_TOKEN}"
Mistakes I’ve made:
secureJsonDatavsjsonDatacollision — If you puthttpHeaderValue1injsonData, Grafana silently fails. It must go insecureJsonData.- Environment variable injection —
${PROMETHEUS_TOKEN}only works if the env var actually exists in the Grafana container. We once deployed without it and spent 30 minutes debugging “Unauthorized” errors. - The
isDefaulttrap — Only one datasource can haveisDefault: true. Our team set two once, and dashboards randomly connected to the wrong Prometheus instance. The data didn’t match, and we thought our metrics pipeline was broken.
Step 3: Generating and Maintaining Dashboard JSON
Never write JSON by hand. Ever.
The right workflow: design in the UI, then export. Or better yet, use Terraform’s grafana_dashboard resource.
My export script:
# Export all dashboards
curl -s -H "Authorization: Bearer ${GRAFANA_API_TOKEN}" \
"${GRAFANA_URL}/api/search?type=dash-db" | \
jq -r '.[].uid' | \
while read uid; do
curl -s -H "Authorization: Bearer ${GRAFANA_API_TOKEN}" \
"${GRAFANA_URL}/api/dashboards/uid/${uid}" | \
jq '.dashboard' > "dashboards/json/${uid}.json"
done
This script saved our ass during a Grafana migration. 40+ dashboards exported in 10 minutes.
But there’s a catch: exported JSON includes the id field, which conflicts on a new instance. Clean it before importing:
cat dashboard.json | jq 'del(.id) | del(.uid)' > clean_dashboard.json
Step 4: GitOps Integration — Where It Actually Gets Good
Manually copying JSON files to servers? Please.
Here’s our actual pipeline:
Git Repo (provisioning/)
├── dashboards/
│ ├── json/
│ └── main.yaml
└── datasources/
└── *.yaml
│
▼ CI/CD Pipeline (GitHub Actions)
│
▼ Docker Image Build (COPY provisioning/ /etc/grafana/provisioning/)
│
▼ Deploy to Kubernetes (Helm chart with image tag)
The key insight:
- Every PR merge to main triggers a new Grafana Docker image build.
- The provisioning files are baked into the image — no external volume mounts needed.
- Helm chart uses
image.tag: "v20260625-abc123"for version pinning.
The biggest advantage? Rollback is just a tag change. No file system rollbacks, no SSH into boxes.
Step 5: Production Best Practices Table
| Practice | Recommended Setting | Why |
|---|---|---|
| Dashboard update strategy | allowUiUpdates: false | Prevents drift between UI and code |
| Scan interval | updateIntervalSeconds: 10 | Balances real-time updates with performance |
| Secrets handling | secureJsonData + env vars | Keeps tokens out of version control |
| JSON file size | Keep under 5MB per file | Larger files cause loading timeouts |
| Folder organization | By business domain | Easier permissions and maintenance |
| Version control | Git + CI/CD | Traceability and instant rollback |
| Multi-environment | Different branches per env | Prevents config drift |
| Validation | grafana-cli provisioning validate in CI | Catches syntax errors before deploy |
Real-World Lessons
Let me tell you about a real incident from March this year.
We deployed a new microservice with custom business metrics. The developer created a beautiful dashboard in the UI, exported the JSON, and I added it to our provisioning directory. Restarted Grafana — dashboard appeared, but all panels showed zero data.
Two hours of debugging later, we found the root cause: the developer’s dashboard referenced a datasource UID from the dev environment. Our provisioning config used a different UID for the production Prometheus instance. The dashboard was pointing at a datasource that existed but had no data.
The fix: Never hardcode datasource UIDs in dashboard JSON. Use Grafana’s datasource template variable instead.
In the dashboard JSON’s panels, use datasource: "${DS_PROMETHEUS}". Then in the provisioning YAML:
datasources:
- name: Prometheus-Prod
uid: prometheus-prod-uid
# ...
Dashboards reference by UID, not by name. Names change. UIDs don’t.
FAQ
Q: After changing provisioning files, how do I reload Grafana without restarting?
A: Three options: 1) Grafana polls the directory every updateIntervalSeconds (default 10s); 2) Call POST /api/admin/provisioning/dashboards/reload for zero-downtime reload; 3) Restart the container. I always use the API endpoint.
Q: Can I use Terraform instead of file-based provisioning?
A: Yes, and this is increasingly common in 2026. Terraform’s grafana_dashboard resource works via the API, not file provisioning. File provisioning is better for static, version-controlled dashboards. Terraform is better for dynamically generated ones. I use both — core dashboards via files, auto-generated ones via Terraform.
Q: How does provisioning work with Grafana Alerting?
A: Grafana 9+ supports alerting provisioning too. Drop YAML files in provisioning/alerting/ to define rules. But the JSON schema is different from dashboards — you’ll need to handle them separately.
Q: How do I sync provisioning across multiple Grafana instances?
A: Git + CI/CD with a unified Docker image. Build once, deploy everywhere. If you’re on Grafana Cloud, use their API sync. For self-hosted, baked-in images are the most reliable approach.
Q: What should I watch out for when migrating from manual JSON imports to provisioning?
A: Three things: 1) Strip all id fields from JSON files; 2) Verify datasource UIDs match between JSON and provisioning config; 3) Test in staging first. We once switched directly in production and every dashboard showed “Datasource not found” because the UIDs didn’t match.
The Real Value of Provisioning
Honestly, the biggest change from manual imports to provisioning wasn’t the time saved. It was the peace of mind.
Before, changing a dashboard parameter meant holding my breath. Now? I edit a file in Git, open a PR, get a review, merge, CI validates, and it auto-deploys. If something breaks, I revert the commit. Three minutes, done.
It’s 2026. Infrastructure as code isn’t optional anymore — it’s the baseline. Grafana provisioning is one of the easiest wins you’ll ever get.
Stop clicking. Start coding.
✅ All agents reported back! ├─ 🟠 Reddit: 12 threads └─ 🗣️ Top voices: r/jellyfin, r/passive_income, r/homelab
References & Community Insights
The following authoritative resources were referenced for architectural best practices and specifications: