Docker & DevOps

Monitor Docker containers with Prometheus, cAdvisor and Grafana

Run cAdvisor, Prometheus and Grafana in Docker Compose on a Linux VPS and get per-container CPU, memory, network and disk graphs in one dashboard.

AdvancedLinux server6 min read

To monitor Docker containers with Prometheus, cAdvisor and Grafana you run three containers: cAdvisor reads container and host metrics from the kernel, Prometheus scrapes and stores them, and Grafana draws the graphs. This guide builds the whole stack with Docker Compose, wires up the scrape config and imports a dashboard you can read at a glance.

The result is per-container CPU, memory, network and filesystem usage with history, so you can tell whether a container is leaking memory or quietly eating disk before it takes the server down.

Before you start

You need:

  • A Linux VPS running Ubuntu 22.04/24.04, Debian 12 or AlmaLinux/Rocky 8–9, with root or sudo.
  • Docker Engine and the Compose plugin installed. If not, follow Install Docker and Docker Compose on a Linux VPS.
  • Roughly 1–2 GB of free RAM and a few GB of disk for the Prometheus time-series database. The stack itself is small; the database grows with retention.
  • Comfort editing YAML files.

Warning: cAdvisor runs with privileged: true and mounts parts of the host filesystem read-only. That is how it reads cgroup and disk statistics. Only run container images you trust, and keep the monitoring ports off the public internet.

Monitor Docker containers with Prometheus and cAdvisor

Create the project directory and the two subfolders you will mount into the containers:

Bash
sudo mkdir -p /opt/monitoring/prometheus
cd /opt/monitoring

Create /opt/monitoring/docker-compose.yml:

YAML
services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    privileged: true
    devices:
      - /dev/kmsg
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    ports:
      - "127.0.0.1:8080:8080"

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    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=15d
    ports:
      - "127.0.0.1:9090:9090"

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    environment:
      GF_SECURITY_ADMIN_PASSWORD: change-me-now
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "127.0.0.1:3000:3000"

volumes:
  prometheus-data:
  grafana-data:

A few things worth knowing:

  • The 127.0.0.1: prefix on each port mapping means the services are only reachable from the server itself. You will tunnel in over SSH or put a reverse proxy in front later.
  • /var/lib/docker mounted read-only is what lets cAdvisor map container IDs to names and images.
  • --storage.tsdb.retention.time=15d keeps 15 days of metrics. Raise or lower it to match your disk.
  • The latest tag is convenient but not reproducible. Once the stack works, pin the exact tags you tested and check the image pages on Docker Hub and the cAdvisor registry for current releases.

Start the stack:

Bash
docker compose up -d
docker compose ps

All three containers should report running.

Configure Prometheus to scrape cAdvisor

Prometheus reads its targets from /opt/monitoring/prometheus/prometheus.yml. Create it:

YAML
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]

  - job_name: cadvisor
    static_configs:
      - targets: ["cadvisor:8080"]

The target is cadvisor:8080, not localhost:8080. Inside the Compose network, containers reach each other by service name. Prometheus is scraping from inside its own container, so localhost would point at Prometheus itself.

Apply the config without a full restart:

Bash
docker compose exec prometheus \
  wget -qO- --post-data='' http://localhost:9090/-/reload

If that returns an error, the --web.enable-lifecycle flag is missing; add it to the Prometheus command list and run docker compose up -d prometheus instead.

Add Grafana and import a dashboard

Open Grafana through an SSH tunnel from your laptop:

Bash
ssh -L 3000:127.0.0.1:3000 root@203.0.113.10

Then browse to http://localhost:3000 and log in as admin with the password you set in GF_SECURITY_ADMIN_PASSWORD. Change it immediately if you left the placeholder in place.

Now connect the data source:

  1. Go to Connections → Data sources → Add data source.
  2. Choose Prometheus.
  3. Set the connection URL to http://prometheus:9090 — again, the Compose service name.
  4. Click Save & test. You should see "Successfully queried the Prometheus API".

Import a dashboard:

  1. Go to Dashboards → New → Import.
  2. Enter dashboard ID 193 (the long-standing cAdvisor / Docker monitoring dashboard) and click Load.
  3. Pick your Prometheus data source and click Import.

Dashboard IDs and their queries change over time. If a panel shows no data, search grafana.com/grafana/dashboards for a current cAdvisor dashboard and check which metric names it expects.

To sanity-check the data yourself, open Explore and run:

rate(container_cpu_usage_seconds_total{name!=""}[5m])

You should get one series per running container.

Check it works

Work through these on the server:

Bash
curl -s http://127.0.0.1:8080/metrics | head -n 20
curl -s http://127.0.0.1:9090/api/v1/targets | grep -o '"health":"[a-z]*"'

The first command should print cAdvisor's metric list. The second should show "health":"up" for both the prometheus and cadvisor jobs.

Then generate load so you have something to look at:

Bash
docker run --rm -d --name loadtest alpine \
  sh -c 'while true; do :; done'

Within a minute the Grafana dashboard should show a CPU spike attributed to loadtest. Remove it when you are done:

Bash
docker rm -f loadtest

Troubleshooting

  • cAdvisor restarts in a loop with "failed to start container manager" — it cannot read the host cgroups. Confirm privileged: true and the /sys:/sys:ro mount are present. On some hosts /dev/kmsg does not exist; remove that devices entry and recreate the container.
  • The cadvisor target shows down in Prometheus — the scrape URL is wrong. Use cadvisor:8080 inside Compose. If Prometheus runs outside Docker, use the host IP and make sure port 8080 is published on an address Prometheus can reach.
  • Grafana panels say "No data" — usually the time range or the data source. Check the query in Explore first; if it returns series there, the dashboard is pointing at a different data source or expects metric names your cAdvisor version does not emit.
  • Containers appear without names, or most are missing — cAdvisor reads /var/lib/docker. If Docker uses the containerd snapshotter or runs rootless, that path differs. Check your Docker storage driver and adjust the mount.
  • Grafana redirects to the wrong host or loops on login — it is behind a reverse proxy without knowing its public URL. Set GF_SERVER_ROOT_URL and GF_SERVER_DOMAIN in the Grafana environment block.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.