Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Containers Difficulty: Advanced

Monitoring with Grafana & Prometheus: your server in live dashboards

A complete monitoring stack of Prometheus, node-exporter, cAdvisor and Grafana behind Traefik – with live metrics for host and containers.

· 11 min read ·Duration: approx. 60 minutes
Table of contents

Uptime Kuma tells you whether a service is running. This stack tells you how it’s doing: CPU, RAM, disk, network and the load of each individual container – as live dashboards with history. We build the combination that has become the de facto standard in self-hosting: Prometheus collects, Grafana shows.

What are we building?

By the end, a complete monitoring stack runs behind your Traefik proxy, reachable at https://grafana.YOUR_DOMAIN with valid HTTPS. Four building blocks interlock:

  • Prometheus 3.13 – the time-series database. It pulls (scrapes) metrics from the other services at intervals and stores them with a timestamp.
  • node-exporter 1.12 – delivers the host metrics: CPU, memory, load, disks, network interfaces.
  • cAdvisor 0.55 – delivers the container metrics: CPU and RAM per running container, so you see which service is eating.
  • Grafana 13.1 – the interface: ready-made dashboards that turn the Prometheus data into graphs and displays.

The decisive difference from Uptime Kuma: instead of “green/red” you get trends – you recognize that the RAM has been slowly filling up for days, before the server swaps. Only Grafana is reachable from outside; Prometheus, node-exporter and cAdvisor get no public route.

How big does the server need to be?

The stack itself is frugal – in the test it occupied only a few hundred MB of RAM. The storage hunger comes from Prometheus’s retention time: the longer you keep metrics and the more services you monitor, the more space and RAM the time-series database needs. Because monitoring usually runs in addition to your actual apps, we recommend the VPS 2000 with more reserve – for a pure test setup the smaller one is enough too.
🍳 Recommendation Ad

VPS 2000 G12

8 vCores · 16 GB RAM · 512 GB NVMe

from €19.24/month

Enough RAM and storage to run monitoring alongside your apps.

Go to netcup →

💶 5 € voucher for new netcup customers: 36nc17844976032 (new customers only, no domains)

Prerequisites

Step by step

Step 1: Understand the architecture

Before we create files, the picture behind it – otherwise you debug blind. Prometheus works on the pull principle: the services don’t send their values somewhere, but Prometheus queries them actively. Every “exporter” provides its metrics under /metrics, Prometheus fetches them every 15 seconds and stores them in its time-series database. Grafana in turn queries Prometheus and draws the graphs from it.

Why pull instead of push at all? Because this way Prometheus always knows whether a target is alive: if an exporter doesn’t respond, that’s itself information (up = 0). You need no agents that actively “phone home”, and you can simply enter every new target into the configuration. An exporter is nothing magical – it’s a tiny web server that delivers a text list of current measurements under /metrics.

From this follows the security rule of this setup: node-exporter, cAdvisor and Prometheus have no authentication. So they must never stand open on the internet. Prometheus and cAdvisor we bind only to the internal Docker network monitoring; node-exporter runs in the host network (why is explained in step 4), but stays unreachable from outside thanks to UFW and the netcup firewall. Only Grafana is additionally on the proxy network and gets a Traefik route – the only service with a login and a public address.

Step 2: Create the project folder and configure Prometheus

Create the folder structure:

Terminal
mkdir -p ~/monitoring/prometheus ~/monitoring/grafana/provisioning/datasources
cd ~/monitoring

Create the Prometheus configuration. It defines whom Prometheus queries at what interval:

YAML
# prometheus/prometheus.yml
global:
  scrape_interval: 15s
  scrape_timeout: 10s

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

  - job_name: node
    static_configs:
      - targets: ["host.docker.internal:9100"]

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

Important: cadvisor:8080 appears as a service name – Docker resolves it automatically in the shared monitoring network. The node-exporter, on the other hand, runs in the host network (step 4 explains why) and is therefore addressed via host.docker.internal:9100 – Prometheus gets this name mapped to the Docker host’s IP via extra_hosts shortly. scrape_interval: 15s is a good compromise – often enough for meaningful curves, seldom enough to hardly create load.

Step 3: Provision the Grafana data source automatically

Instead of clicking the Prometheus data source into Grafana by hand later, we set it up via provisioning – as a file. That’s reproducible: after a rebuild, everything is immediately back, without clicking.

YAML
# grafana/provisioning/datasources/datasource.yml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false

access: proxy means that Grafana itself (server-side) queries Prometheus – the user’s browser never talks to Prometheus directly. That’s exactly why Prometheus can stay internal.

Step 4: The Compose stack

Now the heart. Create the compose.yaml. Replace grafana.YOUR_DOMAIN and the Grafana password:

