Self-hosted мониторинг: Prometheus + Grafana + Alertmanager

monitoringgrafanaprometheusself-hosting

Архитектура стека мониторинга: Prometheus, Grafana, Alertmanager

Когда у тебя один сервер — мониторинг кажется излишеством. Когда серверов пять — ты уже жалеешь, что не настроил его раньше. Я поднял стек Prometheus + Grafana + Alertmanager после того, как однажды узнал о переполнении диска на продакшене от пользователя, а не от системы оповещения (которой не было). Расскажу, как настроить всё это за вечер.

Почему self-hosted, а не SaaS

Datadog, New Relic, Grafana Cloud — отличные сервисы, но:

  • Стоимость растёт с количеством метрик и серверов. Для хоббийных проектов и небольших продакшенов self-hosted стек бесплатен
  • Конфиденциальность — все метрики остаются на ваших серверах
  • Гибкость — можно мониторить абсолютно что угодно, писать свои exporters, настраивать retention по своим правилам
  • Обучение — понимание того, как работает мониторинг изнутри, бесценно для DevOps-инженера

Минус один: вам нужно самим следить за здоровьем мониторинга (кто мониторит мониторинг?). Но для масштаба в 5-15 серверов это не проблема.

Архитектура стека

Prometheus работает по pull-модели: он сам ходит к целям (targets) и забирает метрики по HTTP. Это принципиальное отличие от push-систем вроде Graphite или InfluxDB.

Компоненты:

  • Prometheus — сбор и хранение метрик, движок запросов (PromQL)
  • Node Exporter — агент на каждом сервере, отдаёт системные метрики (CPU, RAM, диск, сеть)
  • Grafana — визуализация, дашборды
  • Alertmanager — маршрутизация и отправка алертов (email, Telegram, Slack)

Prometheus и Grafana живут на одном сервере (выделенном или в контейнере на Proxmox), Node Exporter ставится на все остальные серверы.

Docker Compose для центрального сервера

Создаём директорию проекта и описываем все сервисы:

mkdir -p /opt/monitoring/{prometheus,grafana,alertmanager}
cd /opt/monitoring
# docker-compose.yml
services:
  prometheus:
    image: prom/prometheus:v2.53.0
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=90d'
      - '--web.enable-lifecycle'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
    ports:
      - "127.0.0.1:9090:9090"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:11.2.0
    container_name: grafana
    restart: unless-stopped
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SERVER_ROOT_URL=https://monitoring.example.com
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      - prometheus
    networks:
      - monitoring

  alertmanager:
    image: prom/alertmanager:v0.27.0
    container_name: alertmanager
    restart: unless-stopped
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
    ports:
      - "127.0.0.1:9093:9093"
    networks:
      - monitoring

  node-exporter:
    image: prom/node-exporter:v1.8.1
    container_name: node-exporter
    restart: unless-stopped
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    ports:
      - "127.0.0.1:9100:9100"
    networks:
      - monitoring

volumes:
  prometheus_data:
  grafana_data:

networks:
  monitoring:
    driver: bridge

Обратите внимание: все порты привязаны к 127.0.0.1. Наружу мы выставим только Grafana через Nginx reverse proxy с HTTPS.

Конфигурация Prometheus

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

rule_files:
  - "alerts.yml"

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - alertmanager:9093

scrape_configs:
  # Мониторинг самого Prometheus
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

  # Локальный node exporter
  - job_name: "node-local"
    static_configs:
      - targets: ["node-exporter:9100"]
        labels:
          instance: "monitoring-server"

  # Удалённые серверы
  - job_name: "node-remote"
    scrape_interval: 30s
    static_configs:
      - targets: ["194.87.104.208:9100"]
        labels:
          instance: "production"
          env: "prod"
      - targets: ["185.210.45.67:9100"]
        labels:
          instance: "vpn-server"
          env: "prod"

Для удалённых серверов Node Exporter устанавливается отдельно (без Docker):

# На удалённом сервере
useradd --no-create-home --shell /bin/false node_exporter

