The Prometheus + Grafana Stack: Metrics, Alerting, and Dashboards
Something is wrong with your server at 3am. Response times climbed for 45 minutes before the on-call page fired. Now you’re staring at a terminal and asking yourself: was it CPU? Memory pressure? A disk filling up? A container that kept OOM-crashing and restarting? Did it start before or after that deploy?
Without metrics, you are guessing. With Prometheus and Grafana, you scroll back in time, pull up the exact 45-minute window, and the answer is sitting right there in a graph — CPU saturation on one core, caused by a container that had no resource limits and decided to consume everything it could reach.
This guide builds that capability from scratch. You’ll get a complete Docker Compose stack, working configuration files, real PromQL queries, alerting rules that fire on the things that actually matter, and dashboards you can use immediately.
Part 1: Why Prometheus + Grafana?
The modern observability space organizes around three signals: metrics (numerical measurements over time), logs (timestamped event records), and traces (request journeys through distributed systems). Prometheus owns the metrics tier. It is the most widely deployed metrics system in existence, and for good reason.
Prometheus is a pull-based metrics collector and time-series database built at SoundCloud in 2012 and donated to the CNCF in 2016. Rather than having agents push data to a central server, Prometheus scrapes HTTP endpoints — called targets — on a configurable interval. Every target exposes its metrics in a plain-text format at /metrics. Prometheus fetches that page, parses it, and stores the values in its time-series database (TSDB) with nanosecond-precision timestamps.
The pull model has a subtle but important advantage: Prometheus knows when a target goes silent. If a scrape fails, that’s a data point — the target is down. Push-based systems have to infer absence, which is harder to alert on reliably.
PromQL (Prometheus Query Language) is the query language that turns raw time-series data into answers. It is a functional, expression-based language purpose-built for time-series math. You can compute rates over windows, aggregate across label dimensions, predict future values with linear regression, and correlate metrics from different sources. Once you understand PromQL, the data becomes genuinely useful.
Grafana is the visualization layer. It connects to Prometheus (and dozens of other data sources), renders panels and dashboards, and ships its own alerting engine. The combination of Prometheus as the data store and Grafana as the UI has become the default observability stack for Kubernetes clusters, homelabs, and production infrastructure alike.
The exporter ecosystem is the final piece. Because Prometheus scrapes a simple HTTP text format, anyone can write an exporter. Today there are battle-tested exporters for Linux hosts, Docker containers, PostgreSQL, MySQL, Redis, Nginx, HAProxy, Elasticsearch, SNMP devices, and hundreds of other systems. You do not need to instrument anything to get immediate value — install the exporter, point Prometheus at it, and you have metrics within seconds.
By the end of this guide you will be able to answer:
- Is my server overloaded right now, and has it been overloaded in the past week?
- Why did my application slow down at 3am on Tuesday?
- Which container is consuming the most CPU?
- When will this disk fill up, given the current growth rate?
- Are all my HTTP endpoints responding, and what is the error rate?
Part 2: Architecture Overview
Before touching configuration files, a clear picture of how the components connect will save you debugging time later.
┌─────────────────────────────────────────────────────────────────┐
│ Your Infrastructure │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Node Exporter│ │ cAdvisor │ │ Blackbox Exporter │ │
│ │ (host metrics│ │ (container │ │ (HTTP/TCP/ICMP probes│ │
│ │ :9100/metrics│ │ metrics) │ │ :9115/metrics) │ │
│ │ │ │ :8080/metrics│ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬────────────┘ │
│ │ │ │ │
└─────────┼─────────────────┼──────────────────────┼───────────────┘
│ scrape │ scrape │ scrape
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Prometheus Server (:9090) │
│ │
│ ┌───────────────┐ ┌─────────────┐ ┌───────────────────────┐ │
│ │ Scrape Engine│ │ TSDB Store │ │ Query Engine (PromQL)│ │
│ │ (pull-based) │ │ (on disk) │ │ │ │
│ └───────────────┘ └─────────────┘ └───────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Alerting Rules Evaluation → fires to Alertmanager │ │
│ └───────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────┘
│ query
▼
┌──────────────────────────────────┐
│ Grafana (:3000) │
│ Dashboards │ Panels │ Alerting │
└──────────────────────────────────┘
Prometheus ──alerts──▶ Alertmanager (:9093)
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Slack Email PagerDuty
Prometheus server is the core. It runs the scrape engine (fetching /metrics from targets at configurable intervals), the TSDB (a local time-series database that compresses and indexes metric data on disk), the PromQL query engine, and the alerting rule evaluator.
Exporters are the bridge between your systems and Prometheus. Each exporter runs as a sidecar or standalone process next to the thing it monitors, reads metrics from that system’s native interface, and presents them in Prometheus text format on an HTTP endpoint. Prometheus scrapes that endpoint. Exporters do not push — they wait to be scraped.
Alertmanager handles alert routing, deduplication, grouping, silencing, and notification delivery. Prometheus evaluates alerting rules and fires alerts at Alertmanager. Alertmanager decides who gets notified, when, and through which channel. This separation keeps alerting logic out of Prometheus.
Grafana connects to Prometheus as a datasource and renders dashboards. It also ships a built-in alerting engine (Grafana Unified Alerting) that can complement or replace Alertmanager for simpler setups.
Pushgateway is a short-lived intermediary for batch jobs and ephemeral workloads that do not live long enough to be scraped. A cron job pushes its completion metrics to the Pushgateway, and Prometheus scrapes the Pushgateway. For always-running services, you almost never need it.
Part 3: Installation with Docker Compose
The full stack runs cleanly in Docker Compose. This configuration includes Prometheus, Grafana, Node Exporter, cAdvisor, and Alertmanager with proper data persistence and a dedicated network.
|
|
Create the Grafana datasource auto-provisioning file so Prometheus is wired up automatically on first boot:
|
|
Bring the stack up with:
|
|
Prometheus will be available at http://localhost:9090, Grafana at http://localhost:3000.
Part 4: Prometheus Configuration
The prometheus.yml file controls everything: where to scrape, how often, which rules to evaluate, and where to send alerts.
|
|
Hot reload lets you apply config changes without restarting Prometheus. The --web.enable-lifecycle flag enables the reload endpoint:
|
|
File-based service discovery (file_sd_configs) is the practical choice for homelabs and small fleets. Drop a YAML file in the targets directory and Prometheus picks it up within 30 seconds, no restart needed:
|
|
Part 5: Key Exporters
Node Exporter
Node Exporter is the standard Linux host metrics exporter. It exposes CPU, memory, disk, network, filesystem, and dozens of other system metrics via /proc and /sys. The key metrics you will use constantly:
| Metric | Description |
|---|---|
node_cpu_seconds_total |
CPU time by mode (idle, user, system, iowait, etc.) |
node_memory_MemAvailable_bytes |
Available memory (kernel estimate) |
node_memory_MemTotal_bytes |
Total installed RAM |
node_filesystem_avail_bytes |
Filesystem space available to non-root users |
node_filesystem_size_bytes |
Total filesystem capacity |
node_network_receive_bytes_total |
Network bytes received by interface |
node_network_transmit_bytes_total |
Network bytes transmitted by interface |
node_load1, node_load5, node_load15 |
System load averages |
node_disk_read_bytes_total |
Disk bytes read by device |
cAdvisor
cAdvisor (Container Advisor) exposes resource usage metrics for every running Docker container. It reads from the Docker API and cgroups. Essential metrics:
| Metric | Description |
|---|---|
container_cpu_usage_seconds_total |
Cumulative CPU usage per container |
container_memory_usage_bytes |
Current memory usage |
container_memory_working_set_bytes |
Memory working set (more meaningful than usage) |
container_network_receive_bytes_total |
Container network receive |
container_fs_usage_bytes |
Container filesystem usage |
Blackbox Exporter
Blackbox Exporter probes external endpoints over HTTP, HTTPS, TCP, and ICMP. It answers: is this URL up? What is the response time? Is the TLS certificate about to expire?
|
|
Key Blackbox metrics:
probe_success— 1 if the probe succeeded, 0 if it failedprobe_duration_seconds— how long the probe tookprobe_http_status_code— the HTTP status code returnedprobe_ssl_earliest_cert_expiry— Unix timestamp when the TLS cert expires
Database Exporters
For common databases, drop-in exporters handle instrumentation:
- mysqld_exporter — MySQL/MariaDB: query throughput, connection pool, replication lag
- postgres_exporter — PostgreSQL: transaction rates, cache hit ratios, bloat, replication
- redis_exporter — Redis: commands per second, memory usage, keyspace hits/misses, replication
Application Instrumentation
For custom applications, Prometheus client libraries make instrumentation straightforward. A Go service exposing a counter and histogram:
|
|
Client libraries exist for Go, Python (prometheus_client), Java (simpleclient), Node.js (prom-client), Ruby, Rust, and many more. Register your metrics, instrument your code paths, and expose the default /metrics handler — that’s all Prometheus needs.
Part 6: PromQL Fundamentals
PromQL is the query language that makes Prometheus useful. It takes getting used to, but once it clicks, you can answer almost any infrastructure question from a terminal or dashboard.
Data Types
- Instant vector — a set of time series, each with a single sample at the current time:
node_memory_MemTotal_bytes - Range vector — a set of time series with a range of samples over a time window:
node_cpu_seconds_total[5m] - Scalar — a single floating point number
- String — a string literal (rarely used directly)
Label Matchers
|
|
Essential Functions
rate(v[d]) — per-second average rate of increase over duration d. Use this for counters on dashboards. Always use rate() over [5m] or longer; never over less than two scrape intervals.
irate(v[d]) — instantaneous rate, looking at only the last two data points. More responsive but spikier. Useful for alerting on sudden bursts.
increase(v[d]) — total increase in a counter over duration d. Good for “requests in the last hour”.
sum(v) by (label) — aggregate by summing across the given label dimension.
avg(v) by (label), max(v) by (label), min(v) by (label) — other aggregations.
predict_linear(v[d], t) — linear regression: given the trend over d, what will the value be in t seconds? Essential for disk capacity planning.
histogram_quantile(φ, v) — compute the φ-quantile (e.g., 0.95 for p95) from a histogram.
Common Patterns You Will Use Every Day
CPU usage percentage per host:
|
|
This takes the idle CPU rate, averages across all cores for each instance, converts to a percentage, then inverts to get usage.
Memory usage percentage:
|
|
Use MemAvailable rather than MemFree. Available accounts for reclaimable cache, which is what the kernel actually has to offer new processes.
Disk usage percentage for root filesystem:
|
|
Disk filling up prediction — how many hours until full:
|
|
This fires when the linear projection based on the last 6 hours of growth predicts the disk will be full within 24 hours.
Network receive rate in bytes/sec:
|
|
The label filter excludes loopback, veth (Docker internal), and bridge interfaces to show only physical/WAN traffic.
HTTP error rate (5xx):
|
|
Container CPU usage percentage:
|
|
HTTP probe uptime (Blackbox):
|
|
TLS certificate expiry in days:
|
|
p95 request latency from histogram:
|
|
Part 7: Recording Rules and Alerting Rules
Recording Rules
Some PromQL expressions are expensive — they fan out across many time series and take hundreds of milliseconds to evaluate. Running these on every dashboard refresh is wasteful. Recording rules pre-compute these expressions and store the result as a new metric that is cheap to query.
|
|
Dashboard panels referencing instance:node_cpu_utilization:rate5m will return instantly because Prometheus already has the answer computed.
Alerting Rules
Alert rules evaluate a PromQL expression on each evaluation interval. When the expression returns results, Prometheus puts the alert into pending state. If it remains pending for the duration specified in for, Prometheus fires the alert to Alertmanager.
|
|
|
|
Validate your rule files before applying them:
|
|
Part 8: Alertmanager Configuration
Alertmanager receives alerts from Prometheus and decides what to do with them. Its job is routing, deduplication, grouping, silencing, and delivery. The configuration is a tree of routes where each alert flows down until it matches a receiver.
|
|
Managing Alerts with amtool
amtool is the CLI for interacting with Alertmanager. Useful commands:
|
|
Part 9: Grafana Dashboards
First Login and Datasource Setup
Navigate to http://localhost:3000 and log in with the credentials you set in docker-compose.yml (admin / changeme). Change the admin password immediately.
If you used the datasource provisioning file from Part 3, Prometheus is already wired up. Otherwise, go to Configuration → Data Sources → Add data source, select Prometheus, set the URL to http://prometheus:9090 (the Docker service name), and click Save & Test.
Community Dashboards Worth Importing
Rather than building everything from scratch, import these battle-tested dashboards from the Grafana dashboard catalogue (grafana.com/grafana/dashboards):
| Dashboard | ID | What it shows |
|---|---|---|
| Node Exporter Full | 1860 | Complete host metrics: CPU, memory, disk, network, filesystem |
| cAdvisor | 14282 | Container resource usage with per-container breakdown |
| Blackbox Exporter | 7587 | Endpoint uptime, latency, TLS expiry |
| Prometheus Stats | 2 | Prometheus’s own performance and health |
| Alertmanager | 9578 | Alert routing and notification stats |
Import via Dashboards → Import → Enter dashboard ID, select your Prometheus datasource, and click Import.
Building a “Host Overview” Dashboard
To understand how Grafana works, build a simple host overview dashboard from scratch. Go to Dashboards → New Dashboard → Add visualization.
Dashboard Variables make dashboards reusable. Add a variable: Settings → Variables → New variable:
- Name:
instance - Type: Query
- Query:
label_values(node_uname_info, instance) - Multi-value: enabled
- Include All: enabled
Now every panel can use $instance in its query, and the dropdown at the top of the dashboard lets you switch between hosts.
CPU Usage Time Series Panel:
Query:
|
|
- Panel type: Time series
- Unit: Percent (0-100)
- Add threshold at 85 (warning yellow) and 95 (critical red)
Memory Usage Stat Panel:
Query:
|
|
- Panel type: Stat
- Unit: Percent (0-100)
- Color mode: Background, thresholds at 80 (yellow) and 90 (red)
Disk Usage Gauge Panel:
Query:
|
|
- Panel type: Gauge
- Min: 0, Max: 100
- Thresholds: 80 (yellow), 95 (red)
Network I/O Time Series Panel:
Query A (Receive):
|
|
Query B (Transmit, negated for the classic in/out display):
|
|
- Panel type: Time series
- Unit: bytes/sec
System Load Panel:
Query:
|
|
This normalizes load by CPU count so the alert threshold of 1.0 means “fully saturated”.
Dashboard Provisioning (GitOps for Dashboards)
Rather than clicking through the Grafana UI and hoping your dashboards survive a container rebuild, provision them from JSON files:
|
|
Export a dashboard as JSON from Dashboard Settings → JSON Model, save it to grafana/provisioning/dashboards/json/host-overview.json, and mount the provisioning directory into the Grafana container (already done in the docker-compose.yml above). Grafana picks up JSON files automatically. Now your dashboards live in git and survive container rebuilds.
Grafana Unified Alerting
Grafana ships its own alerting engine (Unified Alerting) that can query any datasource — not just Prometheus. For teams that want all alerting in one place:
- Go to Alerting → Contact Points → New contact point, configure Slack or email
- Go to Alerting → Notification policies, set the default policy to your contact point
- Go to Alerting → Alert rules → New alert rule, write a PromQL expression, set evaluation interval, and configure thresholds
Grafana Unified Alerting is convenient but less powerful than Prometheus alerting rules + Alertmanager for complex routing and inhibition. For homelab use, either works. For production multi-tenant setups, the Prometheus + Alertmanager path scales better.
Part 10: Long-Term Storage and Scaling
Local Storage Limits
By default, Prometheus stores data on local disk. With 15-second scrape intervals and a modest number of targets, you can expect roughly 1-3 GB per day. The --storage.tsdb.retention.time=30d flag keeps 30 days of data on disk.
The tradeoff: local storage is fast and simple, but it does not survive disk failures, it cannot be queried across multiple Prometheus instances, and 30 days of history is limiting for capacity planning.
Remote Write to Long-Term Storage
Prometheus supports remote_write — shipping samples to an external storage backend in real time while keeping local storage for fast recent queries. Add to prometheus.yml:
|
|
VictoriaMetrics is the most popular remote write target for homelabs and small teams. It is a drop-in Prometheus-compatible TSDB with 10x better compression than Prometheus, faster ingestion, and a compatible query API. A single-node VictoriaMetrics instance can handle hundreds of thousands of active time series on modest hardware.
Thanos is the production-grade option for multi-Prometheus setups. The Thanos sidecar attaches to each Prometheus instance, uploads blocks to object storage (S3, GCS, Azure Blob), and the Thanos Store component serves historical data back to Grafana with a unified query interface. This gives you unlimited retention at object storage prices and global querying across multiple Prometheus instances.
Grafana Mimir (formerly Cortex) is the cloud-native, horizontally scalable option — designed for thousands of tenants and billions of active series. Overkill for a homelab, but the architecture Grafana Cloud runs on.
Part 11: Tips and Best Practices
Cardinality: The Number One Footgun
Cardinality is the number of unique time series in Prometheus. Each unique combination of metric name + label set is one time series. High cardinality is the most common way to accidentally fill a Prometheus instance’s memory and crash it.
What causes high cardinality:
- Using user IDs, email addresses, or session tokens as label values
- Including URL paths with IDs in them (e.g.,
/api/user/12345) - Putting IP addresses or UUIDs in labels without aggregation
The rule: label values should have a bounded, small set of possible values. status_code is fine (200, 404, 500 — handful of values). user_id is never fine.
|
|
Check the top cardinality metrics in the Prometheus UI under Status → TSDB Status.
Scrape Interval vs Evaluation Interval
Your scrape_interval (how often Prometheus fetches metrics) and evaluation_interval (how often rules are evaluated) should be the same value or multiples of each other. The scrape interval determines your resolution. 15 seconds is a good default — low enough to catch short spikes, high enough to keep cardinality manageable.
For evaluation intervals, 15-30 seconds is standard. Setting for: 5m on an alert with a 15-second evaluation interval means the condition must hold for at least 20 consecutive evaluations.
Label Naming Conventions
Follow the Prometheus naming conventions to avoid confusion:
- Metric names:
snake_case, with the unit as a suffix (_bytes,_seconds,_total) - Always use base units — bytes not kilobytes, seconds not milliseconds
- Counters must end in
_total - Histograms automatically get
_bucket,_count, and_sumsuffixes
Validate Before Deploying
Always run promtool before applying changes:
|
|
Keep Dashboards in Git
Grafana dashboards are JSON. Export them, commit them, provision them via the filesystem. This means:
- Dashboard changes are reviewed like code (pull requests, diff)
- Dashboards survive container rebuilds and data loss
- You can reproduce the entire observability stack from git in minutes
The single biggest operational mistake in Grafana setups is treating dashboards as ephemeral UI artifacts. They are configuration. Treat them accordingly.
Putting It All Together
Start with the Docker Compose stack and Node Exporter. Within five minutes you will have CPU, memory, disk, and network graphs in Grafana. Import dashboard 1860 (Node Exporter Full) and you will have a comprehensive host overview immediately.
Add the alerting rules from Part 7. Run promtool check rules to validate them, hot-reload Prometheus, and watch the Alertmanager silence rules page for the next few days to tune the thresholds for your environment. What fires constantly in a quiet homelab will be signal in a production environment, and vice versa.
Add cAdvisor for container visibility. Import dashboard 14282. Now you can see which container is consuming the most CPU or memory at any moment.
Add Blackbox Exporter for uptime monitoring. Import dashboard 7587. Configure it to probe every public-facing URL. Add the TLS certificate expiry alert. You will never be surprised by an expired certificate again.
From there, the stack grows with your needs. Add database exporters as you deploy databases. Add remote_write to VictoriaMetrics when you want longer history. Add Thanos when you want to query across multiple sites.
The Prometheus + Grafana stack is genuinely one of the highest-leverage tools in infrastructure. A few hours of setup pays for itself the first time something breaks and you know exactly where to look.
For the logs side of the observability stack — shipping, querying, and correlating with metrics — see the upcoming post on Loki for Log Aggregation.
Comments