How to Self-Host Grafana: The Complete Developer Guide to Monitoring Dashboards
You don't need a managed SaaS to get serious about observability. Grafana is open source, runs beautifully in Docker, and gives you production-grade dashboards without the per-seat pricing. This guide walks you through exactly how to self-host Grafana, wire it up to Prometheus, provision dashboards as code, and build alerts that actually wake you up when things break.
If you want a deeper dive into production architecture decisions, check out See Everything: How to Self-Host Grafana for Production Monitoring. For Docker Compose plus systemd deployment specifics, see How to Deploy Grafana in Production with Docker Compose + systemd.
What You'll Need Before You Start
Don't skip this. Running Grafana is easy. Running it well means having the basics in place first.
- Linux server with Docker and Docker Compose installed (Ubuntu 22.04+ or Debian 12+ recommended)
- 2 CPU cores, 4GB RAM minimum for a small stack (Grafana + Prometheus + Node Exporter)
- 20GB disk for metrics retention at default 15-day scrape intervals
- A domain or subdomain pointed at your server (optional but strongly recommended for TLS)
- Basic familiarity with YAML, Docker, and Prometheus concepts
If you're planning to add log aggregation with Loki, our Production Guide: Deploy Grafana Loki + Promtail with Docker Compose + Traefik + Let's Encrypt on Ubuntu covers that stack end-to-end.
1. Spin Up Grafana with Docker Compose
The fastest way to self-host Grafana is a single docker-compose.yml. No package managers. No systemd units yet. Just get it running.
Create a project directory and drop this in:
version: "3.8"
services:
grafana:
image: grafana/grafana:11.0.0
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=changeme
- GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-piechart-panel
volumes:
- grafana-storage:/var/lib/grafana
- ./provisioning:/etc/grafana/provisioning
volumes:
grafana-storage:
Then run it:
docker compose up -d
Hit http://your-server-ip:3000 and log in with admin / changeme. You now have Grafana running. But it's not monitoring anything yet.
Why pin the image version?
Always pin your Grafana image tag. grafana/grafana:latest will bite you during an unexpected upgrade. Pin to a specific major.minor like 11.0.0, test upgrades in staging, and bump deliberately.
2. Add Prometheus as Your Metrics Source
Grafana without data is just a pretty empty frame. Prometheus is the standard pairing. Here's a minimal docker-compose.yml that adds Prometheus and Node Exporter for host metrics:
version: "3.8"
services:
grafana:
image: grafana/grafana:11.0.0
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=changeme
volumes:
- grafana-storage:/var/lib/grafana
- ./provisioning:/etc/grafana/provisioning
networks:
- monitoring
prometheus:
image: prom/prometheus:v2.52.0
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.enable-lifecycle'
networks:
- monitoring
node-exporter:
image: prom/node-exporter:v1.8.0
container_name: node-exporter
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
networks:
- monitoring
volumes:
grafana-storage:
prometheus-data:
networks:
monitoring:
driver: bridge
And the prometheus.yml config:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
Run docker compose up -d again. Prometheus now scrapes itself and your host every 15 seconds. Grafana can query it.
3. Provision Data Sources and Dashboards as Code
Clicking through the UI to add data sources is fine once. Doing it on every deploy is not. Grafana's provisioning system lets you define data sources and dashboards in YAML and JSON files that load on startup.
Create this directory structure:
provisioning/
├── dashboards/
│ ├── dashboards.yml
│ └── node-exporter.json
└── datasources/
└── datasources.yml
provisioning/datasources/datasources.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
provisioning/dashboards/dashboards.yml:
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/provisioning/dashboards
For the dashboard JSON, grab the official Node Exporter Full dashboard from Grafana.com, save it as node-exporter.json in provisioning/dashboards/, and restart:
docker compose restart grafana
Your dashboard appears automatically. No clicks required. This is how you self-host Grafana like a professional — everything in Git, everything reproducible.
4. Build Your First Custom Dashboard
Provisioning gets you started. But you'll need custom dashboards for your applications. Here's the workflow:
- Explore your metrics in Prometheus at
:9090first. Use the expression browser to test queries. - Create a panel in Grafana with that query. Tweak the visualization type, units, and thresholds.
- Export the dashboard JSON (Share → Export → Save to file) and commit it to
provisioning/dashboards/.
Example query for CPU usage:
100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
Example query for memory usage:
100 * (1 - ((node_memory_MemAvailable_bytes or node_memory_MemFree_bytes) / node_memory_MemTotal_bytes))
Pro tip: use Grafana variables for things like $instance and $job so one dashboard works across all your servers.
5. Set Up Alerting That Actually Works
Grafana 11 ships with unified alerting built in. No more separate Alertmanager setup unless you want it. Here's how to configure a simple high-CPU alert via provisioning:
Create provisioning/alerting/alerting.yml:
apiVersion: 1
contactPoints:
- orgId: 1
name: 'email-alerts'
receivers:
- uid: 'email'
type: email
settings:
addresses: [email protected]
singleEmail: false
policies:
- orgId: 1
receiver: 'email-alerts'
group_by: ['alertname', 'grafana_folder', 'uid']
groups:
- orgId: 1
name: 'infrastructure-alerts'
folder: 'Infrastructure'
interval: 60s
rules:
- uid: 'high-cpu'
title: 'High CPU Usage'
condition: 'B'
data:
- refId: 'A'
relativeTimeRange:
from: 300
to: 0
datasourceUid: 'prometheus'
model:
expr: |
100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
refId: 'A'
- refId: 'B'
relativeTimeRange:
from: 0
to: 0
datasourceUid: '__expr__'
model:
type: threshold
expression: 'A'
conditions:
- evaluator:
type: gt
params:
- 80
Mount the alerting folder in your compose file:
volumes:
- ./provisioning:/etc/grafana/provisioning
Restart Grafana and your alert is live. When CPU stays above 80% for five minutes, you get an email. Adjust thresholds to your environment — 80% might be normal for batch workloads.
6. Secure Your Grafana Instance
Running Grafana on port 3000 with a default password is not acceptable for production. Lock it down.
Change the admin password immediately
Use environment variables, not hardcoded defaults:
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
Store the actual password in a .env file that never gets committed.
Enable HTTPS
Put Traefik, Nginx, or Caddy in front of Grafana. Here's a minimal Traefik snippet:
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`grafana.yourdomain.com`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
For a full Traefik + Let's Encrypt + Loki stack, our Production Guide: Deploy Grafana Loki + Promtail with Docker Compose + Traefik + Let's Encrypt on Ubuntu has you covered.
Disable signup and anonymous access
environment:
- GF_AUTH_ANONYMOUS_ENABLED=false
- GF_AUTH_DISABLE_SIGNOUT_MENU=false
- GF_AUTH_DISABLE_LOGIN_FORM=false
7. Tips, Gotchas, and Troubleshooting
Here is what breaks when you self-host Grafana — and how to fix it fast.
Dashboards not showing after provisioning
Check that your dashboard provider YAML points to the right path. The path is inside the container, not your host. If you mount ./provisioning/dashboards to /etc/grafana/provisioning/dashboards, your provider path should be /etc/grafana/provisioning/dashboards.
Permission denied on bind mounts
Grafana runs as user ID 472 inside the container. If you use bind mounts, the host directory needs to be writable by that UID:
sudo chown -R 472:472 ./data
Or run Grafana as your host user by setting user: "1000:1000" in Docker Compose.
Alerts not firing
Verify your alert query actually returns data in Explore. Then check the alert state in Alerting → Alert rules. A common mistake is setting the threshold condition reference (refId: 'B') but forgetting to define the expression query with __expr__ as the datasource.
High memory usage
Grafana's default SQLite database is fine for small setups. For teams or heavy dashboard usage, switch to PostgreSQL or MySQL:
environment:
- GF_DATABASE_TYPE=postgres
- GF_DATABASE_HOST=postgres:5432
- GF_DATABASE_NAME=grafana
- GF_DATABASE_USER=grafana
- GF_DATABASE_PASSWORD=${DB_PASSWORD}
Back up your dashboards
Since you're provisioning dashboards as JSON files, your Git repo is your backup. For the Grafana database (users, permissions, annotations), snapshot the volume regularly:
docker run --rm -v grafana-storage:/data -v $(pwd):/backup alpine tar czf /backup/grafana-backup-$(date +%F).tar.gz -C /data .
Closing Thoughts
Self-hosting Grafana gives you full control over your monitoring stack, your data, and your costs. With Docker Compose, provisioning as code, and a solid alerting setup, you have an observability platform that rivals anything SaaS — without the recurring bill.
Start with the basics: Grafana + Prometheus + Node Exporter. Provision your data sources and dashboards in YAML. Add alerts that matter. Lock it down with HTTPS and proper auth. Then iterate.
If you're running this in production and need help with scaling, custom integrations, or hardening — get in touch with our team. We build and maintain monitoring stacks for teams that can't afford blind spots.