Эта статья — результат реальной настройки мониторинга для нескольких серверов и нескольких сайтов на одной машине. Здесь собрано то, что работает на практике: не только как поднять экспортеры, но и почему они не поднимаются с первого раза — и как это починить.

Что мы будем делать

Берём несколько Ubuntu-серверов и выстраиваем централизованную систему мониторинга:

  • Prometheus и Grafana — в Docker Compose на одном центральном сервере
  • Системные метрики каждого сервера через Node Exporter
  • Метрики PostgreSQL через postgres_exporter
  • Метрики Nginx с разделением по каждому сайту отдельно
  • Prometheus сам ходит за метриками ко всем экспортерам по сети

Все команды проверены на практике. Типичные ошибки — в том числе неочевидный нюанс с Docker bridge и проблема с файрволом — разобраны отдельно.

Содержание

  1. Архитектура решения
  2. Prometheus и Grafana в Docker Compose
  3. Системные метрики: Node Exporter
  4. Метрики PostgreSQL: postgres_exporter
  5. Метрики Nginx: разделение по сайтам
  6. Настройка prometheus.yml
  7. Подключение Grafana к Prometheus
  8. Готовые дашборды
  9. Типичные ошибки и их решение

1. Архитектура решения

Прежде чем что-то устанавливать, важно понять общую схему. Prometheus работает по pull-модели: он сам периодически обходит экспортеры и забирает метрики. Grafana сама по себе ничего не собирает — она только отображает то, что хранит Prometheus.

В нашем случае схема выглядит так: на центральном сервере в Docker Compose запускаются Prometheus и Grafana. На всех остальных серверах — только экспортеры, никакого Prometheus там нет. Prometheus с центрального сервера сам ходит по сети к экспортерам и забирает метрики. Grafana читает данные из локального Prometheus внутри Docker-сети.

Важный нюанс про Docker bridge: Prometheus работает внутри контейнера. Если на том же физическом сервере установлен Node Exporter (вне Docker), то обратиться к нему через localhost:9100 из контейнера не получится — внутри контейнера localhost означает сам контейнер. Нужно использовать адрес Docker bridge-интерфейса: обычно это 172.17.0.1. Проверить можно командой ip addr show docker0 на хосте.

Важно про файрвол: Между серверами должны быть открыты нужные порты. Prometheus обращается к экспортерам по сети, и если где-то стоит UFW или iptables — это первое место, где всё может сломаться. Подробнее в разделе про типичные ошибки.

2. Prometheus и Grafana в Docker Compose

Prometheus и Grafana поднимаются на центральном сервере через Docker Compose. Для проксирования и SSL удобно использовать Traefik — он автоматически получает сертификаты Let's Encrypt и маршрутизирует запросы к нужным контейнерам по имени домена.

2.1. Структура файлов

Создаём директорию для проекта и файл конфигурации Prometheus:

mkdir -p /etc/docker/monitoring
cd /etc/docker/monitoring
nano prometheus.yml   # Конфигурация разбирается в разделе 6
nano docker-compose.yml

2.2. docker-compose.yml

Минимальная конфигурация с Prometheus и Grafana (без Traefik — если хотите сразу открывать по IP и порту):

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    ports:
      - '9090:9090'   # Убрать если используете Traefik

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=ЗАМЕНИТЕ_НА_НАДЁЖНЫЙ_ПАРОЛЬ
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - '3000:3000'   # Убрать если используете Traefik
    depends_on:
      - prometheus

volumes:
  prometheus-data:
  grafana-data:

Если используете Traefik — убираете блоки ports и добавляете нужные labels и networks. Тогда Grafana будет доступна по домену через HTTPS, а не по голому порту 3000.

2.3. Запуск

docker compose up -d
docker compose ps   # Убеждаемся, что все контейнеры запустились

Grafana будет доступна по адресу http://<IP-сервера>:3000 (или по домену, если настроен Traefik). Логин и пароль по умолчанию: admin / admin, если не задан GF_SECURITY_ADMIN_PASSWORD. При первом входе система попросит сменить пароль — обязательно сделайте это.

2.4. Применение изменений в конфиге Prometheus

После правок в prometheus.yml не нужно пересоздавать контейнер — достаточно послать сигнал SIGHUP:

docker kill --signal=SIGHUP prometheus

Prometheus перечитает конфигурацию без потери собранных данных.

3. Системные метрики: Node Exporter