wget https://github.com/prometheus/node_exporter/releases/download/v1.8.1/node_exporter-1.8.1.linux-amd64.tar.gz
tar xzf node_exporter-1.8.1.linux-amd64.tar.gz
cp node_exporter-1.8.1.linux-amd64/node_exporter /usr/local/bin/
chown node_exporter:node_exporter /usr/local/bin/node_exporter

Systemd unit для Node Exporter:

# /etc/systemd/system/node_exporter.service
[Unit]
Description=Prometheus Node Exporter
After=network-online.target
Wants=network-online.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter 
    --collector.systemd     --collector.processes     --web.listen-address=:9100

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now node_exporter

# Проверяем, что метрики доступны
curl -s localhost:9100/metrics | head -20

Важно: если Node Exporter слушает на публичном интерфейсе, обязательно закройте порт 9100 файрволом и разрешите доступ только с IP сервера мониторинга:

ufw allow from 10.0.0.5 to any port 9100 proto tcp comment "Prometheus"

Правила алертов

# prometheus/alerts.yml
groups:
  - name: node_alerts
    rules:
      # Сервер недоступен
      - alert: InstanceDown
        expr: up == 0
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Сервер {{ $labels.instance }} недоступен"
          description: "{{ $labels.instance }} не отвечает на запросы Prometheus более 3 минут."

      # Высокая загрузка CPU
      - alert: HighCpuUsage
        expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Высокая загрузка CPU на {{ $labels.instance }}"
          description: "CPU загружен на {{ $value | printf "%.1f" }}% более 10 минут."

      # Мало свободной памяти
      - alert: LowMemory
        expr: (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 < 15
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Мало свободной памяти на {{ $labels.instance }}"
          description: "Доступно {{ $value | printf "%.1f" }}% RAM."

      # Диск заполняется
      - alert: DiskSpaceLow
        expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 15
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Заканчивается место на диске {{ $labels.instance }}"
          description: "На корневом разделе осталось {{ $value | printf "%.1f" }}% свободного места."

      - alert: DiskSpaceCritical
        expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 5
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Критически мало места на {{ $labels.instance }}"
          description: "На корневом разделе осталось {{ $value | printf "%.1f" }}%!"

      # Высокий сетевой трафик (> 80 Мбит/с)
      - alert: HighNetworkTraffic
        expr: rate(node_network_receive_bytes_total{device!~"lo|docker.*|br-.*"}[5m]) * 8 > 80000000
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "Высокий входящий трафик на {{ $labels.instance }}"
          description: "Входящий трафик: {{ $value | humanize }}bps на интерфейсе {{ $labels.device }}."

Alertmanager: алерты в Telegram

Для отправки в Telegram понадобится бот. Создайте его через @BotFather, получите токен и chat_id (можно узнать через @userinfobot).

# alertmanager/alertmanager.yml
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'instance']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'telegram'

  routes:
    - match:
        severity: critical
      receiver: 'telegram'
      repeat_interval: 1h

receivers:
  - name: 'telegram'
    telegram_configs:
      - bot_token: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'
        chat_id: -1001234567890
        parse_mode: 'HTML'
        message: |
          {{ if eq .Status "firing" }}🔴{{ else }}🟢{{ end }} <b>{{ .Status | toUpper }}</b>
          {{ range .Alerts }}
          <b>{{ .Labels.alertname }}</b>
          {{ .Annotations.summary }}
          {{ .Annotations.description }}
          {{ end }}

Параметры маршрутизации:

  • group_wait: 30s — ждём 30 секунд перед отправкой группы алертов (чтобы не спамить, если одновременно сработали несколько)
  • group_interval: 5m — минимальный интервал между уведомлениями одной группы
  • repeat_interval: 4h — повторять нерешённый алерт каждые 4 часа (для critical — каждый час)

PromQL: полезные запросы

PromQL — язык запросов Prometheus. Поначалу он кажется непривычным, но после пары дней становится интуитивным.

Базовые запросы:

# Загрузка CPU (%) за последние 5 минут
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# Использование RAM (%)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# Использование диска (%)
(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100

# Входящий трафик (Мбит/с)
rate(node_network_receive_bytes_total{device="eth0"}[5m]) * 8 / 1024 / 1024

# Количество открытых файловых дескрипторов
node_filefd_allocated

# Uptime сервера в днях
(time() - node_boot_time_seconds) / 86400

# Количество запущенных процессов
node_procs_running

Более сложные примеры:

# Прогноз заполнения диска (через сколько часов закончится место при текущей скорости)
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) / 1024 / 1024 / 1024

