Skip to main content

Stack de monitoring homelab — Prometheus + Grafana + Cloudflare Tunnel

Contexte et besoin

Gérer plusieurs serveurs — Pi local, serveur NAS, VPS chez différents hébergeurs — sans supervision centralisée revient à naviguer à l'aveugle. Disque plein, service tombé, pic CPU : on l'apprend par l'incident, pas avant.

Les besoins concrets :

  • Visualiser CPU, RAM, disque et uptime de chaque machine en un coup d'œil
  • Être alerté avant saturation plutôt qu'après
  • Accéder au dashboard depuis n'importe où, sans VPN
  • Ne pas exposer de ports entrants sur le routeur domestique

Solution retenue

Besoin Outil
Collecte des métriques sur chaque serveur Node Exporter (agent léger, port 9100)
Agrégation et stockage time-series Prometheus (scrape toutes les 15s)
Visualisation et dashboards Grafana
Exposition publique sécurisée sans port ouvert Cloudflare Tunnel + Cloudflare Access

Tout tourne sur un Raspberry Pi via Docker Compose. Les serveurs distants sont scrapés directement ou via tunnel SSH selon les contraintes réseau de chaque hébergeur.


Architecture

                          INTERNET
                              │
                 ┌────────────▼────────────┐
                 │     Cloudflare Access    │
                 │  (auth avant d'arriver)  │
                 └────────────┬────────────┘
                              │ HTTPS
                 ┌────────────▼────────────┐
                 │       Raspberry Pi       │
                 │                          │
                 │  cloudflared ──────────► Cloudflare
                 │       │                  │
                 │  ┌────▼────┐             │
                 │  │ Grafana │:3000        │
                 │  └────┬────┘             │
                 │  ┌────▼──────┐           │
                 │  │Prometheus │:9090      │
                 │  └────┬──────┘           │
                 │  ┌────▼──────┐           │
                 │  │node-exp.  │:9100      │
                 └──────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              │               │               │
       ┌──────▼─────┐  ┌──────▼─────┐  ┌─────▼──────┐
       │Serveur local│  │  VPS :9100 │  │VPS SSH only│
       │   :9100    │  │ou proxy :80│  │ SSH tunnel │
       └────────────┘  └────────────┘  └────────────┘

Rôles :

  • Node Exporter — agent sur chaque serveur, expose CPU/RAM/disque/réseau sur :9100/metrics
  • Prometheus — scrape les métriques toutes les 15s et les stocke (time-series DB)
  • Grafana — visualise les données de Prometheus via dashboards
  • Cloudflare Tunnel — expose Grafana publiquement sans ouvrir de port entrant sur le routeur

1. Node Exporter — installation sur chaque serveur

Avec sudo (Debian/Ubuntu)

sudo apt-get install -y prometheus-node-exporter
sudo systemctl enable --now prometheus-node-exporter

Écoute sur 0.0.0.0:9100 par défaut. Vérification : curl http://localhost:9100/metrics | head -5

Sans sudo (service utilisateur)

# Téléchargement
VERSION=1.8.2
ARCH=$(dpkg --print-architecture)  # amd64 ou arm64
wget -q https://github.com/prometheus/node_exporter/releases/download/v${VERSION}/node_exporter-${VERSION}.linux-${ARCH}.tar.gz -O /tmp/ne.tar.gz
tar xzf /tmp/ne.tar.gz -C /tmp
mkdir -p ~/.local/bin
cp /tmp/node_exporter-${VERSION}.linux-${ARCH}/node_exporter ~/.local/bin/

# Service systemd utilisateur
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/node-exporter.service << 'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
ExecStart=%h/.local/bin/node_exporter --web.listen-address=0.0.0.0:9100
Restart=always

[Install]
WantedBy=default.target
EOF

loginctl enable-linger $USER   # démarre au boot sans session active
systemctl --user daemon-reload
systemctl --user enable --now node-exporter

2. Stack monitoring sur le Pi — Docker Compose

Arborescence

~/monitoring/
├── docker-compose.yml
├── prometheus.yml
├── tunnel-entrypoint.sh      # si tunnel SSH nécessaire
└── provisioning/
    └── datasources/
        └── prometheus.yml

docker-compose.yml

services:

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    networks:
      - monitoring_default
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
    restart: unless-stopped

  node-exporter-pi:
    image: prom/node-exporter:latest
    container_name: node-exporter-pi
    networks:
      - monitoring_default
    pid: host
    volumes:
      - /:/host:ro,rslave
    command:
      - '--path.rootfs=/host'
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    networks:
      - monitoring_default
      - portainer_default    # si cloudflared est dans ce réseau
    volumes:
      - grafana_data:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning
    environment:
      - GF_SERVER_HTTP_PORT=3000
      - GF_SERVER_ROOT_URL=https://grafana.mondomaine.com
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SECURITY_COOKIE_SECURE=true
      - GF_SECURITY_STRICT_TRANSPORT_SECURITY=true
    restart: unless-stopped

networks:
  monitoring_default:
    driver: bridge
  portainer_default:
    external: true

volumes:
  prometheus_data:
  grafana_data:

Note réseau : Grafana doit être dans le même réseau Docker que cloudflared pour que le tunnel puisse l'atteindre via http://grafana:3000.

prometheus.yml

global:
  scrape_interval: 15s

scrape_configs:

  - job_name: 'pi-home'
    static_configs:
      - targets: ['node-exporter-pi:9100']   # container dans le même réseau Docker

  - job_name: 'serveur-local'
    static_configs:
      - targets: ['192.168.1.X:9100']         # réseau local, port direct

  - job_name: 'vps-standard'
    static_configs:
      - targets: ['IP_VPS:9100']              # VPS avec port 9100 accessible

  - job_name: 'vps-proxy'
    metrics_path: /metrics
    static_configs:
      - targets: ['IP_VPS:80']               # VPS avec proxy nginx sur port 80

  - job_name: 'vps-tunnel'
    static_configs:
      - targets: ['tunnel-container:9100']   # VPS via tunnel SSH

provisioning/datasources/prometheus.yml

Grafana charge automatiquement cette datasource au démarrage — pas besoin de configurer manuellement dans l'UI.

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090   # nom du container, même réseau Docker
    isDefault: true
    editable: true

Démarrage

cd ~/monitoring
docker compose up -d
docker compose ps   # vérifier que tout est "Up"

3. Firewall — trois cas de figure

Cas 1 — Port 9100 accessible

Ouvrir uniquement depuis l'IP du Pi (ne jamais ouvrir à 0.0.0.0) :

sudo ufw allow from IP_PI to any port 9100 proto tcp comment 'Prometheus'
sudo ufw reload

Diagnostic depuis le Pi :

nc -zv -w 5 IP_VPS 9100
# "succeeded"     → OK
# "timed out"     → bloqué par le provider (cas 2)
# "refused"       → port fermé côté serveur

Cas 2 — Port 9100 bloqué par le provider, mais 80/443 accessible

Proxy Nginx sur le VPS qui relaie /metrics vers node-exporter local :

# /etc/nginx/sites-available/node-exporter
server {
    listen 80;
    server_name IP_VPS;

    location /metrics {
        allow IP_PI;
        deny all;
        proxy_pass http://127.0.0.1:9100/metrics;
    }

    location / { return 404; }
}
sudo ln -s /etc/nginx/sites-available/node-exporter /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Dans prometheus.yml : targets: ['IP_VPS:80'] + metrics_path: /metrics

Cas 3 — Seul le port 22 (SSH) est accessible

Tunnel SSH dans un container Docker dédié. Le container crée un tunnel persistant et expose le port 9100 du VPS dans le réseau Docker du Pi. Prometheus scrape le container comme s'il était local.

Pi                              VPS distant
tunnel-container:9100 ──SSH──► localhost:9100
        ▲
Prometheus scrape

tunnel-entrypoint.sh :

#!/bin/sh
apk add --no-cache openssh-client > /dev/null 2>&1
while true; do
  ssh \
    -o StrictHostKeyChecking=no \
    -o ServerAliveInterval=30 \
    -o ExitOnForwardFailure=yes \
    -i /root/.ssh/id_rsa \
    -N -L 0.0.0.0:9100:localhost:9100 \
    user@IP_VPS
  echo "Tunnel coupé, reconnexion dans 5s..."
  sleep 5
done

Service dans docker-compose.yml :

  tunnel-vps:
    image: alpine:latest
    container_name: tunnel-vps
    networks:
      - monitoring_default
    volumes:
      - /home/user/.ssh/id_rsa:/root/.ssh/id_rsa:ro
      - ./tunnel-entrypoint.sh:/entrypoint.sh:ro
    entrypoint: ["/entrypoint.sh"]
    restart: unless-stopped

La clé SSH doit être copiée sur le Pi au préalable et avoir les bons droits :

chmod 600 ~/.ssh/id_rsa

4. Cloudflare Tunnel — exposition sécurisée

Cloudflare Tunnel crée une connexion sortante chiffrée du Pi vers Cloudflare. Aucun port entrant n'est ouvert sur le routeur. Le trafic entre par Cloudflare et ressort vers Grafana via le tunnel.

Déploiement cloudflared

docker run -d \
  --name cloudflared \
  --restart unless-stopped \
  --network portainer_default \   # même réseau que Grafana
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run --token TOKEN

Le token se génère dans Cloudflare Zero Trust → Networks → Tunnels → Create a tunnel.

Configuration du hostname public

Zero Trust → Networks → Tunnels → votre tunnel → Public Hostnames → Add :

Champ Valeur
Subdomain grafana
Domain mondomaine.com
Service type HTTP
URL grafana:3000 (nom container)

Piège fréquent : Si cloudflared et Grafana ne sont pas dans le même réseau Docker, utiliser le nom de container ne fonctionnera pas. Vérifier avec docker network inspect NOM_RESEAU.

Cloudflare Access — authentification avant Grafana

Sans Access, l'URL est publique. Access ajoute une page d'auth Cloudflare devant l'application.

Zero Trust → Access → Applications → Add → Self-hosted :

Application domain : grafana.mondomaine.com
Policy             : Allow
Condition          : Emails → liste d'emails autorisés
MFA                : Recommandé (TOTP ou hardware key)

Flux résultant :

Navigateur → grafana.mondomaine.com
           → Cloudflare Access (login email + MFA)
           → cloudflared → Grafana (login Grafana)

5. Requêtes PromQL — référence

# CPU en %
100 - (avg(rate(node_cpu_seconds_total{mode="idle", job="NOM"}[2m])) * 100)

# RAM utilisée en %
100 - (node_memory_MemAvailable_bytes{job="NOM"} / node_memory_MemTotal_bytes{job="NOM"} * 100)

# Disque / utilisé en %
100 - (
  node_filesystem_avail_bytes{job="NOM", mountpoint="/", fstype!="tmpfs"} /
  node_filesystem_size_bytes{job="NOM", mountpoint="/", fstype!="tmpfs"} * 100
)

# Uptime en secondes (afficher avec unité "s" dans Grafana)
node_time_seconds{job="NOM"} - node_boot_time_seconds{job="NOM"}

# Statut up/down
up{job="NOM"}   # 1 = up, 0 = down

6. Récapitulatif sécurité

Surface Accès Protection
Grafana (:3000) Interne Docker uniquement Cloudflare Access + auth Grafana
Prometheus (:9090) Interne Docker uniquement Pas exposé
Node Exporter Pi (:9100) Interne Docker uniquement Pas exposé
Node Exporter VPS Port 9100 restreint ou proxy nginx ufw allow from IP_PI
VPS backup SSH tunnel Aucun port supplémentaire ouvert
URL publique HTTPS uniquement via Cloudflare Cloudflare Access + MFA

Point de vigilance : Les règles ufw allow from IP_PI sont liées à l'IP externe du Pi. Si le FAI change cette IP, les VPS ne seront plus scrapés. Vérifier périodiquement avec curl -4 https://ifconfig.me depuis le Pi.


7. Commandes utiles

# Vérifier l'état de tous les targets Prometheus
docker exec prometheus wget -qO- 'http://localhost:9090/api/v1/targets' | \
  python3 -c 'import sys,json; d=json.load(sys.stdin); \
  [print(t["labels"]["job"], t["health"]) for t in d["data"]["activeTargets"]]'

# Recharger la config Prometheus sans redémarrage
docker exec prometheus kill -HUP 1

# Mettre à jour la datasource Grafana via API
curl -X PUT -H 'Content-Type: application/json' \
  -u 'admin:PASSWORD' \
  http://GRAFANA_IP:3000/api/datasources/uid/UID \
  -d '{"name":"Prometheus","type":"prometheus","url":"http://prometheus:9090","isDefault":true,"access":"proxy"}'

# Logs du tunnel SSH
docker logs tunnel-vps --tail 20 -f

# Redémarrer uniquement Prometheus
docker compose restart prometheus