YAML
services:
  prometheus:
    image: prom/prometheus:v3.13.1
    container_name: prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=30d"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prom_data:/prometheus
    extra_hosts:
      - "host.docker.internal:host-gateway"
    networks: [monitoring]
    restart: unless-stopped

  node-exporter:
    image: prom/node-exporter:v1.12.1
    container_name: node-exporter
    command:
      - "--path.rootfs=/host"
    network_mode: host
    pid: host
    volumes:
      - "/:/host:ro,rslave"
    restart: unless-stopped

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.55.1
    container_name: cadvisor
    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
    networks: [monitoring]
    restart: unless-stopped

  grafana:
    image: grafana/grafana:13.1.0
    container_name: grafana
    depends_on: [prometheus]
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: YOUR_GRAFANA_PASSWORD
      GF_SERVER_ROOT_URL: https://grafana.YOUR_DOMAIN
      GF_USERS_ALLOW_SIGN_UP: "false"
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.grafana.rule=Host(`grafana.YOUR_DOMAIN`)"
      - "traefik.http.routers.grafana.entrypoints=websecure"
      - "traefik.http.routers.grafana.tls.certresolver=le"
      - "traefik.http.services.grafana.loadbalancer.server.port=3000"
    networks: [monitoring, proxy]
    restart: unless-stopped

volumes:
  prom_data:
  grafana_data:

networks:
  monitoring:
  proxy:
    external: true

What matters here:

  • --storage.tsdb.retention.time=30d keeps metrics for 30 days. More = more meaning, but also more storage. 30 days is a good start.
  • node-exporter needs the host root directory (/:/host:ro,rslave), pid: host and network_mode: host to read real host values instead of container values. --path.rootfs=/host tells it where the host view is (read-only, :ro). The host network isn’t a nice-to-have: /proc/net is bound to the network namespace – in a normal bridge network, node-exporter would see only its own container interface (effectively just the scrape traffic), not the real host interfaces. The price: node-exporter thus listens on port 9100 of the host – your firewall keeps the port closed from outside, and Prometheus reaches it via the extra_hosts entry (host-gateway).
  • cAdvisor needs privileged: true, the device /dev/kmsg and read mounts on /sys and /var/lib/docker to detect all containers. That’s unavoidable for container monitoring – that’s why cAdvisor stays strictly internal.
  • Only grafana carries Traefik labels and is on the proxy network. The internal port is 3000. GF_SERVER_ROOT_URL must be the public HTTPS address, otherwise logins and redirects break behind the proxy.
  • GF_USERS_ALLOW_SIGN_UP: "false" prevents strangers from creating their own account. Set a strong admin password – Grafana is publicly accessible.

Step 5: Start and check the Prometheus targets

Start the complete stack:

Terminal
docker compose up -d

The first time, Docker downloads four images – that takes a moment. Check that all containers are running:

Terminal
docker compose ps

All four services (node-exporter, cadvisor, prometheus, grafana) should be running. Now the decisive test: Does Prometheus see its targets? Since Prometheus stays internal, we query it from a short-lived container that’s on the same network (Compose calls the network monitoring_monitoring – project folder plus network name):

Terminal
docker run --rm --network monitoring_monitoring curlimages/curl:8.21.0 \
  -s http://prometheus:9090/api/v1/query?query=up

In the JSON response there’s a "value" with "1" for each target – that means “reachable”. In the test all three jobs (prometheus, node, cadvisor) returned a 1. A 0 means Prometheus can’t scrape the target – then the section “When things go wrong” helps.

Step 6: Open Grafana and import dashboards

Open https://grafana.YOUR_DOMAIN. Traefik fetches the certificate on the first access (a few seconds). Log in with admin and your password from the compose.yaml. Thanks to provisioning, the Prometheus data source is already connected – you find it under Connections → Data sources:

The provisioned Prometheus data source in Grafana with the internal URL http://prometheus:9090
The data source is provisioned via file – Grafana reports it as preconfigured

Now the dashboards. Instead of building panels ourselves, we import two proven ones from the Grafana community. Go to Dashboards → New → Import, enter the ID and click Load:

The Grafana import dialog loads the “Node Exporter Full” dashboard from Grafana.com
Import a dashboard by ID – here \"Node Exporter Full\" (ID 1860)

Import these two:

  • 1860 – “Node Exporter Full”: the comprehensive host dashboard.
  • 19792 – “cAdvisor Dashboard”: metrics per container.

On import, select the Prometheus data source each time. After that, “Node Exporter Full” shows your host in real time – CPU usage, used RAM, disk, network, uptime:

