Содержание:
- Настройка DNS
- Настройка реверс-прокси Caddy
- Структура директорий для сервисов
- Настройка Prometheus
- docker-compose для мониторинга
- UFW-правило для node_exporter
- Запуск
- Настройка домена Grafana в Caddy
- Защита Grafana: права на Caddyfile, basic_auth, fail2ban
- Подключение Prometheus к Grafana
- Импорт дашбордов
- Версионирование конфигов в Git
- Памятка для добавления нового сервиса
Это вторая часть гайда по настройке self-hosted-инфраструктуры. В первой части мы поставили минимально необходимые утилиты, подняли Docker для наших сервисов, сделали базовую настройку безопасности (UFW, fail2ban, настроили ssh) и настроили минимальный мониторинг средствами DigitalOcean.
В этой части поднимем полноценный мониторинг на стеке Prometheus + Grafana, а также настроим домен для него.
Ожидается, что у читателя уже куплен домен. Подойдет абсолютно любой, у меня Hostinger.
Чеклист: что настроим в этой части
DNS и reverse proxy
-
A-запись
@ -> IP_VPSдобавлена - A-запись для каждого нужного вам поддомена
-
dig +shortвозвращает корректный IP - Caddy установлен
-
/etc/caddy/Caddyfileзаменен на свой конфиг с email -
caddy validateпроходит -
HTTPS работает на корне домена:
curl -I https://example.comотдаетHTTP/2 200 -
В логах Caddy есть
certificate obtained successfully
Структура и конфиги
-
Директория
~/services/observability/создана -
prometheus.ymlс тремя job (prometheus,node,cadvisor) -
docker-compose.ymlс четырьмя сервисами -
Все порты привязаны к
127.0.0.1:(кроме node_exporter в host-режиме) -
mem_limitвыставлен для каждого сервиса - Конфиги мониторинга трекаются git’ом
Сеть и UFW
-
UFW-правило
from 172.16.0.0/12 to any port 9100 proto tcp - UFW снаружи открыт только 22, 80, 443
Запуск
-
docker compose up -dотработал без ошибок -
docker compose psпоказывает все сервисы какrunning -
Все три таргета в Prometheus
up -
Grafana открывается по
https://grafana.example.com -
Datasource Prometheus подключен по URL
http://prometheus:9090 - Импортированы дашборды 1860 и 193
Защита Grafana (опционально)
-
Пароль Grafana (
GF_SECURITY_ADMIN_PASSWORDв.env) сменен сchangemeпосле первого входа -
Caddyfileимеет права640 root:caddy(если Caddy работает под user/groupcaddy, см. 9.1) -
Хеш сгенерирован через
caddy hash-password -
basic_authдобавлен в блокgrafana.example.com - При входе в инкогнито браузер спрашивает basic_auth логин до открытия Grafana
-
Access-лог Caddy пишется в
/var/log/caddy/grafana-access.log, ротация логов настроена -
Фильтр
caddy-grafana-authв/etc/fail2ban/filter.d/настроен - fail2ban-regex ловит фейлы
-
Jail
caddy-grafana-authактивен (sudo fail2ban-client statusпоказывает его) - Тест: 5 неверных попыток приводят к бану IP в UFW
Grafana я хочу открывать из браузера, поэтому ей нужен публичный вход по HTTPS - отсюда требования к настройке.
Главное
- Обязательный HTTPS. У нас будет открыт сервис с Grafana - поэтому нам необходим сертификат Let’s Encrypt.
- Главный принцип из первой статьи - минимизация поверхности атаки. Любой сервис слушает на
127.0.0.1(единственное исключение -node_exporterв host-режиме на0.0.0.0:9100, но снаружи его закрывает UFW; детали в разделе 6). Наружу доступен только реверс-прокси (в прошлой статье опционально открывали порты 80 и 443 ровно для этого случая). В качестве реверс-прокси используем Caddy (но можно взять любой, хоть Nginx). - Изоляция сервисов. Каждый сервис (кроме Caddy в нашем случае) живет в своем контейнере.
1. Настройка DNS
Реверс-прокси - это сервер, который стоит перед бэкенд-сервисами, принимает запросы от клиентов и проксирует на них. Клиент общается с реверс-прокси и не видит сам сервис. Назначений у реверс-прокси несколько, но в нашем случае важна именно маршрутизация.
Прежде чем запускать Caddy, домены должны резолвиться на IP нашего VPS. Иначе Let’s Encrypt не сможет проверить владение и не выдаст сертификаты.
В панели регистратора домена нужно добавить A-записи (где IP_VPS - IP вашего VPS):
A @ IP_VPS TTL 60-300
A grafana IP_VPS TTL 300
@ - это обозначение корня домена, то есть это сам домен без точки слева. Т.е. example.com - это корень, а www.example.com, grafana.example.com и т.д. - это поддомены.
Каждый поддомен - это отдельная A-запись (в нашем случае grafana). DNS не наследует записи, т.е. если прописана @ запись для корня, это не значит, что grafana.example.com тоже будет резолвиться на указанный в @ IP. Конкретно для Grafana нам нужна только вторая запись, первую добавляем для блога.
Проверка через минуту-две (где example.com - ваш домен):
dig +short grafana.example.com
Он должен вернуть IP VPS.
2. Настройка реверс-прокси Caddy
Дока Caddy: https://caddyserver.com/docs/install#debian-ubuntu-raspbian
Установка:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
После установки Caddy будет работать на 80 и 443 с дефолтным “Welcome”. UFW для них уже открыт (как открыть, если еще не - в предыдущей статье).
Заменим /etc/caddy/Caddyfile (где example.com - ваш домен):
{
email username@example.com
}
example.com {
respond "Hello from Caddy"
}
www.example.com {
redir https://example.com{uri} permanent
}
На email в глобальном блоке Let’s Encrypt пришлет уведомление, если с сертификатом будут какие-то проблемы.
Валидируем и применяем конфиг:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo journalctl -u caddy -n 30 --no-pager
В логах должно быть certificate obtained successfully и Valid configuration. Если открыть в браузере https://example.com, то увидим “Hello from Caddy”. Из терминала то же самое проверяется так:
curl -I https://example.com
Должен вернуться HTTP/2 200.
3. Структура директорий для сервисов
Все docker-сервисы будут храниться в ~/services/:
mkdir -p ~/services/observability/prometheus
cd ~/services/observability4. Настройка Prometheus
Prometheus работает по pull-модели - он сам ходит по каждому таргету и забирает метрики по HTTP с пути /metrics. В конфиге ниже перечислен список адресов, к которым он стучится.
~/services/observability/prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
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']
scrape_interval- интервал сбора метрик: как часто Prometheus опрашивает таргеты и забирает/metrics.evaluation_interval- Prometheus по этой частоте проверяет alerting rules. При срабатывании условия Prometheus пушит алерт в Alertmanager, который занимается рассылкой. У нас алерты будут через Grafana (в будущем), поэтому не обязательно. Если хотите через Alertmanager, то оставьте.scrape_configs- список джобов. Каждая джоба - это группа таргетов одного типа.job_name- это просто метка, по ней потом можно посмотреть метрики через PromQL, например:up{job="node"}.static_configs- список статично заданных адресов таргетов (еще можно получать адреса через service discovery, но в статье это не рассматривается).targets- массив адресов, откуда Prometheus пойдет пуллить метрики по/metrics.
У нас перечислено 3 таргета:
prometheus- Prometheus собирает свои собственные метрики (про самого себя). Стучится вlocalhost:9090внутри своего контейнера.node- метрики хоста: CPU, RAM, диски, сеть. node_exporter запущен вnetwork_mode: host, поэтому биндится на0.0.0.0:9100и слушает на всех IP-адресах хоста. Prometheus идет до хоста черезhost.docker.internal:9100- это служебное hostname Docker, которое резолвится в IP хост-машины со стороны контейнера. На Linux оно не работает из коробки, поэтому вdocker-compose.ymlниже у Prometheus прописанextra_hosts: ["host.docker.internal:host-gateway"]-host-gatewayозначает “IP хоста, видимый из контейнера”. Альтернатива - хардкодить172.17.0.1(дефолтный IP мостаdocker0), но это ломается, если Docker стартует с другой подсетью (конфликт с локальной сетью, VPN и т.д.).cadvisor- метрики контейнеров. cadvisor - обычный контейнер в той же compose-сети, что и Prometheus. Они общаются по DNS-имени сервиса (cadvisor), которое Docker Compose автоматически резолвит в текущий IP контейнера. Делаем по доменному имени, потому что IP контейнера может смениться при пересоздании.
5. docker-compose для мониторинга
~/services/observability/docker-compose.yml:
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "127.0.0.1:9090:9090"
volumes:
- ./prometheus:/etc/prometheus:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--storage.tsdb.retention.size=5GB'
- '--web.enable-lifecycle'
extra_hosts:
- "host.docker.internal:host-gateway"
mem_limit: 512m
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=https://grafana.example.com
mem_limit: 512m
node_exporter:
image: prom/node-exporter:latest
container_name: node_exporter
restart: unless-stopped
pid: host
network_mode: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
mem_limit: 64m
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
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
ports:
- "127.0.0.1:8080:8080"
mem_limit: 256m
volumes:
prometheus_data:
grafana_data:
Пароль не держим в compose открытым текстом - ${GF_SECURITY_ADMIN_PASSWORD} подставляется из .env рядом с compose (создаем с правами 600, чтобы прочитать мог только владелец):
touch ~/services/observability/.env
chmod 600 ~/services/observability/.env
echo 'GF_SECURITY_ADMIN_PASSWORD=changeme' >> ~/services/observability/.env
Это первичный пароль admin, changeme заменим в разделе 10, когда зайдем в Grafana из веба. .env обязательно попадает в .gitignore (раздел 12), иначе пароль уедет в git.
grafana.example.com нужно поменять на свое значение.
Краткое пояснение:
- Для
imageя поставилlatest, поскольку это некритичные сервисы. Для сервисов, где важна стабильность и воспроизводимость, рекомендуется ставить конкретную версию. ports: "127.0.0.1:9090:9090"- если без префикса127.0.0.1:, то порт откроется на0.0.0.0. С префиксом он будет слушать только на loopback, доступен только локально и через Caddy.network_mode: hostдляnode_exporterнужен, чтобы он видел сетевые интерфейсы и метрики хоста.mem_limit- чтобы контейнер не съел всю память. Если контейнер превышает лимит, срабатывает OOM-киллер. Здесь я подобрал лимиты с запасом под 2GB RAM, можете поставить под свои ресурсы и потребности.retention.time=30d,retention.size=5GB- политика удаления старых метрик. Данные хранятся не дольше 30 дней и не превышают 5 ГБ. Аналогично - ставьте под свои задачи и доступные ресурсы.privileged: trueдляcadvisor- нужен для чтения cgroup других контейнеров.restart: unless-stopped- перезапускать при падении контейнера или рестарте хоста, кроме случаев ручной остановки.- В разделе
volumesмонтируем конфиг Prometheus и именованные томаprometheus_dataиgrafana_data- в них хранится база метрик Prometheus и данные Grafana (настройки, дашборды и т.д.). Это нужно, чтобы данные сохранялись после пересоздания контейнеров. - Конфиг Prometheus монтируем директорией (
./prometheus:/etc/prometheus:ro), а не отдельным файлом. Если монтировать файл напрямую, при правке черезvim(или любой редактор, использующий атомарную запись черезmv) у файла меняется inode, а bind-mount остается привязан к старому. Prometheus при следующем SIGHUP читает прежнее содержимое, поэтому конфиг не подхватывается. Mount директории этой проблемы не имеет.
6. UFW-правило для node_exporter
Несмотря на то что в конфиге host.docker.internal, под капотом это имя резолвится в IP моста docker0 (обычно 172.17.0.1) - то есть пакет все равно идет с docker-подсети на хост. UFW по дефолту блокирует такой трафик. Разрешаем трафик с 172.16.0.0/12 (все стандартные docker-подсети) на порт 9100 хоста:
sudo ufw allow from 172.16.0.0/12 to any port 9100 proto tcp
sudo ufw reload
Без этого правила Prometheus вернет context deadline exceeded, так как не сможет получить метрики.
7. Запуск
cd ~/services/observability
docker compose up -d
docker compose ps
Все четыре контейнера должны быть running. Проверим таргеты:
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {scrapeUrl, health}'
Вывод:
{
"scrapeUrl": "http://cadvisor:8080/metrics",
"health": "up"
}
{
"scrapeUrl": "http://host.docker.internal:9100/metrics",
"health": "up"
}
{
"scrapeUrl": "http://localhost:9090/metrics",
"health": "up"
}
Все три должны быть up. Если node - down, проверьте UFW из раздела 6 и что в docker-compose.yml из раздела 5 для node_exporter указан network_mode: host.
8. Настройка домена Grafana в Caddy
Чтобы дашборды с нашими метриками были доступны в браузере, Caddy нужно знать, куда проксировать запросы с адреса grafana.example.com. Настроим проксирование на наш localhost:3000, куда мапится порт 3000 контейнера с Grafana из раздела 5. В /etc/caddy/Caddyfile нужно добавить:
grafana.example.com {
reverse_proxy localhost:3000
}
Дальше валидируем конфиг и делаем reload сервиса:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Caddy сам получит TLS-сертификат (кому интересно, как он это делает: https://caddyserver.com/docs/automatic-https). Теперь Grafana доступна по https://grafana.example.com.
9. Защита Grafana: права на Caddyfile, basic_auth, fail2ban
Опциональный шаг.
В разделе 10 мы зайдем в Grafana под ее собственным admin-логином. Перед этим стоит добавить еще один слой защиты на уровне реверс-прокси: тогда брутфорсить логин Grafana через интернет не получится - запрос не дойдет до Grafana, пока не пройдена авторизация на Caddy.
Алгоритм:
- Закрываем права на Caddyfile, чтобы его могли читать только root и сам Caddy.
- Добавляем basic_auth в конфиг Caddy для целевого сервиса, который смотрит наружу - в нашем случае Grafana. Попытки логина будут видны в логах.
- Через fail2ban настраиваем бан IP при превышении порога неудачных попыток - как это делали в первом гайде для ssh.
9.1 Права на Caddyfile
Проверим, что Caddy запускается под своими юзером и группой:
systemctl cat caddy | grep -E '^(User|Group)='
Должно быть:
User=caddy
Group=caddy
Если у вас другие юзер и группа - не выполняем шаги из этого подраздела, иначе сервис упадет при перезапуске или при перепрочтении конфига.
Теперь, если файл доступен на чтение всем (644 root:root), как здесь:
ls -l /etc/caddy/Caddyfile
# -rw-r--r-- 1 root root
То меняем владельца группы на caddy и закрываем чтение для остальных:
sudo chown root:caddy /etc/caddy/Caddyfile
sudo chmod 640 /etc/caddy/Caddyfile
Будет так:
ls -l /etc/caddy/Caddyfile
# -rw-r----- 1 root caddy
Теперь читать файл могут только root и сервис caddy (по группе), писать может только root. Любой другой юзер на VPS получит Permission denied. Делаем релоад и смотрим статус (должно быть Active: active (running)):
sudo systemctl reload caddy
sudo systemctl status caddy9.2 basic_auth для Grafana (и любого публичного сервиса с ограниченным доступом)
basic_auth - это доп. слой авторизации на уровне нашего реверс-прокси. До сервиса запрос не дойдет, пока не пройдена эта авторизация.
Нам нужно сгенерировать хеш, который затем положим в Caddyfile:
caddy hash-password
Задаем и подтверждаем пароль. Сохраняем его куда-нибудь себе. Команда вернет хеш - его нужно скопировать в /etc/caddy/Caddyfile в блок grafana.example.com -> basic_auth:
grafana.example.com {
reverse_proxy localhost:3000
basic_auth {
<USER> <HASH>
}
}
В конфиге выше <USER> - это ваш логин для basic_auth, <HASH> - хеш пароля.
Валидируем и перезагружаем:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Открываем https://grafana.example.com (лучше в инкогнито). Браузер спросит логин и пароль до загрузки Grafana.
Раз в несколько месяцев или при подозрении на утечку стоит менять basic_auth пароль. Шаги те же:
- Генерим новый хеш:
caddy hash-password - Меняем хеш в
/etc/caddy/Caddyfile - Релоадим сервис:
sudo systemctl reload caddy
9.3 fail2ban для защиты от перебора basic_auth
Если помните первую часть, то fail2ban - это сервис, который читает логи и банит IP по определенным параметрам. Сейчас мы настроим правило (jail) для basic_auth Caddy.
Для начала включим access-логи Caddy. Access-лог - это запись об HTTP-запросе (IP клиента, URL, статус ответа и т.д.). Caddy по умолчанию их не пишет, поэтому включаем вручную - читать эти логи будет fail2ban.
Добавляем в /etc/caddy/Caddyfile секцию log:
grafana.example.com {
reverse_proxy localhost:3000
basic_auth {
admin $2a$14$qwerty...
}
log {
output file /var/log/caddy/grafana-access.log {
roll_size 10mb
roll_keep 5
}
format json
}
}
roll_size 10mb- ротация логов при превышении 10 МБ. Без ротации логи съедят весь диск.roll_keep 5- количество архивов при ротации.
Валидируем конфиг:
sudo caddy validate --config /etc/caddy/Caddyfile
Создаем директорию для логов из конфига выше, выдаем права и релоадим, чекаем статус (ожидается Active: active (running)):
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddy
sudo systemctl reload caddy
sudo systemctl status caddy
Зайдем на https://grafana.example.com, введем заведомо неправильный пароль. В логе будет запись с status: 401:
sudo tail /var/log/caddy/grafana-access.log
Создаем фильтр /etc/fail2ban/filter.d/caddy-grafana-auth.conf:
[Definition]
failregex = ^.*"remote_ip":"<HOST>".*"status":401.*$
datepattern = "ts":{EPOCH}
ignoreregex =
failregex- регулярка, которая ловит строки соstatus:401и вынимает из них IP клиента.<HOST>- плейсхолдер fail2ban для IP-кандидата на бан.datepattern- паттерн для извлечения времени фейла из строки лога.{EPOCH}- встроенный плейсхолдер fail2ban для unix timestamp. Фигурные скобки обязательны - без них fail2ban не распознает шаблон.
Проверим, что фильтр действительно ловит 401-е ошибки и распознает даты:
sudo fail2ban-regex -v /var/log/caddy/grafana-access.log /etc/fail2ban/filter.d/caddy-grafana-auth.conf | head -60
В выводе будет что-то вроде:
Running tests
=============
Use failregex filter file : caddy-grafana-auth, basedir: /etc/fail2ban
Use datepattern : "ts":{EPOCH} : "ts":{EPOCH}
Use log file : /var/log/caddy/grafana-access.log
Use encoding : UTF-8
Results
=======
Failregex: 34 total
|- #) [# of hits] regular expression
| 1) [34] ^.*"remote_ip":"<HOST>".*"status":401.*$
| <IP> Sun May 10 01:39:10 2026
| <IP> Sun May 10 01:39:14 2026
`-
Ignoreregex: 0 total
Date template hits:
|- [# of hits] date format
| [67] "ts":{EPOCH}
`-
Lines: 67 lines, 0 ignored, 34 matched, 33 missed
Как понять, что парсинг логов работает:
Failregex: N total- сколько строк с 401 нашлось в логе. Под каждым правилом fail2ban печатает найденный IP и распарсенную дату.Date template hits:для"ts":{EPOCH}больше нуля - значит, fail2ban распознал формат и распарсил временные метки из лога.
Создаем jail для Grafana-логов на 401: sudo nano /etc/fail2ban/jail.d/caddy-grafana-auth.conf
Содержимое:
[caddy-grafana-auth]
enabled = true
filter = caddy-grafana-auth
logpath = /var/log/caddy/grafana-access.log
maxretry = 5
findtime = 10m
bantime = 1h
banaction = ufw
ignoreip = 127.0.0.1/8 ::1
findtime = 10m- при параметреmaxretry = 5читаем как “если в течение 10 минут было 5 неудачных попыток - баним IP”bantime = 1h- бан на 1 час.banaction = ufw- правило deny from IP через UFW.ignoreip = 127.0.0.1/8 ::1- не банить локалхост, чтобы случайно не заблокировать себя при локальных тестах. Сюда же стоит добавить tailnet-диапазон (100.64.0.0/10), когда появится headscale.
Сначала перезагружаем fail2ban, чтобы подхватился новый jail, и убеждаемся, что он активен:
sudo systemctl restart fail2ban
sudo fail2ban-client status caddy-grafana-auth
Теперь проверяем бан: открываем grafana.example.com (лучше в инкогнито) и вводим 5 раз неверный пароль. На шестую попытку браузер не откроет страницу вообще. Смотрим примененное правило:
sudo ufw status numbered
В UFW будет видно примененное правило by Fail2Ban after 5 attempts against caddy-grafana-auth:
Status for the jail: caddy-grafana-auth
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- File list: /var/log/caddy/grafana-access.log
`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: <IP>
Status: active
To Action From
-- ------ ----
[ 1] Anywhere REJECT IN <IP> # by Fail2Ban after 5 attempts against caddy-grafana-auth
[ 2] 22/tcp ALLOW IN Anywhere
[ 3] 80/tcp ALLOW IN Anywhere
[ 4] 443/tcp ALLOW IN Anywhere
[ 5] 9100/tcp ALLOW IN 172.16.0.0/12
[ 6] 22/tcp (v6) ALLOW IN Anywhere (v6)
[ 7] 80/tcp (v6) ALLOW IN Anywhere (v6)
[ 8] 443/tcp (v6) ALLOW IN Anywhere (v6)
Чтобы не ждать, можно разбанить себя:
sudo fail2ban-client unban <IP>10. Подключение Prometheus к Grafana
Заходим в Grafana по https://grafana.example.com. Если делали раздел 9 - сначала браузер спросит basic_auth логин (от Caddy), затем откроется страница логина самой Grafana. Логинимся под admin и паролем changeme из .env (раздел 5), сразу меняем пароль. Новый пароль Grafana хранит в своем volume (grafana_data); GF_SECURITY_ADMIN_PASSWORD из .env применяется только при первой инициализации пустого volume, так что менять его в .env потом не нужно.
Добавляем data-source: Connections -> Data sources -> Add new data source -> Prometheus.
Connection -> Prometheus server URL: http://prometheus:9090 (Grafana и Prometheus в одной compose-сети, общаются по DNS-имени контейнера).
Остальное по дефолту. Save & Test -> “Successfully queried the Prometheus API”.
11. Импорт дашбордов
Dashboards -> New -> Import
Готовые дашборды: https://grafana.com/grafana/dashboards/
Достаточно просто вставить ID -> Load.
У меня такие:
- 1860 - Node Exporter Full
- 193 - метрики Docker (сервис cAdvisor)
Но, естественно, можно рисовать свои. После импорта ждем минут 15, чтобы накопились данные.
Часть панелей дашборда 1860 (systemd-юниты, процессы) останется пустой - под них node_exporter нужно запускать с флагами --collector.systemd и --collector.processes, я их не включал.
12. Версионирование конфигов в Git
(Настраивали Git в первой части)
Конфиги кладем в git, состояние уже хранится в docker volumes (в первой части мы настроили бэкапы DO, их хватит):
cd ~/services/observability
git init
echo "*.log" > .gitignore
echo ".env" >> .gitignore # секреты в env не коммитим
git add .
git status
git commit -m "Initial observability setup"
Можно запушить в приватную репу. Тогда при пересоздании VPS достаточно git clone && docker compose up -d.
13. Памятка для добавления нового сервиса
13.1 Базовые шаги
Детали см. в разделах 4-5.
- Директория
~/services/<имя>со своимdocker-compose.yml. - Сервис слушает только
127.0.0.1:port. Никогда не0.0.0.0. - В
docker-compose.ymlобязательно:
restart: unless-stoppedmem_limitcontainer_name
- Конфиги трекаются Git.
13.2 Если сервис должен быть доступен наружу
Детали см. в разделе 8.
- У DNS-провайдера задана A-запись
<имя>.example.com -> IP_VPS. - Проверяем, что запись резолвится:
dig +short <имя>.example.comвозвращаетIP_VPS. Это обязательно сделать до запуска Caddy - иначе Let’s Encrypt не сможет пройти ACME challenge и не выдаст сертификат. - Caddy: в
/etc/caddy/Caddyfileнастроено проксированиеreverse_proxy localhost:<port>. - Проверяем валидность конфига и перечитываем:
sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy.
13.2.1 Если у сервиса есть UI-админка или дашборд
Детали см. в разделе 9.
Закрываем за basic_auth Caddy:
- Если еще не сделано на первом сервисе - закрыть права на Caddyfile:
sudo chown root:caddy /etc/caddy/Caddyfile && sudo chmod 640 /etc/caddy/Caddyfile(раздел 9.1). Это разовая настройка для всего файла, не на каждый сервис. - Сгенерировать хеш пароля:
caddy hash-password. - Добавить в Caddyfile в блок сервиса:
basic_auth { admin <hash> }. - Включить access-лог в тот же блок:
log { output file /var/log/caddy/<имя>-access.log { roll_size 10mb roll_keep 5 } format json }. Если директория/var/log/caddyеще не создана -sudo mkdir -p /var/log/caddy && sudo chown -R caddy:caddy /var/log/caddy. - Создать фильтр и jail в fail2ban по образцу
caddy-grafana-authиз раздела 9.3, поменяв путь к логу.
13.3 Если сервис не должен смотреть наружу постоянно, но при необходимости нужно подключиться
Доступ через SSH-туннель: ssh -L <local_port>:localhost:<port> dovps (где dovps - SSH-alias из ~/.ssh/config, либо <user>@<IP_VPS>).
После запуска команды SSH-сессия остается открытой - туннель живет, пока мы ее не закроем. В браузере открываем http://localhost:<local_port>.
Пример: подключиться напрямую к Prometheus, чтобы посмотреть Status -> Targets, проверить up{} через PromQL или дернуть сырой /metrics:
ssh -L 9090:localhost:9090 dovps
# в браузере: http://localhost:9090
Это работает, потому что у Prometheus в compose ports: "127.0.0.1:9090:9090" - порт смаплен на loopback хоста. Снаружи он закрыт (UFW не пускает), но SSH, зайдя на VPS, может постучать туда от имени самой машины.
13.4 Если хотите собирать метрики с сервиса через Prometheus
Детали см. в разделах 4-5.
- Добавить в
~/services/observability/prometheus/prometheus.yml:
- job_name: '<имя>'
static_configs:
- targets: ['<адрес>:<port>']
Адрес зависит от того, в какой docker-сети будет жить сервис:
- Если в той же compose-сети, что и Prometheus, то по DNS-имени контейнера:
<имя>:<port>. - Если в другой сети, то через
host.docker.internal:<host_port>(порт, замапленный на хост) и правило в UFW:sudo ufw allow from 172.16.0.0/12 to any port <host_port> proto tcp. У Prometheus вdocker-compose.ymlдолжен бытьextra_hosts: ["host.docker.internal:host-gateway"](он уже там из раздела 5).
- Перечитаем конфиг Prometheus без рестарта контейнера:
docker compose exec prometheus kill -HUP 1
Команда посылает SIGHUP процессу с PID 1 внутри контейнера Prometheus - это его собственный сигнал на reload конфига.
13.5 Чеклист для нового сервиса
-
Директория
~/services/<имя>/сdocker-compose.yml -
restart: unless-stopped,mem_limit,container_nameдля каждого контейнера -
Сервис доступен только на
127.0.0.1:port -
DNS-запись добавлена и резолвится (
dig +short <имя>.example.com->IP_VPS) -
Блок в Caddyfile,
caddy validateпроходит, reload отработал -
HTTPS работает на поддомене сервиса:
curl -I https://<имя>.example.comотдаетHTTP/2 200 -
Если есть admin-UI:
basic_auth+ access-лог + fail2ban jail -
Если есть Prometheus-эндпоинт: добавлен в
prometheus.yml, SIGHUP отправлен - Конфиги закоммичены в git