Monitor a Host and Its Containers with node_exporter and cAdvisor
Run Prometheus, node_exporter, and cAdvisor together, scrape both, and confirm the targets are healthy.
Host-level metrics (CPU, memory, disk, network) and per-container metrics come from two different exporters. This guide runs Prometheus alongside both with Docker Compose, configures Prometheus to scrape them, and confirms it’s working before handing the data to a dashboard in the next guide.
The three pieces
- Prometheus itself: scrapes targets on a schedule and stores the results.
- node_exporter: exposes the host operating system’s own metrics, CPU, memory, disk, network, at
/metricson port 9100. - cAdvisor (Container Advisor, maintained by Google): exposes per-container resource usage, CPU, memory, I/O, at
/metricson port 8080.
Both exporters need visibility into things a normal container can’t see: node_exporter needs the host’s root filesystem and process namespace to report accurately on the host, not just its own container, and cAdvisor needs access to the container runtime’s own data to report on every container running on the host, not just itself.
Docker Compose for all three
version: "3"
volumes:
prometheus-data:
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=15d"
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
pid: host
volumes:
- /:/host:ro,rslave
command:
- "--path.rootfs=/host"
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
node-exporter’s pid: host and the read-only root mount are what let it report on the host rather than on its own isolated container view; --path.rootfs=/host tells it where that mounted root actually landed inside the container. cadvisor needs its own set of read-only mounts into the host’s runtime state for the same reason: without them, it can only see itself, not the other containers on the host. Nothing here needs write access to the host, every mount is read-only, which is worth keeping in mind if a security review asks what these containers can actually do.
Configure Prometheus to scrape both
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "cadvisor"
static_configs:
- targets: ["cadvisor:8080"]
global.scrape_interval isn’t Prometheus’s built-in default (that’s one minute); setting it explicitly to 15 seconds, as most getting-started examples do, is a convention worth following since it’s frequent enough to give responsive dashboards without generating excessive scrape volume. Each job_name is an arbitrary label you choose, attached to every series that job scrapes, which is how you’d later distinguish “this metric came from node-exporter” from “this metric came from cadvisor” in a query. Targets are addressed by their Compose service name (node-exporter, cadvisor) because containers on the same Compose network resolve each other by service name automatically; there’s no need for container IP addresses or localhost here.
Bring it up and confirm the targets are healthy
docker compose up -d
Open http://localhost:9090/targets in a browser. Each configured job should show as UP. If one doesn’t, check that container’s own logs first, and confirm you can reach its /metrics endpoint directly (curl http://localhost:9100/metrics from the host, or docker exec into the Prometheus container and curl the target’s Compose service name) before assuming the problem is in Prometheus’s configuration rather than the target itself.
From Prometheus’s own Graph page, try a quick sanity check:
up{job="node-exporter"}
up is a synthetic metric Prometheus generates for every target itself, 1 if the last scrape succeeded, 0 if it didn’t, which makes it a fast way to check target health directly through PromQL rather than the Targets page.
A note before you go further
Neither Prometheus’s UI nor cAdvisor’s ships with authentication or TLS by default. That’s a reasonable default for something meant to be scraped and queried from inside a trusted network, but it means these ports shouldn’t be exposed directly to the public internet without adding access control, whether that’s a reverse proxy with authentication in front of them, or simply keeping them reachable only from an internal network in the first place.
With both exporters reporting into Prometheus, the next guide covers turning this into an actual dashboard with Grafana.
Popular Guides
-
Prometheus Architecture and the Pull Model Why Prometheus scrapes instead of receiving, and how targets, exporters, and service discovery fit together. -
The Prometheus Data Model: Metrics, Labels, and Metric Types What a time series actually is in Prometheus, and the plain-text format targets expose them in. -
Get Started with PromQL: Selecting, Rating, and Aggregating Build up from a simple metric selector to a real query: per-path error rate, computed live.