Node Exporter читает данные из /proc и /sys и отдаёт их Prometheus в нужном формате. Устанавливается на каждый сервер, который хотите мониторить — как на удалённые, так и на тот, где запущен Docker с Prometheus.

3.1. Устанавливаем Node Exporter

Создаём отдельного пользователя без прав на вход в систему — запускать экспортер от root не нужно:

sudo useradd --no-create-home --shell /bin/false node_exporter

Скачиваем и устанавливаем бинарный файл (проверьте актуальную версию на github.com/prometheus/node_exporter/releases):

wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xvf node_exporter-1.8.2.linux-amd64.tar.gz
sudo cp node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter
rm -rf node_exporter-1.8.2.linux-amd64*

3.2. Создаём systemd-сервис

sudo nano /etc/systemd/system/node_exporter.service

Содержимое:

[Unit]
Description=Node Exporter
After=network.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target

3.3. Запускаем и проверяем

sudo systemctl daemon-reload
sudo systemctl start node_exporter
sudo systemctl enable node_exporter
sudo systemctl status node_exporter

Должно быть active (running). Убеждаемся, что экспортер слушает на всех интерфейсах, а не только на localhost:

sudo ss -tulpn | grep 9100

В выводе должно быть *:9100 или 0.0.0.0:9100. Если 127.0.0.1:9100 — значит в параметрах сервиса явно задан флаг --web.listen-address=127.0.0.1:9100, который нужно убрать. Метрики сервера теперь доступны по адресу http://<IP-сервера>:9100/metrics.

Node Exporter на сервере с Docker: если Node Exporter установлен на том же хосте, где запущен Prometheus в контейнере, в prometheus.yml нужно указывать не localhost:9100, а 172.17.0.1:9100 — адрес Docker bridge-интерфейса хоста. Иначе Prometheus просто не найдёт экспортер.

Что именно мониторит Node Exporter: загрузка CPU по ядрам, использование оперативной памяти и swap, дисковое пространство и I/O операции, сетевой трафик, среднее время нагрузки (load average), количество запущенных процессов и системное время работы сервера.

4. Метрики PostgreSQL: postgres_exporter

postgres_exporter устанавливается на тот же сервер, где работает база данных, и подключается к ней от имени специального пользователя с правами только на чтение статистики.

4.1. Создаём пользователя в PostgreSQL

Подключаемся к базе и выполняем:

CREATE USER postgres_exporter WITH PASSWORD 'ЗАМЕНИТЕ_НА_НАДЁЖНЫЙ_ПАРОЛЬ' CONNECTION LIMIT 5;
ALTER USER postgres_exporter SET search_path TO 'postgres_exporter', 'public';
GRANT CONNECT ON DATABASE your_database_name TO postgres_exporter;  -- ЗАМЕНИТЕ ИМЯ БД
GRANT pg_monitor TO postgres_exporter;  -- Для PostgreSQL 10+

Роль pg_monitor даёт доступ к статистическим представлениям без привилегий суперпользователя — именно то, что нужно.

4.2. Устанавливаем postgres_exporter

sudo useradd --no-create-home --shell /bin/false postgres_exporter

wget https://github.com/prometheus-community/postgres_exporter/releases/download/v0.15.0/postgres_exporter-0.15.0.linux-amd64.tar.gz
tar xvf postgres_exporter-0.15.0.linux-amd64.tar.gz
sudo cp postgres_exporter-0.15.0.linux-amd64/postgres_exporter /usr/local/bin/
sudo chown postgres_exporter:postgres_exporter /usr/local/bin/postgres_exporter
rm -rf postgres_exporter-0.15.0.linux-amd64*

4.3. Настраиваем строку подключения

Создаём файл с переменными окружения:

sudo nano /etc/default/postgres_exporter

Содержимое:

DATA_SOURCE_NAME="postgresql://postgres_exporter:ВАШИ_ДАННЫЕ@localhost:5432/your_database?sslmode=disable"  # ЗАМЕНИТЕ

Ограничиваем права на файл — там хранится пароль:

sudo chmod 600 /etc/default/postgres_exporter
sudo chown postgres_exporter:postgres_exporter /etc/default/postgres_exporter

4.4. Создаём systemd-сервис и запускаем

sudo nano /etc/systemd/system/postgres_exporter.service
[Unit]
Description=Postgres Exporter
After=network.target

[Service]
User=postgres_exporter
Group=postgres_exporter
EnvironmentFile=/etc/default/postgres_exporter
ExecStart=/usr/local/bin/postgres_exporter

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start postgres_exporter
sudo systemctl enable postgres_exporter

