Prometheus Basics - Pull-Based Metrics Collection
Status: Active
Last Updated: 2026-08-26
Category: Observability - Phase 2: Metrics Collection
Prerequisites: netdata-basics, uptime-kuma-setup
Time: 2-3 hours
Tags: prometheus, metrics, promql, node-exporter, cadvisor, docker-compose, time-series, monitoring
Summary
Prometheus is the de facto standard for time-series metrics collection: a single Go binary that pulls (scrapes) metrics from HTTP endpoints every 15-60 seconds, stores them in a local time-series database, and answers queries with PromQL. This article installs Prometheus in Docker Compose alongside Node Exporter (host metrics) and cAdvisor (container metrics), walks through prometheus.yml scrape configuration line by line, and teaches the PromQL you'll actually use daily โ rate(), up, memory queries โ plus retention tuning.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Explain the pull model and why Prometheus chose it
- โ Run a full metrics stack with one Docker Compose file
- โ
Write and debug
prometheus.ymlscrape configs - โ Collect host metrics with Node Exporter
- โ Collect Docker container metrics with cAdvisor
- โ
Query with PromQL:
up,rate(), memory/disk/CPU queries - โ Tune data retention to fit your disk
Table of Contents
- The Pull Model
- Architecture Overview
- Docker Compose Installation
- Understanding prometheus.yml
- Node Exporter - Host Metrics
- cAdvisor - Container Metrics
- PromQL Basics
- Retention Configuration
- Troubleshooting & Common Pitfalls
- Sources & Related
The Pull Model
Every monitoring system must answer: who initiates โ the server collecting, or the thing being measured?
| Model | How it works | Consequence |
|---|---|---|
| Push | Agents send metrics to the server | Server can't tell "down" from "quiet"; agents need config/credentials |
| Pull (Prometheus) | Server fetches /metrics HTTP endpoints |
Dead-simple health check: no response = target down; targets expose plain HTTP |
In Prometheus's pull model, anything that serves a plain-text metrics page at /metrics is a valid target:
$ curl http://localhost:9100/metrics | head -4
# HELP node_cpu_seconds_total Seconds the CPUs spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="0",mode="idle"} 123456.78
node_cpu_seconds_total{cpu="0",mode="user"} 9876.54
No SDKs, no daemons required โ any app that can print text on an HTTP port is monitorable. This is why thousands of exporters exist.
๐ When pushes make sense: short-lived batch jobs that die before a scrape can happen use the Pushgateway; event-driven systems use remote write. Those are exceptions โ 95% of setups are pure pull.
Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ Docker host โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ โโโโโโโโโโโโโโ scrapes /metrics โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Prometheus โ โโโโโโโโโโโโโโโโโโโโบโ Node Exporter :9100 โ host โ
โ โ :9090 โ โโโโโโโโโโโโโโโโโโโโบโ cAdvisor :8080 โ contnr โ
โ โ โ โโโโโโโโโโโโโโโโโโโโบโ Netdata :19999 โ (opt.) โ
โ โโโโโโโฌโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ PromQL โ
โ โโโโโโโผโโโโโโโ โ
โ โ Grafana โ โ Phase 3 ([grafana-dashboards](grafana-dashboards)) โ
โ โโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Each component has one job: exporters expose, Prometheus collect and store (and query), Grafana visualize. Alerting comes later via Alertmanager or Grafana alerting โ see simple-alerts.
Docker Compose Installation
Create a project directory with three files:
mkdir -p ~/apps/prometheus && cd ~/apps/prometheus
docker-compose.yml
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "127.0.0.1:9090:9090" # localhost only โ proxy/tunnel for UI
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=90d
- --web.enable-lifecycle # allows POST /-/reload without restart
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./data:/prometheus # TSDB survives rebuilds
depends_on:
- node-exporter
- cadvisor
node-exporter:
image: quay.io/prometheus/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
pid: host # see all host processes
ports:
- "127.0.0.1:9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro # filesystem stats need root mountpoint
command:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/rootfs
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
privileged: true
devices:
- /dev/kmsg # kernel log access for OOM events
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
ports:
- "127.0.0.1:8080:8080"
Notes:
- All three bind mounts (
/proc,/sys,/) follow the same pattern as the Netdata Docker install in netdata-basics: show the exporter the host's view, not the container's. - Ports bind to
127.0.0.1so nothing metric-bearing is internet-facing. Access via SSH tunnel or reverse proxy.
prometheus.yml (minimal starter)
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "cadvisor"
static_configs:
- targets: ["cadvisor:8080"]
Targets use service names, not localhost โ containers resolve each other through the Compose network (kb/containers/docker-networking).
Start and verify
mkdir -p data && sudo chown 65534:65534 data # prometheus runs as nobody
docker compose up -d
# Validate config inside the container (catches YAML errors early):
docker exec prometheus promtool check config /etc/prometheus/prometheus.yml
# Confirm targets are UP:
curl -s http://127.0.0.1:9090/api/v1/targets | grep -o '"health":"[a-z]*"'
# expect: {"health":"up"} x3
Open http://<server>:9090 โ Status โธ Targets: all three should show green UP. The built-in graph page (Graph tab) lets you test queries before Grafana ever enters the picture.
Understanding prometheus.yml
Every scrape job follows this shape:
scrape_configs:
- job_name: "my-app" # becomes the `job` label on every sample
scrape_interval: 30s # optional override of global 15s
metrics_path: /metrics # default; change for nonstandard apps
scheme: http # https if the exporter has TLS
static_configs:
- targets:
- "app1.example.com:9100"
- "app2.example.com:9100"
labels: # extra labels attach to all samples here
env: prod
region: eu-west
Key rules learned the hard way:
- YAML is indentation-sensitive โ run
promtool check configafter every edit. - Reload without downtime:
curl -X POST http://localhost:9090/-/reload(needs--web.enable-lifecycle). Config errors leave the old config running. - Labels are your query dimensions. Everything a label distinguishes is stored as a separate time series โ don't put unbounded values (user IDs, request paths) into labels, or cardinality explodes your disk.
job+instanceare automatic labels:instancedefaults tohost:portof the target.
Node Exporter - Host Metrics
Node Exporter exposes ~2,000 host-level metrics: CPU time per core/mode, memory breakdown, disk I/O, filesystem usage, network interfaces, load average, systemd units. It's the same data Netdata charts in real time โ but now it's queryable history.
Useful sanity checks once scraped:
# CPU utilization (all cores averaged) โ see PromQL section for derivation
100 * (1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])))
# Root filesystem % used
100 * (1 - node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs|overlay"}
/ node_filesystem_size_bytes{mountpoint="/",fstype!~"tmpfs|overlay"})
# Available memory in GB
node_memory_MemAvailable_bytes / 1024^3
โ ๏ธ Run one Node Exporter per host (bare metal install or container with the mounts shown above). A single instance cannot see other machines'
/proc.
For bare-metal installs, systemd handles it:
# quick manual run for testing
./node_exporter --web.listen-address=127.0.0.1:9100
# then add the host to prometheus.yml targets: ["host1.example.com:9100"]
cAdvisor - Container Metrics
cAdvisor (Google's Container Advisor) auto-discovers every Docker/containerd container and exports per-container CPU, memory, network, and filesystem stats under the container_* metric family. No per-container registration needed โ new containers appear as targets' series automatically within one scrape cycle.
The most useful queries:
# Memory used per container, sorted
topk(10, container_memory_usage_bytes{name!=""} )
# Container memory vs its limit (% of cgroup limit)
100 * container_memory_usage_bytes{name!=""}
/ on(container) container_spec_memory_limit_bytes
# CPU usage normalized to one core (cgroup accounting is cumulative-nanoseconds)
rate(container_cpu_usage_seconds_total{name!=""}[5m])
# Network receive rate per container
rate(container_network_receive_bytes_total{name!=""}[5m])
Filtering tricks worth memorizing:
{name!=""}drops the ~hundreds of aggregate/system cgroup rows you don't want.{image=~".*postgres.*"}matches by image name.container_memory_cacheis page cache โ exclude it when comparing against limits, or healthy containers look like OOM candidates.
PromQL Basics
PromQL reads like SQL filtered through vector algebra. Four concepts cover 90% of daily use:
1. Instant vectors โ "what is X right now"
up # 1 = scraping OK, 0 = target down
node_load1 # 1-minute load average
node_memory_MemAvailable_bytes # bytes available now
Every result carries its labels: up{job="node", instance="node-exporter:9100"} = 1.
2. Range selectors + rate() โ "how fast is X changing"
Counters only go up (bytes sent since boot), so raw values are useless โ take the per-second derivative over a window:
rate(node_network_receive_bytes_total[5m]) # bytes/sec inbound
rate(http_requests_total[5m]) # requests/sec
irate(...[5m]) # last-two-points slope, spikier
increase(node_cpu_seconds_total{mode="user"}[1h]) # total increase over window
Rule of thumb: window โฅ 4ร scrape interval (5m for 15 s scrapes) so rate() can absorb missed scrapes gracefully.
3. Aggregation โ reshape with sum/avg/by
sum(rate(container_cpu_usage_seconds_total{name!=""}[5m])) by (name)
avg by (mode) (
rate(node_cpu_seconds_total[5m])
) * 100
4. Arithmetic between series โ ratios and percentages
# Disk usage % โ match series on device/fstree labels
100 * (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes)
Copy-paste starter kit
# Is anything down?
up == 0
# CPU % busy (host-wide)
100 * (1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])))
# RAM used %
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
# Disk write throughput per device
rate(node_disk_written_bytes_total[5m])
# Top 5 memory-hungry containers
topk(5, container_memory_usage_bytes{name!=""})
# Network throughput (bits/sec) summed across interfaces
sum(rate(node_network_receive_bytes_total{device!~"lo|veth.*"}[5m])) * 8
# Predict root-fs full date (days until disk fills at current trend,
# minimum 1h of history needed)
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 7*24*3600) < 0
Test everything on the Prometheus Graph tab first; panels fail loudly there but quietly in dashboards.
Retention Configuration
Default retention is 15 days โ fine for demos, wasteful for capacity planning. Set it explicitly via flags (already in the Compose file above):
command:
- --storage.tsdb.retention.time=90d # age-based (preferred)
# --storage.tsdb.retention.size=50GB # size-based cap (alternative/complement)
Sizing guidance:
| Resolution | Approximate cost |
|---|---|
| ~2,000 active series @ 15 s | ~2โ5 GB per month |
Rough math: count your series (curl -s http://localhost:9090/api/v1/status/tsdb | jq '.data.headStats.numSeries'), assume a few KB per series-day, and pick a number your disk tolerates. For years-long history, add Thanos/Mimir/VictoriaMetrics remote storage โ beyond Phase 2 scope.
Changing retention requires a container recreate (docker compose up -d after editing), not just a reload โ storage flags are startup-only.
Troubleshooting & Common Pitfalls
Target shows DOWN
- Wrong address space: using
localhost:9100inside the Prometheus container points at itself. Use service names on the shared Compose network. - Exporter bound to
127.0.0.1on a remote host? Prometheus can't reach it โ bind to0.0.0.0behind a firewall, or run exporters on the same host as Prometheus. - Check the error string on the Targets page: connection refused = wrong port; timeout = firewall; 404 = wrong
metrics_path.
Charts flatline or show huge spikes after gaps
- Window too small relative to scrape interval โ
rate()sees too few points. Use[5m]with 15 s scrapes. - Counters reset on container restart create one fake spike;
rate()handles resets,irate()amplifies them.
Empty cAdvisor results
- Missing volume mounts (
/,/var/run,/sys) silently produce emptycontainer_*families on some hosts. Compare against the Compose block above. - On cgroup-v2 hosts, older cAdvisor images misreport โ pin a recent image tag instead of trusting a stale local cache.
{name=""}rows dominate results: filter with{name!=""}.
Disk filling faster than expected
- Someone added high-cardinality labels (per-request IDs). Inspect: Status โธ TSDB โธ Label pairs with highest metric names cardinality.
- Lower
retention.sizeas a guardrail while you fix the source.
Config edits do nothing
You restarted the browser, not Prometheus. Either docker compose restart prometheus or better: curl -X POST http://localhost:9090/-/reload after validating with promtool.
Permission denied on ./data
The container writes as uid 65534 (nobody): sudo chown -R 65534:65534 ./data. Symptom: crash loop with "open /prometheus/lock: permission denied".
Sources & Related
Web sources consulted during research (2026-08-26):
- Prometheus installation docs (Docker image, flags): https://prometheus.io/docs/prometheus/latest/installation/
- Official guide โ Monitoring Docker container metrics with cAdvisor (Compose example): https://prometheus.io/docs/guides/cadvisor/
- Official guide โ Monitoring Linux host metrics with Node Exporter: https://prometheus.io/docs/guides/node-exporter/
- Prometheus configuration reference: https://prometheus.io/docs/prometheus/latest/configuration/configuration/
Related KB lessons:
- Previous phase: netdata-basics โ real-time dashboards; scrape
/api/v1/allmetrics?format=prometheusinto this stack for best of both - Next lesson: grafana-dashboards โ visualize everything collected here
- Alert routing: simple-alerts
- Compose fundamentals: containers/docker-compose-patterns
- Volume-mount pattern explanation: containers/docker-volumes
Change Log
- 2026-08-26: Initial draft created via headless-browser web research session.