Self-Host Grafana: Build a Full Monitoring Stack With Dashboards That Actually Tell You Something
Most developers only find out something is wrong after a user reports it. A proper monitoring stack flips that — you see the problem forming before it becomes an incident. Grafana is the visualization layer that ties it all together: connect your metrics, logs, and traces, and build dashboards that give you a real-time picture of everything running on your infrastructure. This guide shows you how to self-host Grafana with Prometheus and Node Exporter using Docker, from zero to a fully operational monitoring stack.
Prerequisites
- A Linux server or local machine (Ubuntu 20.04+ recommended)
- Docker Engine and Docker Compose v2 installed
- At least 1GB RAM free — Grafana and Prometheus are lightweight
- Basic familiarity with Docker Compose and YAML
- Port 3000 available for Grafana (or configure a reverse proxy like Traefik)
Verify Docker is ready and check available resources:
docker --version
docker compose version
free -h
df -h /
Understanding the Stack: Grafana, Prometheus, and Exporters
Before writing a single config line, it helps to understand how these pieces fit together. They each do one job well:
The Three Components
- Prometheus — a time-series database that scrapes metrics from targets on a schedule. It stores numeric measurements over time: CPU usage, memory, request counts, error rates. It also handles alerting rules.
- Node Exporter — a small agent that runs on each machine and exposes host-level metrics (CPU, RAM, disk, network) to Prometheus via an HTTP endpoint. One Node Exporter per machine you want to monitor.
- Grafana — the visualization layer. It doesn't store data — it queries Prometheus (and other sources) and renders the results as dashboards, graphs, and alerts. This is the UI you'll actually use day to day.
The flow is: Node Exporter exposes metrics → Prometheus scrapes and stores them → Grafana queries and visualizes them. You can self-host all three on the same server or spread them across your infrastructure.
Deploying the Full Stack with Docker Compose
Project Structure
Create a clean directory and set up the config files before bringing anything up:
mkdir -p ~/monitoring/prometheus
cd ~/monitoring
Prometheus Configuration
Create the Prometheus scrape config. This tells Prometheus what to collect and how often:
# prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Prometheus monitors itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Host metrics via Node Exporter
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
# Docker container metrics via cAdvisor
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
The Full Docker Compose File
This Compose file brings up Grafana, Prometheus, Node Exporter, and cAdvisor (for container-level metrics) as a complete monitoring stack:
# docker-compose.yml
version: '3.8'
services:
grafana:
image: grafana/grafana-oss:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=strongpassword
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
volumes:
- grafana_data:/var/lib/grafana
networks:
- monitoring
depends_on:
- prometheus
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus/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=30d'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
networks:
- monitoring
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
pid: host
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
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
- /dev/disk:/dev/disk:ro
networks:
- monitoring
volumes:
grafana_data:
prometheus_data:
networks:
monitoring:
driver: bridge
Start the stack:
docker compose up -d
docker compose ps
# Verify Prometheus is scraping targets
curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}'
All three targets (prometheus, node-exporter, cadvisor) should show "health": "up". Now open http://localhost:3000, log in with your admin credentials, and Grafana is running.
Connecting Prometheus to Grafana
Adding the Data Source
In Grafana, go to Connections → Data Sources → Add data source → Prometheus. Set the URL to http://prometheus:9090 — using the container name works because both services are on the same Docker network. Leave everything else at defaults and click Save & Test. You should see a green Data source is working confirmation.
Provisioning Data Sources Automatically
For a reproducible setup, provision the data source via config file instead of the UI. Create a provisioning directory and file:
mkdir -p ~/monitoring/grafana/provisioning/datasources
# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
Mount this directory in your Grafana service by adding to the volumes section:
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
Recreate the Grafana container and the data source is wired up automatically on every fresh deploy — no UI clicks needed:
docker compose up -d --force-recreate grafana
Building and Importing Dashboards
Importing a Community Dashboard
The fastest way to get useful dashboards is to import from Grafana's community library at grafana.com/grafana/dashboards. These are battle-tested dashboards built by the community. The most useful ones for this stack:
- Node Exporter Full — Dashboard ID
1860— comprehensive host metrics: CPU, memory, disk I/O, network, filesystem - Docker and system monitoring — Dashboard ID
893— container-level metrics from cAdvisor - Prometheus Stats — Dashboard ID
2— Prometheus self-monitoring
To import: Dashboards → New → Import, enter the dashboard ID, select your Prometheus data source, and click Import. You'll have a fully populated dashboard in under 30 seconds.
Writing Your First PromQL Query
Understanding PromQL is what separates useful dashboards from ones that just look busy. Here are the queries you'll use constantly:
# CPU usage percentage across all cores
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# Memory usage percentage
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100
# Disk usage percentage per mount
(1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"})) * 100
# Network traffic in (bytes/sec)
rate(node_network_receive_bytes_total{device!~"lo"}[5m])
# Container CPU usage
rate(container_cpu_usage_seconds_total{name!=""}[5m]) * 100
# Container memory usage
container_memory_usage_bytes{name!=""}
In the Grafana panel editor, paste any of these into the query field and hit Run queries. The Explore view (Explore → Prometheus) is the best place to iterate on PromQL before building it into a panel.
Setting Up Alerts
Creating an Alert Rule in Grafana
Grafana's built-in alerting (Grafana Alerting, not the old per-panel alerts) lets you define alert rules, contact points, and notification policies from a unified UI. Go to Alerting → Alert Rules → New alert rule.
A practical example — alert when disk usage exceeds 85%:
# Alert rule PromQL expression
(1 - (
node_filesystem_avail_bytes{fstype!~"tmpfs|overlay",mountpoint="/"}
/
node_filesystem_size_bytes{fstype!~"tmpfs|overlay",mountpoint="/"}
)) * 100 > 85
Set the evaluation interval to 1m, pending period to 5m (fires only if the condition holds for 5 minutes — avoids flapping), and attach a contact point.
Configuring a Webhook or Email Contact Point
Go to Alerting → Contact Points → Add contact point. Grafana supports email, Slack, PagerDuty, webhooks, Telegram, and more out of the box. For a simple webhook (works with n8n, Zapier, or any HTTP endpoint):
# Contact point config (set via UI or provisioning)
name: webhook-alerts
type: webhook
settings:
url: https://your-webhook-endpoint.com/alert
httpMethod: POST
# Optional: add auth header
authorization_scheme: Bearer
authorization_credentials: your-token-here
For email, add your SMTP config to Grafana's environment variables in Compose:
environment:
- GF_SMTP_ENABLED=true
- GF_SMTP_HOST=smtp.yourdomain.com:587
- [email protected]
- GF_SMTP_PASSWORD=your-smtp-password
- [email protected]
- GF_SMTP_FROM_NAME=Grafana Alerts
Tips, Gotchas, and Troubleshooting
Grafana Shows No Data After Import
Community dashboards sometimes use variable names or label filters that don't match your setup exactly. Check the dashboard variables (gear icon → Variables) and make sure the instance label matches what Prometheus is actually reporting:
# Check what labels Prometheus has for node metrics
curl -s http://localhost:9090/api/v1/labels | jq .data
# Check the actual instance value being reported
curl -s 'http://localhost:9090/api/v1/query?query=node_uname_info' | jq '.data.result[].metric'
Prometheus Targets Showing as Down
# Check the Prometheus targets page directly
curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health, lastError: .lastError}'
# Check if node-exporter is reachable from prometheus container
docker exec prometheus wget -qO- http://node-exporter:9100/metrics | head -5
# Check Prometheus config was loaded correctly
docker exec prometheus promtool check config /etc/prometheus/prometheus.yml
Grafana Dashboard Panels Show "No data"
First check the time range — new deployments have no historical data yet; set the time picker to Last 5 minutes and wait for Prometheus to scrape a few cycles. Then check the data source selection on the panel — it may be pointing to a data source that doesn't exist in your instance.
High Disk Usage from Prometheus
Prometheus stores all metric data locally. For a busy server, this can grow fast. Control it with the retention flag already in the Compose file (--storage.tsdb.retention.time=30d). You can also cap by size:
# Add to Prometheus command flags:
- '--storage.tsdb.retention.size=10GB'
# Check current Prometheus storage usage
docker exec prometheus du -sh /prometheus
Updating the Stack
cd ~/monitoring
docker compose pull
docker compose up -d
# Verify all services came back healthy
docker compose ps
Grafana and Prometheus data volumes persist across updates — all your dashboards, alerts, and metrics history survive.
Pro Tips
- Version-pin your images in production — use
grafana/grafana-oss:10.4.2instead of:latestso updates are intentional, not automatic on next deploy. - Export dashboards as JSON and commit them to Git — go to the dashboard settings → JSON Model, copy, and save. This makes your monitoring config reproducible and reviewable.
- Use dashboard folders and tags to keep things organized as you add more dashboards — infrastructure, app performance, business metrics, and alerts each deserve their own folder.
- Add application metrics early — most web frameworks have Prometheus client libraries (Python:
prometheus-client, Node.js:prom-client). Instrument your apps from day one and scrape them the same way as Node Exporter. - Grafana's Explore mode is your best friend for debugging — you can run raw PromQL queries, inspect individual time series, and prototype alert expressions without touching a dashboard.
Wrapping Up
When you self-host Grafana with Prometheus and the right exporters, you go from flying blind to having a complete, real-time picture of your infrastructure. CPU, memory, disk, network, per-container metrics, alerts when things go sideways — all running on your own server, with no data leaving your infrastructure and no per-seat SaaS bill.
Start with the stack in this guide, import the Node Exporter Full dashboard to confirm everything is wired up correctly, then build alert rules for the things that would actually wake you up at 3am. Once the foundation is solid, layer in application-level metrics from your services and you have a monitoring setup that genuinely earns its place in your stack.
Need Monitoring Built for a Production Platform?
If you're running a multi-server infrastructure and need a monitoring setup that covers your full stack — application metrics, uptime, log aggregation, and intelligent alerting — the sysbrix team can design and deploy it. We build observability stacks that surface real problems, not just pretty graphs.
Talk to Us →