Метрики PostgreSQL доступны по адресу http://<IP-сервера>:9187/metrics.

5. Метрики Nginx: разделение по сайтам

Здесь есть важный нюанс, который легко пропустить: стандартный модуль stub_status в Nginx не умеет разделять статистику по виртуальным хостам. Он отдаёт суммарные глобальные метрики по всему Nginx — сколько бы сайтов на нём ни работало.

Поэтому если запустить один nginx_exporter на всё — в Grafana будет видна общая картина, но будет невозможно понять, какой из сайтов создаёт нагрузку или перестал отвечать.

Решение: для каждого сайта создать отдельный location для stub_status, запустить отдельный экземпляр nginx_exporter на своём порту с константной меткой site=имя-сайта. Тогда в Prometheus и Grafana каждая метрика будет знать, к какому сайту относится.

5.1. Добавляем stub_status в конфиги Nginx

В каждый HTTPS-блок (там, где listen 443 ssl) нужно добавить отдельный location. Используем уникальные пути для каждого сайта. Открываем конфиг первого сайта:

sudo nano /etc/nginx/sites-available/your-site  # ЗАМЕНИТЕ ИМЯ ФАЙЛА

Внутри блока server { ... } для порта 443 добавляем (например, после location = /favicon.ico):

location /nginx_status_site1 {  # ЗАМЕНИТЕ site1 НА УНИКАЛЬНОЕ ИМЯ
    stub_status;
    access_log off;
    allow 127.0.0.1;
    allow ::1;
    deny all;
}

Для второго сайта — тот же блок, но путь другой: /nginx_status_site2. И так для каждого. Главное — уникальность пути и ограничение allow 127.0.0.1: экспортер будет обращаться только с localhost, посторонние статус не увидят.

После добавления во все конфиги проверяем синтаксис и перезагружаем Nginx:

sudo nginx -t
sudo systemctl reload nginx

Проверяем, что каждый endpoint отвечает:

curl http://localhost/nginx_status_site1
curl http://localhost/nginx_status_site2

Ответ должен выглядеть примерно так:

Active connections: 2
server accepts handled requests
 145 145 312
Reading: 0 Writing: 1 Waiting: 1

Если 403 Forbidden — значит в allow что-то не так. Если 404 — location добавлен не в тот блок сервера.

5.2. Устанавливаем nginx_exporter

sudo useradd --no-create-home --shell /bin/false nginx_exporter

wget https://github.com/nginxinc/nginx-prometheus-exporter/releases/download/v1.3.0/nginx-prometheus-exporter_1.3.0_linux_amd64.tar.gz
tar xvf nginx-prometheus-exporter_1.3.0_linux_amd64.tar.gz
sudo cp nginx-prometheus-exporter /usr/local/bin/
sudo chown nginx_exporter:nginx_exporter /usr/local/bin/nginx-prometheus-exporter
rm nginx-prometheus-exporter*

5.3. Создаём отдельный сервис для каждого сайта

Для первого сайта:

sudo nano /etc/systemd/system/nginx_exporter_site1.service  # ЗАМЕНИТЕ site1
[Unit]
Description=Nginx Exporter for site1.example.com  # ЗАМЕНИТЕ НА ВАШ ДОМЕН
After=network.target

[Service]
User=nginx_exporter
Group=nginx_exporter
Type=simple
ExecStart=/usr/local/bin/nginx-prometheus-exporter \
  --nginx.scrape-uri=http://localhost/nginx_status_site1 \  # ЗАМЕНИТЕ ПУТЬ
  --web.listen-address=:9114 \  # УНИКАЛЬНЫЙ ПОРТ ДЛЯ КАЖДОГО САЙТА
  --prometheus.const-label=site=site1.example.com  # ЗАМЕНИТЕ НА ВАШ ДОМЕН

[Install]
WantedBy=multi-user.target

Для второго сайта создаём аналогичный файл nginx_exporter_site2.service, меняя три параметра: путь к nginx_status_site2, порт :9115 и метку site=site2.example.com. Каждый следующий сайт — следующий порт по порядку: 9116, 9117 и так далее.

Почему константная метка важна: флаг --prometheus.const-label=site=site1.example.com добавляет к каждой метрике этого экспортера дополнительное поле site. В Grafana потом можно написать запрос nginx_http_requests_total{site="site1.example.com"} и увидеть данные только для этого сайта.

Запускаем все созданные сервисы:

