Self-hosted мониторинг: 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.
Но я рекомендую со временем собрать свой кастомный дашборд, заточенный под ваши нужды. Вот несколько панелей, которые я считаю обязательными:
- Общий статус — row с stat-панелями: uptime, CPU, RAM, disk для каждого сервера
- CPU по ядрам — graph с breakdown по каждому ядру (полезно для диагностики однопоточной нагрузки)
- Сетевой трафик — отдельные графики для входящего и исходящего трафика
- Дисковый I/O — latency и throughput (помогает выявить деградацию диска до того, как он умрёт)
- Системный лог ошибок — если подключен 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 через
/metricsendpoint
Пример добавления 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.
Ключевые рекомендации:
- Начните с
node_exporterна всех серверах и готового дашборда (ID 1860) - Настройте алерты на критичные метрики: диск, память, доступность
- Отправляйте алерты в Telegram — это быстрее и заметнее email
- Задайте разумный retention (90 дней достаточно для большинства случаев)
- Не забудьте замониторить сам сервер мониторинга (внешним чекером, например UptimeRobot)
В следующем посте расскажу, как автоматизировать деплой всего этого стека через Ansible, чтобы разворачивать мониторинг на новом сервере одной командой.