Provisioning Grafana as code, and Elasticsearch logs
-
Phase 1: Grafana setup and your first dashboards
-
Phase 2: Data sources, logs and metrics
-
Phase 3: Alerting, teams and running Grafana
Clicking data sources and dashboards together is fine for one Grafana. When you have three (test, staging, production) or a server that must be rebuilt in an hour, you want Grafana provisioning: YAML and JSON files on disk that Grafana reads at start-up, kept in Git so every change is reviewed and repeatable. In this lesson you export dashboards with the backup-bot token from Lesson 12, put them in a Git repository, and let Grafana provision a folder of dashboards and a data source from those files.
The data source you provision is new: Elasticsearch, fed by Filebeat with app01’s Nginx logs, so you see a second log store next to Loki. The break labs cover a provisioned dashboard that refuses to save, one tab character that stops Grafana from starting, and Elasticsearch running out of memory.
Where this lesson fits
Lesson 13 of 14 in Grafana Dashboards and Observability · Phase 3: Alerting, teams and running Grafana · Before this: Lesson 12, users and teams · Next: Lesson 14, troubleshooting
| Time | You need | By the end you can |
|---|---|---|
| About 110 minutes | graf01 and app01 from Lesson 12 (backup-bot token in ~/.grafana-backup-token, Nginx on app01 from Lesson 8); graf01 raised to 4 GB RAM in step 5 | Provision dashboards and data sources from Git, run Elasticsearch with Filebeat, and query Elasticsearch logs and counts in Grafana |
What it is, in one line
Provisioning means Grafana creates its data sources, dashboards and alerting objects from files in /etc/grafana/provisioning every time it starts, so the files (kept in Git) become the source of truth instead of clicks in the UI.
In Simple Words
The recipe card in a Chennai hotel kitchen
A hotel chain in Chennai serves the same sambar in all its branches. The head chef doesn’t phone each branch and describe it; she writes a recipe card, the card is copied to every kitchen, and any change goes through her first. If a branch cook “improves” the sambar on his own, the next morning the card wins and his change is gone.
The recipe card is the provisioning file in Git, each branch kitchen is a Grafana server, and the cook’s quiet change is someone editing a provisioned dashboard in the UI.
Engineer's View
What Grafana provisions and from where
At start-up Grafana reads every .yaml or .yml file under /etc/grafana/provisioning (the [paths] provisioning setting). Data sources are created or updated to match the files; dashboard providers point Grafana at folders of JSON files, which it rescans every updateIntervalSeconds. Values can come from environment variables ($VAR) or files ($__file{/path}), which keeps passwords out of Git.
| Folder | What goes there | Behaviour |
|---|---|---|
| datasources/ | apiVersion: 1 and a datasources list | Created or updated at start-up; a YAML error stops Grafana starting |
| dashboards/ | Providers: name, folder, path, allowUiUpdates | JSON files under path are loaded and rescanned |
| alerting/ | Rules, contact points, policies, templates | Same idea for alerting objects |
| plugins/ | App plugin settings | Enables and configures app plugins |
A provisioned dashboard is marked as such. With allowUiUpdates: false the UI refuses to save it, which is what you want when Git is the truth. disableDeletion stops Grafana deleting a dashboard when its file disappears. The other route is the HTTP API with a service account token, which suits pipelines and Terraform; files suit a server you rebuild.
For the second log store this course uses Elasticsearch 9.x rather than OpenSearch. Filebeat, the lightest shipper for Nginx logs, only ships to Elasticsearch (it refuses OpenSearch since its 7.13 product check), and Grafana’s built-in Elasticsearch data source supports 7.17, 8.x and 9.x, while OpenSearch needs a separate plugin. Both are Java and need about the same memory, so on an 8 GB laptop you run one node with a fixed 1 GB heap and give graf01 4 GB for this lesson.
How it works, step by step
1Files are read at start-up
When grafana-server starts, the provisioning service reads data source files first, then plugins, alerting and finally dashboard providers. Data sources with the same name or UID are updated to match the file. The diagram shows the path from a Git commit to objects in Grafana.
2Dashboard providers keep watching
Each dashboard provider names a folder in Grafana and a path on disk. Grafana loads every JSON file under that path, uses its uid to decide which dashboard to create or update, and rescans on a timer, so a git pull into that path shows up in Grafana within seconds without a restart.
3Logs reach Elasticsearch through Filebeat
On app01, Filebeat’s Nginx module tails /var/log/nginx/access.log and sends each line over HTTPS to Elasticsearch on graf01. An ingest pipeline inside Elasticsearch parses the line into fields such as http.response.status_code and url.original, and the document lands in the filebeat-9.x data stream. The diagram below shows that path and where Grafana reads it.
4Loki and Elasticsearch store logs differently
| Question | Loki (Lessons 7 and 8) | Elasticsearch (this lesson) |
|---|---|---|
| What is indexed | Only labels | Every field (full-text and structured) |
| Parsing | At query time with LogQL parsers, or in Alloy | At ingest time with ingest pipelines |
| Memory and disk | Light; object storage friendly | Heavier; JVM heap plus index size |
| Strong at | Cheap high-volume logs with known labels | Searching any field fast, aggregations on fields |
| Grafana query | LogQL | Lucene query plus metric aggregations (Count, Terms, Date histogram) |
Hands-on lab: Grafana as code in Git, plus Elasticsearch and Filebeat logs
Before you run any command, double-check it
Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.
Part A (steps 1 to 4) is pure Grafana provisioning on graf01. Part B (steps 5 to 11) adds Elasticsearch on graf01 and Filebeat on app01, then provisions the Elasticsearch data source from your Git repository. Take a snapshot of both VMs before step 5.
1Look at the provisioning folders
Run this on graf01:
sudo ls -R /etc/grafana/provisioning
sudo grep -E '^;?provisioning' /etc/grafana/grafana.iniYou should see:
/etc/grafana/provisioning:
access-control alerting dashboards datasources plugins
/etc/grafana/provisioning/access-control:
sample.yaml
/etc/grafana/provisioning/alerting:
sample.yaml
/etc/grafana/provisioning/dashboards:
sample.yaml
/etc/grafana/provisioning/datasources:
sample.yaml
/etc/grafana/provisioning/plugins:
sample.yaml
;provisioning = conf/provisioningThe package creates these folders with commented sample files. The provisioning line is commented in grafana.ini, but the packaged service passes cfg:default.paths.provisioning=/etc/grafana/provisioning, so that’s the folder in use.
✅ Checkpoint: /etc/grafana/provisioning has access-control, alerting, dashboards, datasources and plugins folders.
2Export dashboards to a Git repository with the backup-bot token
Paste this whole block on graf01:
sudo apt-get install -y git
mkdir -p ~/grafana-as-code/{dashboards,provisioning/datasources,provisioning/dashboards}
cd ~/grafana-as-code && git init -q && git config user.email student@lab.local && git config user.name student
cat > export.sh <<'EOF'
#!/bin/bash
# Export every dashboard the token can see into dashboards/<uid>.json
TOKEN=$(cat ~/.grafana-backup-token)
API=http://localhost:3000/api
for uid in $(curl -s -H "Authorization: Bearer $TOKEN" "$API/search?type=dash-db" | jq -r '.[].uid'); do
curl -s -H "Authorization: Bearer $TOKEN" "$API/dashboards/uid/$uid" \
| jq '.dashboard | del(.id, .version)' > "dashboards/$uid.json"
echo "exported $uid"
done
EOF
chmod +x export.sh && ./export.sh | tail -n 4
ls dashboards | wc -lThe script prints one exported line per dashboard with its UID, for example exported rYdddlPWk for Node Exporter Full, and you see the last four. The final number is how many JSON files you now have, which depends on how many dashboards you built in the earlier lessons (about a dozen if you did them all).
del(.id, .version) removes the numeric database ID and version, which belong to one Grafana server and would cause conflicts on another; the uid stays, because it’s the portable identity. The token is read from the 600 file, never typed into the script. Run this nightly from cron and commit the result, and you have dashboard backups with a history.
✅ Checkpoint: dashboards/ in ~/grafana-as-code holds one JSON file per dashboard backup-bot can see.
3Provision a folder of dashboards from the repository
Make one dashboard that lives only in Git: a copy of Grafana health with its own UID and title. Then point a dashboard provider at a folder that holds it.
Paste this whole block on graf01:
cd ~/grafana-as-code
mkdir -p dashboards/provisioned
jq '.uid = "grafana-health-code" | .title = "Grafana health (as code)"' \
dashboards/grafana-health-copy.json > dashboards/provisioned/grafana-health-code.json
cat > provisioning/dashboards/saatvedha.yaml <<'EOF'
apiVersion: 1
providers:
- name: saatvedha-git
orgId: 1
folder: Provisioned
type: file
disableDeletion: true
allowUiUpdates: false
updateIntervalSeconds: 30
options:
path: /var/lib/grafana/dashboards/saatvedha
EOF
sudo mkdir -p /var/lib/grafana/dashboards/saatvedha
sudo cp dashboards/provisioned/*.json /var/lib/grafana/dashboards/saatvedha/
sudo cp provisioning/dashboards/saatvedha.yaml /etc/grafana/provisioning/dashboards/
sudo chown -R root:grafana /var/lib/grafana/dashboards /etc/grafana/provisioning/dashboards/saatvedha.yaml
sudo systemctl restart grafana-server && sleep 10
curl -s -u admin:Lab@Grafana2026 localhost:3000/api/dashboards/uid/grafana-health-code \
| jq '{title: .dashboard.title, folder: .meta.folderTitle, provisioned: .meta.provisioned}'You should see:
{
"title": "Grafana health (as code)",
"folder": "Provisioned",
"provisioned": true
}Grafana created the Provisioned folder, loaded the file and marked the dashboard provisioned: true. Because allowUiUpdates is false, changes have to go through the file. disableDeletion: true protects you from an accidental empty folder on disk wiping the dashboard.
✅ Checkpoint: Grafana health (as code) is in the Provisioned folder and the API reports provisioned true.
4Commit the first version
Run this on graf01:
cd ~/grafana-as-code
printf '*.pass\n*.token\n' > .gitignore
git add . && git commit -qm "Export dashboards, add provisioned folder" && git log --oneline | head -n 1You should see this (your commit hash will differ):
3f2a9c1 Export dashboards, add provisioned folderAt work this repository lives on your Git server, changes come as merge requests, and a pipeline copies files to each Grafana. Secrets never go in: the .gitignore keeps password and token files out, and the data source file in step 10 only points at secret files with $__file{}.
✅ Checkpoint: git log shows your first commit in ~/grafana-as-code.
5Give graf01 4 GB and install Elasticsearch 9
Run this on your laptop with graf01 shut down (sudo poweroff), then start it:
VBoxManage snapshot graf01 take before-elastic
VBoxManage modifyvm graf01 --memory 4096
VBoxManage startvm graf01 --type headlessThen paste this whole block on graf01:
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch \
| sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/9.x/apt stable main" \
| sudo tee /etc/apt/sources.list.d/elastic-9.x.list
sudo apt-get update && sudo apt-get install -y elasticsearch | tee ~/elastic-install.txt | grep -A2 'generated password'You should see this (your password will differ):
The generated password for the elastic built-in superuser is : h7Qz+Kp2vXm1sT9wLe4R
If this node should join an existing cluster, you can reconfigure this withElasticsearch 9 turns on security during install: TLS on the HTTP port with its own CA in /etc/elasticsearch/certs/http_ca.crt, and a random password for the elastic superuser. Save that password now in ~/.elastic.pass with chmod 600; elasticsearch-reset-password -u elastic makes a new one if you lose it.
✅ Checkpoint: elasticsearch is installed, ~/elastic-install.txt contains the generated password, and you saved it in ~/.elastic.pass.
6Fix the heap at 1 GB and start Elasticsearch
Paste this whole block on graf01:
printf -- '-Xms1g\n-Xmx1g\n' | sudo tee /etc/elasticsearch/jvm.options.d/heap.options
sudo systemctl daemon-reload && sudo systemctl enable --now elasticsearch
sudo curl -s --cacert /etc/elasticsearch/certs/http_ca.crt -u "elastic:$(cat ~/.elastic.pass)" \
https://localhost:9200 | jq '{name, version: .version.number, tagline}'
free -m | head -n 2You should see this (your memory numbers will differ):
-Xms1g
-Xmx1g
Created symlink '/etc/systemd/system/multi-user.target.wants/elasticsearch.service' → '/usr/lib/systemd/system/elasticsearch.service'.
{
"name": "graf01",
"version": "9.5.6",
"tagline": "You Know, for Search"
}
total used free shared buff/cache available
Mem: 3916 2930 211 21 969 986Without the heap file, Elasticsearch sizes its heap from the VM’s total memory and competes with Grafana, Prometheus, Loki and InfluxDB on the same box. Equal -Xms and -Xmx reserve the heap once at start, so it can’t grow later and surprise you. The first start takes up to a minute while the JVM warms up.
✅ Checkpoint: Elasticsearch answers on https://localhost:9200 with version 9.x and name graf01, and free shows memory still available.
7Create a read-only user for Grafana and open 9200 to app01
Paste this whole block on graf01:
openssl rand -base64 18 | tr -d '\n' > ~/es-grafana-reader.pass && chmod 600 ~/es-grafana-reader.pass
sudo curl -s --cacert /etc/elasticsearch/certs/http_ca.crt -u "elastic:$(cat ~/.elastic.pass)" \
-X POST https://localhost:9200/_security/user/grafana_reader -H 'Content-Type: application/json' \
-d "{\"password\":\"$(cat ~/es-grafana-reader.pass)\",\"roles\":[\"viewer\"]}"; echo
sudo install -o root -g grafana -m 640 ~/es-grafana-reader.pass /etc/grafana/es-grafana-reader.pass
sudo install -o root -g grafana -m 640 /etc/elasticsearch/certs/http_ca.crt /etc/grafana/es-http-ca.crt
sudo ufw allow from 192.168.56.71 to any port 9200 proto tcpYou should see:
{"created":true}
Rule addedThe built-in viewer role can read indices but not write or change settings, which is all Grafana needs. Grafana reads the password and the CA certificate from files owned by root and readable by the grafana group, so neither ever appears in a provisioning file or in Git.
✅ Checkpoint: grafana_reader exists, both files are in /etc/grafana with mode 640 root:grafana, and ufw allows 9200 from 192.168.56.71 only.
8Install Filebeat on app01 with the Nginx module
Copy the CA first, on graf01:
sudo cat /etc/grafana/es-http-ca.crt | ssh student@app01 'cat > /tmp/es-http-ca.crt'Then paste this whole block on app01:
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch \
| sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/9.x/apt stable main" \
| sudo tee /etc/apt/sources.list.d/elastic-9.x.list
sudo apt-get update && sudo apt-get install -y filebeat
sudo install -m 644 /tmp/es-http-ca.crt /etc/filebeat/es-http-ca.crt
sudo filebeat keystore create
sudo filebeat keystore add ES_PWD # paste the elastic password when asked
sudo tee /etc/filebeat/filebeat.yml >/dev/null <<'EOF'
filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
output.elasticsearch:
hosts: ["https://192.168.56.70:9200"]
username: "elastic"
password: "${ES_PWD}"
ssl.certificate_authorities: ["/etc/filebeat/es-http-ca.crt"]
ssl.verification_mode: certificate
setup.ilm.enabled: true
EOF
sudo filebeat modules enable nginx
sudo tee /etc/filebeat/modules.d/nginx.yml >/dev/null <<'EOF'
- module: nginx
access:
enabled: true
var.paths: ["/var/log/nginx/access.log*"]
error:
enabled: false
EOF
sudo filebeat test config && sudo filebeat test outputYou should see:
Enabled nginx
Config OK
elasticsearch: https://192.168.56.70:9200...
parse url... OK
connection...
parse host... OK
dns lookup... OK
addresses: 192.168.56.70
dial up... OK
TLS...
security: server's certificate chain verification is enabled
handshake... OK
TLS version: TLSv1.3
dial up... OK
talk to server... OK
version: 9.5.6The keystore keeps the password out of filebeat.yml, and ${ES_PWD} pulls it in at start. verification_mode: certificate checks that the server certificate comes from your CA without insisting that its name matches 192.168.56.70. Using the elastic superuser keeps the lab short; at work you’d create a writer role limited to the filebeat indices.
✅ Checkpoint: filebeat test config says Config OK and test output ends with talk to server OK and the Elasticsearch version.

9Load the ingest pipeline and start shipping
Run this on app01, then the check on graf01:
sudo filebeat setup --pipelines --modules nginx
sudo systemctl enable --now filebeat
# on graf01, after running ~/exam-traffic.sh 120 from Lesson 8:
sudo curl -s --cacert /etc/elasticsearch/certs/http_ca.crt -u "elastic:$(cat ~/.elastic.pass)" \
'https://localhost:9200/_cat/indices/.ds-filebeat-*?h=index,docs.count'You should see this (your document count will differ):
Loaded Ingest pipelines
.ds-filebeat-9.5.6-2026.11.08-000001 1873The Nginx module’s pipeline runs inside Elasticsearch and turns each raw line into fields like source.address, http.request.method, url.original and http.response.status_code, and it moves the raw line itself from message to event.original, so the stored documents have no message field. Filebeat writes to the filebeat-9.5.6 data stream, and the .ds- index behind it rolls over by size and age under its lifecycle policy.
✅ Checkpoint: A .ds-filebeat-9.x backing index exists on graf01 and its docs.count grows while traffic runs.
10Provision the Elasticsearch data source from Git
Paste this whole block on graf01:
cd ~/grafana-as-code
cat > provisioning/datasources/elasticsearch.yaml <<'EOF'
apiVersion: 1
datasources:
- name: Elasticsearch app01 logs
uid: es-app01-logs
type: elasticsearch
access: proxy
url: https://localhost:9200
basicAuth: true
basicAuthUser: grafana_reader
editable: false
jsonData:
index: 'filebeat-*'
timeField: '@timestamp'
logMessageField: event.original
tlsAuthWithCACert: true
secureJsonData:
basicAuthPassword: $__file{/etc/grafana/es-grafana-reader.pass}
tlsCACert: $__file{/etc/grafana/es-http-ca.crt}
EOF
sudo cp provisioning/datasources/elasticsearch.yaml /etc/grafana/provisioning/datasources/
sudo chown root:grafana /etc/grafana/provisioning/datasources/elasticsearch.yaml
sudo systemctl restart grafana-server && sleep 10
curl -s -u admin:Lab@Grafana2026 localhost:3000/api/datasources/uid/es-app01-logs/health | jq .
git add . && git commit -qm "Provision Elasticsearch data source" && echo committedYou should see:
{
"message": "Elasticsearch data source is healthy.",
"status": "OK"
}
committedlogMessageField: event.original is the setting the UI calls Message field name, and it points at event.original because that’s where the Nginx module keeps the raw line; with message your log rows would come up empty. Access logs carry no log.level, so the Level field name stays empty and timeField keeps the default @timestamp, which the pipeline fills from the Nginx time.
The file holds only the location of the secrets, so it’s safe in Git. editable: false stops anyone changing the data source in the UI, which would be undone at the next restart anyway.
✅ Checkpoint: The health API returns Elasticsearch data source is healthy and the commit succeeded.
11Query logs and counts from Elasticsearch in Grafana
In Explore pick Elasticsearch app01 logs, set the time range to the last 15 minutes and try both query types:
| Query type | Lucene query | Settings | What you get |
|---|---|---|---|
| Logs | event.dataset:nginx.access AND http.response.status_code:500 | none | The failing /api/submit lines with parsed fields |
| Metrics | event.dataset:nginx.access | Metric Count, Group by Terms http.response.status_code, then Date histogram @timestamp | One line per status code over time |

Add the Metrics query as a panel on a new dashboard and save it in SaatVedha Lab. Compare it with the Loki status graph from Lesson 8: same traffic, but here every field was parsed at ingest and can be searched or aggregated without a parser in the query.
✅ Checkpoint: Explore shows Nginx log lines from Elasticsearch, and a Count by status code graph shows 200, 404 and 500.
Break It, Then Fix It
Snapshot graf01 before Break 3; it deliberately starves the VM of memory.
1The Save button refuses a provisioned dashboard
Open Grafana health (as code), click Edit, change a panel title and click Save dashboard.
The save drawer opens with the title Provisioned dashboard and this text:
This dashboard cannot be saved from the Grafana UI because it has been provisioned from another source. Copy the JSON or save it to a file below, then you can update your dashboard in the provisioning source.
See documentation for more information about provisioning.Below that are the file path, the dashboard JSON and three buttons: Save JSON to file, Copy JSON to clipboard and Cancel. There is no button that saves to Grafana.

That’s allowUiUpdates: false doing its job. If saving were allowed, the change would live only in grafana.db and be overwritten the next time the file changed, which is worse than an error.
Fix: make the change in Git, on graf01:
cd ~/grafana-as-code
jq '.panels[0].title = "Grafana version (from Git)"' dashboards/provisioned/grafana-health-code.json > /tmp/gh.json \
&& mv /tmp/gh.json dashboards/provisioned/grafana-health-code.json
git commit -qam "Rename version panel" && sudo cp dashboards/provisioned/*.json /var/lib/grafana/dashboards/saatvedha/
sleep 35; curl -s -u admin:Lab@Grafana2026 localhost:3000/api/dashboards/uid/grafana-health-code | jq -r '.dashboard.panels[0].title'Grafana version (from Git)✅ Checkpoint: The new panel title shows in Grafana within about 30 seconds, without a restart, and is committed in Git.
2One tab stops Grafana starting
Someone edits the data source file in a hurry with an editor that inserts a tab:
Run this on graf01:
sudo cp /etc/grafana/provisioning/datasources/elasticsearch.yaml /root/es-ds.yaml.good
sudo sed -i 's/^ url: /\turl: /' /etc/grafana/provisioning/datasources/elasticsearch.yaml
sudo systemctl restart grafana-server; sleep 5; systemctl is-active grafana-server
sudo journalctl -u grafana-server --since "1 min ago" | grep -m1 'provision data sources'systemctl is-active prints activating or failed, because the unit has Restart=on-failure and systemd gives up after a few quick failures. The journal shows why (your times and PID will differ):
Nov 08 13:05:12 graf01 grafana[5120]: logger=provisioning t=2026-11-08T13:05:12.418593025Z level=error msg="Failed to provision data sources" error="Datasource provisioning error: yaml: line 6: found a tab character that violates indentation"Data source provisioning runs before Grafana serves anything, and an error there stops the whole server: one tab in one file takes every dashboard down. YAML forbids tabs for indentation, and the line number points near the problem (here the line before the tab).
Fix: restore the good file and check YAML before every deploy:
sudo cp /root/es-ds.yaml.good /etc/grafana/provisioning/datasources/elasticsearch.yaml
grep -nP '\t' /etc/grafana/provisioning/datasources/*.yaml || echo "no tabs"
sudo systemctl restart grafana-server && systemctl is-active grafana-serverno tabs
activeIn a pipeline, run a YAML linter (or the same grep) on every change before files are copied to a server, and copy to one test Grafana first.
✅ Checkpoint: grafana-server is active again and the Elasticsearch data source health check is OK.
3Elasticsearch runs out of memory
A colleague reads “give Elasticsearch half the RAM” on a forum and sets a 3 GB heap on the 4 GB VM, ignoring everything else running there:
Run this on graf01:
printf -- '-Xms3g\n-Xmx3g\n' | sudo tee /etc/elasticsearch/jvm.options.d/heap.options
sudo systemctl restart elasticsearch
sleep 90; systemctl status elasticsearch --no-pager | head -n 3
sudo journalctl -u elasticsearch --since "5 min ago" | grep -i -m2 'oom'When the OOM killer steps in, you’ll get this (your times will differ):
× elasticsearch.service - Elasticsearch
Loaded: loaded (/usr/lib/systemd/system/elasticsearch.service; enabled; preset: enabled)
Active: failed (Result: oom-kill) since Sun 2026-11-08 13:12:40 UTC; 20s ago
Nov 08 13:12:40 graf01 systemd[1]: elasticsearch.service: The kernel OOM killer killed some processes in this unit.
Nov 08 13:12:40 graf01 systemd[1]: elasticsearch.service: Failed with result 'oom-kill'.Elasticsearch starts the JVM with -XX:+AlwaysPreTouch, so it touches the whole 3 GB heap at start instead of growing into it. If graf01 has a swap file, the VM may crawl for a few minutes before the kill, and the status shows activating until then.
With a fixed 3 GB heap the JVM claims that memory up front, Grafana, Prometheus, Loki and InfluxDB still need theirs, and the kernel’s OOM killer picks the biggest process, Elasticsearch. On a busier box the killer may take Grafana or Prometheus instead, which is even harder to spot. The “half the RAM” advice is for a server that runs only Elasticsearch.
Fix: put the heap back to what the box can afford:
printf -- '-Xms1g\n-Xmx1g\n' | sudo tee /etc/elasticsearch/jvm.options.d/heap.options
sudo systemctl restart elasticsearch && sleep 40 && systemctl is-active elasticsearch-Xms1g
-Xmx1g
activeA related mistake gives “No data” with no error at all: an index name in the data source that matches nothing, such as logstash-* on a Filebeat setup. Check with _cat/indices which names exist before blaming Grafana.
✅ Checkpoint: Elasticsearch is active with a 1 GB heap and Explore shows the Nginx logs again.
This happened at work
The Saturday rebuild at the bank
A DevOps engineer at a bank in Chennai told me about the weekend their Grafana VM’s disk failed during a storage migration. The backups of grafana.db were three weeks old, and the operations floor needed its dashboards by Monday 9 am for the month-end batch.
The team’s first plan was to restore the old database and rebuild the missing three weeks by hand from screenshots in emails. That worked for about two dashboards before everyone realised nobody remembered which data source settings, passwords and alert rules had changed. Then one engineer remembered a half-finished Git repository from a proof of concept, with data source YAML and a script that exported dashboards every night for one team.
They finished the job for the remaining teams on Sunday: exported the latest dashboards they could find from a staging Grafana, wrote provisioning files for the seven data sources, and rebuilt the server from the repository in under an hour once the files were ready. Afterwards the rule was simple: every data source, dashboard and alert rule for production lives in Git, changes go through merge requests, and the nightly export alerts the team if it fails.
Common mistakes and troubleshooting
| Mistake | What happens | What to do instead |
|---|---|---|
| Putting passwords or tokens in provisioning files in Git | Secrets leak to everyone with repository access | Use $__file{} or environment variables and keep secret files out of Git |
| Allowing UI updates on provisioned dashboards | Changes live only in grafana.db and vanish at the next file change | Keep allowUiUpdates false and change the JSON in Git |
| Copying untested YAML straight to production | A tab or bad indent stops Grafana starting | Lint YAML in the pipeline and deploy to a test Grafana first |
| Exporting dashboards with their numeric id | Imports clash or overwrite the wrong dashboard on another server | Remove id and version, keep uid as the identity |
| Letting Elasticsearch size its own heap on a shared VM | The OOM killer stops Elasticsearch or another service | Set equal -Xms and -Xmx in jvm.options.d to what the box can afford |
| Using the elastic superuser for Grafana | A dashboard bug or leak has full cluster rights | Give Grafana a read-only user such as one with the viewer role |
| Wrong index name in the data source | No data, often without an error | Check _cat/indices and use the pattern that exists, like filebeat-* |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. What is Grafana provisioning and why use it?
Why they ask this: A basic definition with a practical reason.
How you might answer: Provisioning means Grafana creates data sources, dashboards and alerting objects from YAML and JSON files in /etc/grafana/provisioning when it starts. I use it so the configuration lives in Git, gets reviewed, and a server can be rebuilt or copied to staging from the same files.
2. Where would you keep a data source password if the provisioning file is in Git?
Why they ask this: They check basic secret hygiene.
How you might answer: Not in the file. I reference it with $__file{} pointing to a file readable only by root and the grafana group, or with an environment variable set in the service environment. The Git copy then only shows where the secret comes from.
2–5 years experience
3. A teammate says “I fixed the dashboard but my change disappeared”. What happened?
Why they ask this: They want the provisioning overwrite behaviour.
How you might answer: The dashboard is provisioned. If UI updates were allowed, their save went to the database, and the next time the JSON file changed Grafana overwrote it from the file. If UI updates were off they couldn’t save at all.
The fix is to make the change in the JSON in Git; I usually copy the panel JSON from the UI into the file and commit it.
4. Grafana won’t start after a deploy of new provisioning files. How do you find the cause?
Why they ask this: A practical debugging question.
How you might answer: I check systemctl status and journalctl for grafana-server and look for the provisioning logger. A YAML problem shows as Datasource provisioning error with the line number, for example a tab character, and Grafana exits because data sources are provisioned before it serves.
I restore the previous file from Git, start Grafana, then fix and lint the change before redeploying.
Senior level
5. Your company already has Loki. When would you also run Elasticsearch, and what would you watch for?
Why they ask this: They want a balanced tool comparison and capacity thinking.
How you might answer: I’d add Elasticsearch when teams need fast search on any field, heavy aggregations across many fields, or existing Elastic tooling; Loki stays cheaper for high-volume logs queried by known labels. The cost is memory and disk: each node needs a fixed JVM heap, typically no more than half the RAM of a dedicated box, and indices need lifecycle policies.
I’d watch heap usage, disk watermarks, shard counts and ingest pipeline failures, and give Grafana a read-only user.
6. How would you manage dashboards for 40 teams across dev, staging and production Grafana?
Why they ask this: They are checking scalable process design.
How you might answer: One Git repository per team or one with folders per team, dashboards as JSON with fixed UIDs and data source variables instead of hard-coded UIDs, and providers using foldersFromFilesStructure so folders follow the repo. A pipeline lints JSON and YAML, deploys to dev, then staging, then production.
Teams can experiment in a scratch folder with UI saves allowed, and anything that matters gets exported and merged into Git. A nightly export from production catches drift.
Quick quiz
Q1. Where does a packaged Grafana look for provisioning files?
- /var/lib/grafana/plugins
- /etc/grafana/provisioning
- /usr/share/grafana/public
- /etc/grafana/dashboards.d
Show answer
B — The deb package points the provisioning path at /etc/grafana/provisioning.
Q2. What does allowUiUpdates: false do for a provisioned dashboard?
- Hides it
- Prevents saving it from the UI
- Deletes it on restart
- Makes it read-only for admins only
Show answer
B — The UI refuses to save and tells you to update the provisioning source instead.
Q3. A data source YAML file has a tab for indentation. What happens on restart?
- Grafana ignores that file
- Grafana fails to start with a Datasource provisioning error
- Only that data source shows an error
- Grafana converts tabs to spaces
Show answer
B — Data source provisioning errors stop the server from starting.
Q4. Why did this course choose Elasticsearch over OpenSearch for Filebeat logs?
- OpenSearch can’t store logs
- Filebeat refuses OpenSearch and Grafana’s built-in data source supports Elasticsearch 9
- Elasticsearch needs no memory
- OpenSearch has no HTTPS
Show answer
B — Filebeat checks for Elasticsearch and Grafana’s core data source targets Elasticsearch; OpenSearch needs a plugin.
Q5. Elasticsearch on a 4 GB VM with -Xmx3g is killed. What is the right fix here?
- Add more Grafana panels
- Lower the heap to what the box can afford, like 1 GB
- Disable TLS
- Delete the indices
Show answer
B — The heap must leave room for the OS and the other services on the VM.
Quick recap
- Grafana reads /etc/grafana/provisioning at start-up; data source YAML errors stop the server, dashboard providers rescan JSON folders.
- ~/grafana-as-code holds exported dashboards (no id or version), a provider for the Provisioned folder and the Elasticsearch data source file.
- Secrets stay out of Git through $__file{} pointing at root:grafana 640 files.
- Elasticsearch 9 runs on graf01 (now 4 GB RAM) with a fixed 1 GB heap and TLS; Filebeat on app01 ships Nginx logs through the nginx module pipeline.
- Grafana queries Elasticsearch with Lucene for logs and Count by terms and date histogram for metrics, through the read-only grafana_reader user.
Next lesson
Lesson 14: Troubleshooting Grafana and where to go next → You learn a fixed troubleshooting order, read Grafana logs and the query inspector, speed up slow dashboards, back up and move grafana.db, and finish with a Service Health dashboard.
Further reading: Grafana provisioning
Want to go deeper into Grafana provisioning? Grafana’s provisioning reference and Elasticsearch data source pages, Elastic’s guides to installing Elasticsearch with the Debian package and the Filebeat Nginx module cover the details. Lesson 12 created the backup-bot service account used here, and Lesson 8 compares the same logs in Loki.
You can also see the full plan for this course on the Grafana Dashboards and Observability course page.