sudo systemctl daemon-reload
sudo systemctl start nginx_exporter_site1
sudo systemctl start nginx_exporter_site2
sudo systemctl enable nginx_exporter_site1
sudo systemctl enable nginx_exporter_site2

Проверяем, что метрики отдаются с нужной меткой:

curl http://localhost:9114/metrics | grep site

Должны увидеть строки вида nginx_connections_active{site="site1.example.com"} 2.

Что именно мониторит nginx_exporter: активные соединения, соединения в состоянии reading/writing/waiting, общее количество принятых и обработанных соединений, суммарное количество HTTP-запросов. На основе последней метрики строится RPS — количество запросов в секунду.

6. Настройка prometheus.yml

Вся конфигурация сбора метрик находится в одном файле prometheus.yml, который монтируется в контейнер. Prometheus сам ходит к каждому экспортеру по указанному адресу и забирает метрики с заданным интервалом.

Полная конфигурация для нашего случая — системные метрики нескольких серверов, PostgreSQL и Nginx с разделением по сайтам:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

  # Node Exporter на сервере с Docker (не localhost, а bridge-адрес хоста)
  - job_name: 'node_central'
    static_configs:
      - targets: ['172.17.0.1:9100']  # Docker bridge — адрес хоста внутри контейнера
        labels:
          server: 'central'

  # Node Exporter на удалённом сервере
  - job_name: 'node_remote'
    static_configs:
      - targets: ['192.168.1.10:9100']  # ЗАМЕНИТЕ: IP удалённого сервера
        labels:
          server: 'remote'

  # Метрики PostgreSQL
  - job_name: 'postgresql'
    static_configs:
      - targets: ['192.168.1.10:9187']  # ЗАМЕНИТЕ: IP сервера с postgres_exporter
        labels:
          server: 'remote'

  # Метрики Nginx: каждый сайт — отдельная цель
  - job_name: 'nginx_site1'
    static_configs:
      - targets: ['192.168.1.10:9114']  # ЗАМЕНИТЕ: IP сервера с Nginx
        labels:
          server: 'remote'

  - job_name: 'nginx_site2'
    static_configs:
      - targets: ['192.168.1.10:9115']  # ЗАМЕНИТЕ
        labels:
          server: 'remote'

  # Добавьте по аналогии для каждого nginx_exporter

После изменений применяем конфигурацию без перезапуска контейнера:

docker kill --signal=SIGHUP prometheus

Проверяем, что все цели подхватились: открываем веб-интерфейс Prometheus (Status → Targets) — все настроенные экспортеры должны отображаться со статусом UP.

Про метки (labels): метки server, role и другие в prometheus.yml добавляются ко всем метрикам конкретного job на уровне Prometheus. Это удобно для фильтрации в Grafana: можно сразу отобразить всё по конкретному серверу запросом вида node_cpu_seconds_total{server="remote"}.

7. Подключение Grafana к Prometheus

Grafana и Prometheus работают в одной Docker-сети, поэтому Grafana обращается к Prometheus по имени контейнера — не по IP и не по внешнему адресу.

Заходим в Grafana, переходим в Connections → Data sources → Add data source, выбираем Prometheus. В поле Prometheus server URL указываем:

http://prometheus:9090

Называем источник понятно, например Prometheus. Нажимаем Save & test — должна появиться зелёная надпись об успешном подключении.

Почему по имени контейнера: Docker Compose автоматически создаёт внутреннюю DNS-запись для каждого сервиса. Контейнеры в одной сети резолвят друг друга по именам сервисов из docker-compose.yml. Использовать localhost нельзя по той же причине, что и с Node Exporter — внутри контейнера localhost это сам контейнер.

8. Готовые дашборды

Создавать дашборды с нуля не нужно — на сайте Grafana есть тысячи готовых вариантов. Импорт занимает несколько секунд: в меню Dashboards → Import вводим ID и выбираем источник данных.

Рекомендуемые дашборды для нашей конфигурации: для системных метрик Node Exporter подходит ID 1860 («Node Exporter Full» — самый популярный и информативный). Для PostgreSQL хорошо работает ID 9628. Для Nginx — ID 12708. Для мониторинга самого Prometheus есть ID 3662 («Prometheus 2.0 Stats»).

После импорта дашборда для Nginx в настройках переменных можно задать фильтр по метке site — тогда в верхней части дашборда появится выпадающий список для переключения между сайтами.

9. Типичные ошибки и их решение

9.1. Prometheus не достучивается до экспортера: i/o timeout