# Top-5 серверов по загрузке CPU
topk(5, 100 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# Аномальное потребление RAM (отклонение от среднего за неделю)
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)
  / avg_over_time((node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)[7d:1h])

Grafana: настройка дашборда

После запуска добавляем Prometheus как источник данных. Это можно автоматизировать через provisioning:

# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1

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

Для Node Exporter есть отличный готовый дашборд — ID 1860 на grafana.com. Импортируется за один клик через Dashboards -> Import -> ввести 1860.

Но я рекомендую со временем собрать свой кастомный дашборд, заточенный под ваши нужды. Вот несколько панелей, которые я считаю обязательными:

  1. Общий статус — row с stat-панелями: uptime, CPU, RAM, disk для каждого сервера
  2. CPU по ядрам — graph с breakdown по каждому ядру (полезно для диагностики однопоточной нагрузки)
  3. Сетевой трафик — отдельные графики для входящего и исходящего трафика
  4. Дисковый I/O — latency и throughput (помогает выявить деградацию диска до того, как он умрёт)
  5. Системный лог ошибок — если подключен Loki, панель с фильтрацией по level=error

Nginx reverse proxy

Grafana должна быть доступна по HTTPS. Минимальный конфиг для Nginx:

server {
    listen 443 ssl http2;
    server_name monitoring.example.com;

    ssl_certificate /etc/letsencrypt/live/monitoring.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/monitoring.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebSocket для live-обновления дашбордов
    location /api/live/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Запуск

cd /opt/monitoring

# Задаём пароль Grafana
echo "GRAFANA_PASSWORD=your_secure_password" > .env

# Запускаем
docker compose up -d

# Проверяем статус
docker compose ps

# Смотрим логи Prometheus
docker compose logs -f prometheus

# Проверяем, что targets доступны
curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {instance: .labels.instance, health: .health}'

Ожидаемый вывод проверки targets:

{
  "instance": "monitoring-server",
  "health": "up"
}
{
  "instance": "production",
  "health": "up"
}

Что мониторить помимо системных метрик

Node Exporter покрывает базовые системные метрики, но для полной картины стоит добавить:

  • cAdvisor или Docker metrics — метрики контейнеров (CPU, RAM, сеть для каждого контейнера)
  • Blackbox Exporter — проверка доступности HTTP-эндпоинтов, DNS, TCP/ICMP
  • Postgres Exporter / MySQL Exporter — метрики баз данных (количество соединений, latency запросов, размер таблиц)
  • Custom metrics — ваши приложения могут отдавать метрики в формате Prometheus через /metrics endpoint

Пример добавления Blackbox Exporter для проверки доступности сайтов:

# В prometheus.yml добавляем
- job_name: 'blackbox-http'
  metrics_path: /probe
  params:
    module: [http_2xx]
  static_configs:
    - targets:
        - https://example.com
        - https://api.example.com/health
  relabel_configs:
    - source_labels: [__address__]
      target_label: __param_target
    - source_labels: [__param_target]
      target_label: instance
    - target_label: __address__
      replacement: blackbox-exporter:9115

Заключение

Полноценный стек мониторинга можно поднять за пару часов. Prometheus + Grafana + Alertmanager — это проверенная комбинация, которую используют компании любого масштаба, от стартапов до Netflix и Cloudflare.

Ключевые рекомендации:

  1. Начните с node_exporter на всех серверах и готового дашборда (ID 1860)
  2. Настройте алерты на критичные метрики: диск, память, доступность
  3. Отправляйте алерты в Telegram — это быстрее и заметнее email
  4. Задайте разумный retention (90 дней достаточно для большинства случаев)
  5. Не забудьте замониторить сам сервер мониторинга (внешним чекером, например UptimeRobot)

В следующем посте расскажу, как автоматизировать деплой всего этого стека через Ansible, чтобы разворачивать мониторинг на новом сервере одной командой.

© 2026 Terminal Notes. Built with SvelteKit.