The Grafana dashboard “Node Exporter Full” shows live values of the server: CPU 1 percent, RAM 14.7 percent used, 4 CPU cores, 8 GB RAM, uptime 2 days
Node Exporter Full with real live data from the test server

The cAdvisor dashboard breaks the same load down to individual containers – here you immediately see which service draws how much CPU and RAM:

The cAdvisor dashboard shows CPU consumption per container – each color is a container like grafana, prometheus or traefik
cAdvisor: CPU and memory consumption per container, broken down by name

Provision dashboards too

Like the data source, dashboards can also be provisioned via file (under grafana/provisioning/dashboards/ a .yml plus the dashboard JSONs). For getting started, the import via ID is faster; once your setup is in place, provisioning is worth it so all dashboards are automatically back after a rebuild.

Step 7: Read the most important metrics correctly

A dashboard full of curves is of little use if you don’t know what to watch for. These five values in the “Node Exporter Full” dashboard tell you almost everything about the health of your server:

  • CPU Busy. Short spikes are normal. It gets critical when the usage stays permanently high – then the server is the bottleneck.
  • Sys Load. The load should on average be below the number of your CPU cores (in the test: 4 cores → load permanently above 4 is a warning sign of overload).
  • RAM Used. Important: Linux uses free memory as a file cache – so “used” isn’t the same as “tight”. Only when little stays free besides the cache and SWAP Used rises does the server get sluggish. It’s exactly this creeping rise you want to see early.
  • Root FS Used. The classic among server outages is the full disk. The trend shows you the fill rate – so you recognize days in advance when it’s getting tight.
  • Network Traffic. Unusual spikes even though nothing is running can indicate a backup, an update or – in the bad case – unwanted traffic.

In the cAdvisor dashboard the most exciting question is: Which container draws the load? If your server suddenly gets slow, you see here at a glance whether it’s due to a single app – and don’t have to guess. This turns “something is slow” into a targeted “the photo import in container X is eating the CPU right now”.

When things go wrong

A target is at up = 0 or “DOWN” in Prometheus. Prometheus can’t reach the exporter. For cadvisor, check that it’s on the same monitoring network and the name and port are exactly right (cadvisor:8080). For the node job, check the extra_hosts entry on the Prometheus service and the target host.docker.internal:9100 – without both, Prometheus doesn’t find the node-exporter in the host network. After changes to the prometheus.yml, restart the Prometheus container: docker compose restart prometheus.

The cAdvisor dashboard partly shows “No data” or cAdvisor won’t start. If the mounts or privileged: true are missing, cAdvisor doesn’t see the containers. Check the cadvisor block (mounts, devices: /dev/kmsg). Individual “No data” panels are normal – some metrics (like CPU throttling) only exist if you’ve set CPU limits on the containers.

Grafana doesn’t load correctly behind Traefik – login fails or the layout is broken. Almost always GF_SERVER_ROOT_URL is wrong. It must be exactly the public address (https://grafana.YOUR_DOMAIN). Also check that the Traefik label loadbalancer.server.port=3000 is set – Grafana listens on 3000 internally.

Grafana shows “No data” in the panels, even though the targets are up. Usually the time range. A fresh stack has no history yet – set “Last 15 minutes” at the top right and wait a few minutes for data to accumulate. Also check that the Prometheus data source was selected on dashboard import.

The disk space on the server grows steadily. That’s the Prometheus TSDB. Reduce --storage.tsdb.retention.time (e.g. to 15d) or monitor fewer targets. The prom_data volume grows with the number of metrics × retention time.

Maintenance & backups

  • What you must back up. Important is the grafana_data volume: it contains your users, settings and self-built dashboards. Back it up encrypted and off-site with Restic. The Prometheus metrics (prom_data) are pure measurements – a loss is bearable, a backup optional.
  • Updates. The image tags are pinned (prometheus:v3.13.1, grafana:13.1.0 …). To update, raise the tags, then docker compose pull && docker compose up -d. Read the release notes first – Grafana in particular occasionally changes defaults between major versions. You get cAdvisor from gcr.io; there v0.55.1 is currently the latest available image version.
  • Security. Grafana is publicly accessible – assign a strong admin password and leave GF_USERS_ALLOW_SIGN_UP at false. Prometheus and cAdvisor stay internal; don’t expose them “just quickly” for debugging, they have no authentication.
  • The next step: alerts. A dashboard only helps if you look at it. Grafana can trigger alerts itself at thresholds (RAM > 90%, disk almost full) and report e.g. via push. This turns the pretty dashboard into an early-warning system – a good connection to your existing Uptime Kuma.
  • Honest about the effort: the stack runs largely on its own after setup. Occasionally plan a look at the retention time and storage usage, and bump the versions roughly quarterly.

You might also like