Самая распространённая проблема при работе с несколькими серверами. В интерфейсе Prometheus (Status → Targets) видно что-то вроде:

Post "http://192.168.1.10:9100/metrics": dial tcp 192.168.1.10:9100: i/o timeout

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

Сначала убеждаемся, что экспортер действительно запущен на целевом сервере:

sudo systemctl status node_exporter
sudo ss -tulpn | grep 9100

Если экспортер работает, проверяем файрвол на сервере с экспортером. Именно здесь чаще всего и зарыта проблема:

sudo ufw status

Если UFW активен и порта 9100 нет в списке разрешённых, добавляем правило — лучше ограниченное конкретным IP центрального сервера, а не открытое всему миру:

sudo ufw allow from 192.168.1.20 to any port 9100 proto tcp  # ЗАМЕНИТЕ: IP центрального сервера

Для nginx_exporter аналогично открываем нужные порты. Если экспортеров несколько — можно разрешить диапазон:

sudo ufw allow from 192.168.1.20 to any port 9114:9121 proto tcp  # ЗАМЕНИТЕ

После изменений проверяем доступность порта с центрального сервера:

nc -zv 192.168.1.10 9100  # ЗАМЕНИТЕ IP

Если подключение прошло — Connection to 192.168.1.10 9100 port [tcp/*] succeeded!.

9.2. Node Exporter на хосте недоступен из контейнера

Если Node Exporter установлен на том же сервере, где запущен Docker с Prometheus, и в prometheus.yml указан localhost:9100 или 127.0.0.1:9100 — Prometheus его не найдёт. Внутри контейнера localhost это сам контейнер, а не хост.

Решение: используем адрес Docker bridge-интерфейса. Проверяем его на хосте:

ip addr show docker0

Обычно это 172.17.0.1. Именно этот адрес и указываем в prometheus.yml:

- targets: ['172.17.0.1:9100']

После правки применяем изменения:

docker kill --signal=SIGHUP prometheus

9.3. Экспортер слушает только на localhost

Иногда экспортер запускается, но принимает соединения только с локальной машины. Симптом: curl http://localhost:9100/metrics работает, но с другого сервера — нет.

Проверяем вывод ss -tulpn: если там 127.0.0.1:9100, значит в ExecStart сервиса явно задан флаг --web.listen-address=127.0.0.1:9100. Убираем его или заменяем на --web.listen-address=:9100 (двоеточие без IP означает «все интерфейсы»):

sudo nano /etc/systemd/system/node_exporter.service
# Убираем или меняем флаг --web.listen-address
sudo systemctl daemon-reload
sudo systemctl restart node_exporter

9.4. Метрики Nginx не различаются по сайтам

Если все сайты в Grafana показывают одинаковые значения — скорее всего, запущен один общий nginx_exporter без метки site. Проверяем:

curl http://localhost:9113/metrics | grep site

Если метки нет — нужно создать отдельные сервисы для каждого сайта по инструкции из раздела 5, а старый общий экспортер остановить и отключить:

sudo systemctl stop nginx_exporter
sudo systemctl disable nginx_exporter
sudo rm /etc/systemd/system/nginx_exporter.service
sudo systemctl daemon-reload

9.5. Grafana не видит данные

Если источник данных добавлен, но графики пустые — проверяем URL источника данных. Grafana должна обращаться к Prometheus по имени контейнера (http://prometheus:9090), а не по localhost и не по внешнему IP. Также убеждаемся, что временной диапазон в правом верхнем углу Grafana не слишком узкий — метрики могут просто не успеть накопиться.

Заключение

Теперь у вас работает полноценная система мониторинга: Prometheus и Grafana подняты в Docker Compose на одном сервере, каждый удалённый сервер отдаёт метрики через свои экспортеры, а по каждому сайту на Nginx видно свои показатели независимо от остальных.

Главные выводы, которые стоит запомнить: если экспортер запущен и работает, но Prometheus его не видит — в первую очередь смотрим на файрвол. Если Prometheus в Docker не видит Node Exporter на том же хосте — используем 172.17.0.1 вместо localhost. Если Grafana не подключается к Prometheus — используем имя контейнера prometheus, а не localhost.

При расширении инфраструктуры — новый сайт или новый сервер — достаточно повторить соответствующие шаги: добавить stub_status в Nginx, создать новый сервис экспортера с уникальным портом и меткой, добавить цель в prometheus.yml, открыть порт в файрволе и применить конфиг через docker kill --signal=SIGHUP prometheus.