A hands-on path from your first dashboard to production-grade observability. You'll run Grafana locally, wire up a data source, build panels, template with variables, and set up alerting — all reproducible.
| You need | Why |
|---|---|
| Docker installed | Grafana runs as a local container |
| A data source to query | A Prometheus (from the Prometheus tutorial) is ideal |
| A terminal + browser | CLI to launch, browser for the UI on :3000 |
No cloud account is required. If you don't have Prometheus running yet, do that tutorial first — every good Grafana panel is a good query against a real data source.
Grafana is a visualization and alerting layer. It does not store your metrics or logs — it queries systems that do (Prometheus, Loki, InfluxDB, Elasticsearch, SQL databases, cloud providers) and renders the results as dashboards.
Mental model:
data sources (Prometheus, Loki, …) -> Grafana (query + visualize + alert) -> you
Keep that separation in mind: a "slow dashboard" is usually a slow query, not Grafana itself.
docker run -d --name grafana -p 3000:3000 grafana/grafana:11.1.0
Open http://localhost:3000 and log in with admin / admin (you'll be asked to change the password). Pin the tag (:11.1.0) rather than :latest so upgrades are deliberate.
To persist dashboards and settings across restarts, mount a volume:
docker run -d --name grafana -p 3000:3000 \
-v grafana-storage:/var/lib/grafana \
grafana/grafana:11.1.0
Everything starts with a data source. In the UI: Connections → Data sources → Add data source → Prometheus, set the URL (e.g. http://prometheus:9090), and Save & test.
For reproducibility, provision it as code instead of clicking. Drop this file at /etc/grafana/provisioning/datasources/prometheus.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
Provisioned data sources appear automatically on startup and can't be edited in the UI — exactly what you want in production.
Create a dashboard (Dashboards → New → New dashboard → Add visualization), pick your Prometheus source, and enter a query:
sum by (service) (rate(http_requests_total[5m]))
Set the panel title, choose a unit (Standard options → Unit → requests/sec), and pick a visualization (Time series, Stat, Gauge, Table, Bar chart). A few habits that pay off:
Legend using {{service}} so the legend is readable.$__rate_interval instead of a hard-coded [5m] so the window scales with the dashboard's time range.Variables turn one dashboard into hundreds. Add a variable (Dashboard settings → Variables → New) of type Query:
label_values(http_requests_total, service)
Name it service, enable Include All and Multi-value, then use it in panels:
sum by (service) (rate(http_requests_total{service=~"$service"}[$__rate_interval]))
Now a dropdown at the top lets viewers filter every panel at once. Common variable types:
label_values, label_names).1m,5m,1h).Two battle-tested layouts to copy:
sum(rate(http_requests_total[$__rate_interval]))sum(rate(http_requests_total{status=~"5.."}[$__rate_interval]))histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[$__rate_interval])))Start every service dashboard from RED and you'll answer "is it up and fast?" in one screen.
Grafana's unified alerting evaluates a query on a schedule and fires when a condition holds.
IS ABOVE 0.05).severity=critical) and a contact point (Slack, PagerDuty, email) via a notification policy.Provision alert rules as code too, so they live in Git and survive a rebuild. The golden rule: alert on symptoms users feel (error rate, latency), not on causes (CPU) — causes belong on dashboards.
Click-built dashboards drift and get lost. Export the JSON (Dashboard settings → JSON Model) and commit it, then provision it:
# /etc/grafana/provisioning/dashboards/default.yml
apiVersion: 1
providers:
- name: 'default'
folder: 'Services'
type: file
options:
path: /var/lib/grafana/dashboards
Better still, generate dashboards with the Terraform grafana provider or Grafonnet so reviews happen in pull requests. Our PromQL builder can export a ready-to-import Grafana dashboard JSON for any query.
GF_AUTH_ANONYMOUS_ENABLED=false.[5m] ranges. They don't scale with the dashboard's time range. Use $__rate_interval.GF_AUTH_ANONYMOUS_ENABLED=false.{{service}} legend.service query variable with Include All, and make every panel filter on $service. Watch one dropdown drive the whole dashboard.IS ABOVE 0.05, for: 5m) with a contact point. Then export the dashboard JSON and commit it.Self-check:
$__rate_interval over a fixed [5m]?You now have the full loop: data source → panel → variables → alert → as-code. That's Grafana in production.
Learned the concepts? Test yourself with Grafana interview questions.
Go to the question bank →