Самостоятельный ntfy: свой push-сервер уведомлений
Как поднять свой ntfy-сервер для push-уведомлений: Caddy reverse proxy, токены доступа, интеграция с AI-агентом через webhooks. Мониторинг, алерты и ловушка с бесконечным циклом.
Самостоятельный ntfy: свой push-сервер уведомлений
На моём сервере крутится AI-агент (Hermes), мониторинг, блог, и куча мелких сервисов. Мне нужно было получать уведомления от всего этого хозяйства на телефон — не через Telegram Bot API (с его rate limit и зависимостью от серверов Telegram), а через свой канал, который я контролирую.
Решение — ntfy: лёгкий Go-сервер, HTTP pub-sub, Android/iOS приложения, и zero vendor lock-in. Вот как я его поднял и к чему пришёл.
Установка
На Debian/Ubuntu всё предельно просто:
apt-get install -y ntfy
Пакет создаёт пользователя _ntfy, systemd-сервис и конфиг по умолчанию. Но «по умолчанию» — это listen на :80 и без авторизации. Для production нужно поменять.
Конфигурация
Файл /etc/ntfy/server.yml:
base-url: "https://ntfy.example.com"
listen-http: "127.0.0.1:8080"
behind-proxy: true
cache-file: /var/cache/ntfy/cache.db
cache-duration: "24h"
auth-file: /var/lib/ntfy/user.db
auth-default-access: "deny-all"
log-level: info
Разберу ключевые моменты:
listen-http: "127.0.0.1:8080" — слушаем только на localhost. Весь внешний трафик идёт через reverse proxy. Если поставить 0.0.0.0:80, ntfy будет доступен напрямую, минуя SSL и rate limiting.
behind-proxy: true — обязательно, если вы за Caddy/nginx. Без этого ntfy не увидит реальные IP-адреса подписчиков (будет видеть 127.0.0.1), и rate limiting не будет работать корректно.
auth-default-access: "deny-all" — по умолчанию всё закрыто. Без этого любой, кто угадает имя топика, может читать и писать в него. Для публичного ntfy.sh это нормально, для self-hosted — нет.
Питфолл: не пишите конфиг через cat > /etc/ntfy/server.yml из терминала — некоторые security-сканеры блокируют heredoc в bash. Используйте Python:
import pathlib
pathlib.Path('/etc/ntfy/server.yml').write_text(config_yaml)
Права на директории
Пакетный _ntfy пользователь должен иметь права на запись в /var/cache/ntfy и /var/lib/ntfy:
mkdir -p /var/cache/ntfy /var/lib/ntfy
chown _ntfy:_ntfy /var/cache/ntfy /var/lib/ntfy
Без этого сервис упадёт с "unable to open database file". Классика.
Caddy reverse proxy
У меня Caddy слушает на :80 (Cloudflare handles SSL termination снаружи). Конфиг для ntfy:
@ntfy host ntfy.example.com
handle @ntfy {
reverse_proxy localhost:8080
}
Два нюанса:
- WebSocket — ntfy использует WebSocket для real-time подписок. Caddy проксирует его автоматически, но если вы за nginx, нужно явно прописать
UpgradeиConnectionзаголовки. - Cloudflare SSL:Flexible — Cloudflare terminates SSL на своём edge, а до origin идёт plain HTTP. Поэтому Caddy слушает на
:80, а не:443. Если поставить SSL:Full, нужно ещё и сертификат на origin настраивать — для self-hosted хобби-проекта это overkill.
Пользователи и токены
Создаём первого пользователя:
ntfy user add --role=admin admin
Пароль запрашивается интерактивно. После этого все запросы требуют авторизации (из-за deny-all).
Для программного доступа (скрипты, AI-агент) лучше использовать токены, а не логин/пароль:
ntfy token add admin
Токен выглядит как tk_abc123.... Передаётся в заголовке Authorization: Bearer tk_abc123....
Публикация уведомлений
Отправить сообщение — один HTTP POST:
curl -u admin:password \
-d "Сервер перегрелся! CPU 95%" \
https://ntfy.example.com/monitoring
Или с токеном:
curl -H "Authorization: Bearer tk_abc123..." \
-d "Сервер перегрелся!" \
https://ntfy.example.com/monitoring
С заголовками для кастомизации:
curl -H "Authorization: Bearer tk_abc123..." \
-H "Title: Мониторинг" \
-H "Priority: high" \
-H "Tags: warning,fire" \
-d "CPU 95%, RAM 90%" \
https://ntfy.example.com/monitoring
Подписка на уведомления
CLI:
ntfy subscribe ntfy.example.com/monitoring
HTTP (long-polling):
curl -s ntfy.example.com/monitoring/json
WebSocket (для интеграций):
const ws = new WebSocket('wss://ntfy.example.com/monitoring/ws');
ws.onmessage = (e) => console.log(JSON.parse(e.data));
Android/iOS приложение — просто добавляете сервер https://ntfy.example.com и подписываетесь на топики.
Интеграция с Hermes: webhooks
Самое интересное — подключить ntfy к AI-агенту. У Hermes есть webhook-система, и nfy поддерживает actions — автоматические HTTP-запросы при получении сообщения.
В /etc/ntfy/server.yml:
actions:
- action: "webhook"
label: "Forward to Hermes"
url: "http://localhost:8644/webhooks/ntfy"
headers:
Authorization: "Bearer my-hermes-secret"
body: |
{
"topic": "{{ .Topic }}",
"message": "{{ .Message }}",
"title": "{{ .Title }}",
"sender": "{{ .Sender }}",
"time": "{{ .Time }}"
}
topic: "hermes"
Теперь каждое сообщение в топик hermes автоматически пересылается в Hermes API. Агент может обработать уведомление и ответить.
Ловушка: если Hermes отвечает в тот же топик hermes, ntfy снова вызывает webhook, Hermes снова отвечает — бесконечный цикл. Решение: отвечать в другой топик (например, hermes-responses), или фильтровать по sender/headers в обработчике.
Реальные кейсы
Мониторинг сервера
Cron-скрипт каждые 5 минут проверяет CPU, RAM, диск. Если значение порог — отправляем HTTP POST в ntfy. Приоритет high, тег warning — и на телефоне сразу видно, что проблема. Скрипт занимает 5 строк bash, инициализация — один curl.
Алёрты от AI-агента
Hermes запускает cron-задачу (ежедневный брифинг), и результат шлёт в ntfy:
hermes cron job → Hermes API → POST ntfy.example.com/hermes
Пользователь видит уведомление на телефоне, открывает — там саммари новостей, задач, статус сервисов.
Уведомления о деплоях
CI/CD pipeline (или простой скрипт) шлёт статус деплоя: тег white_check_mark для успеха, x для провала, приоритет high для критичных ошибок. Один POST-запрос — и вы видите результат на телефоне, не открывая CI.
Почему не Telegram
Telegram Bot API — отличный вариант, и я его тоже использую. Но у self-hosted ntfy есть преимущества:
- Нет rate limit от Telegram (30 сообщений/сек на чат)
- Нет зависимости от серверов Telegram (они иногда падают)
- Полный контроль над данными (уведомления не проходят через Telegram)
- Нет необходимости в Bot Token и chat ID — просто HTTP POST
- WebSocket подписки встроены, без polling
Минусы: нет rich-контента (кнопки, inline keyboard), нет групповых чатов с историей, нет E2E шифрования. Для мониторинга и алертов — идеально. Для мессенджера — нет.
Безопасность: что может пойти не так
Публичные топики
Если auth-default-access не deny-all, любой, кто угадает имя топика, может читать из него. Топик-имена — не секреты. ntfy subscribe ntfy.example.com/my-secret-topic — это не защита, это security through obscurity.
Всегда ставьте auth-default-access: "deny-all" и создавайте пользователей с явными правами на конкретные топики:
ntfy access admin monitoring rw
ntfy access admin hermes rw
ntfy access bot-hermes hermes rw
ntfy access bot-hermes monitoring ro
Rate limiting
ntfy имеет встроенный rate limiting (по умолчанию 250 сообщений в день на visitor). Для self-hosted с 1-2 пользователями — более чем достаточно. Но если вы шлете алерты каждые 10 секунд, можете упереться лимит.
Настройка в /etc/ntfy/server.yml:
visitor-subscription-limit: 30
visitor-request-limit-burst: 60
visitor-request-limit-replenish: 5s
Логирование
ntfy пишет логи в stdout (systemd journal). Для продакшена стоит поднять логирование:
log-level: info
log-format: json
JSON-формат удобнее для парсинга в Loki/ELK, но для хобби-проекта достаточно text.
Миграция и бэкапы
ntfy хранит всё в двух файлах:
/var/cache/ntfy/cache.db— кеш сообщений (SQLite)/var/lib/ntfy/user.db— пользователи и токены (SQLite)
Для бэкапа — просто копируйте эти файлы. Для миграции на другой сервер — перенесите файлы и конфиг.
Питфолл: при обновлении ntfy через apt, конфиг /etc/ntfy/server.yml может быть перезаписан. Используйте dpkg --force-confold или бэкапите конфиг отдельно.
Траблшутинг
ntfy не стартует: "unable to open database file"
Самая частая проблема. Проверьте:
ls -la /var/cache/ntfy /var/lib/ntfy
# Должны принадлежать _ntfy:_ntfy
journalctl -u ntfy --no-pager -n 20
# Смотрим последние логи
Уведомления не доходят через Cloudflare
Проверьте, что Cloudflare не кеширует API-ответы. ntfy API endpoints (/v1/...) не должны кешироваться. В Dashboard: Caching → Configuration → Cache Level: Bypass для ntfy.example.com/v1/*.
WebSocket не подключается
За Cloudflare WebSocket работает, но нужно убедиться, что:
behind-proxy: trueв конфиге ntfy- Cloudflare не блокирует WebSocket (на Free плане не блокирует)
- Caddy/nginx проксирует Upgrade заголовки
"403 Forbidden" при публикации
Пользователь не имеет доступа к топику. Проверьте:
ntfy access LIST # Кто имеет доступ к чему
ntfy access admin my-topic rw # Дать доступ
Альтернативы
Gotify
Похож на ntfy, но написан на Go с другим API. Меньше комьюнити, меньше приложений. Если уже выбрали ntfy — нет смысла менять.
Apprise
Python-библиотека для отправки уведомлений в 80+ сервисов (Telegram, Slack, Discord, ntfy, и т.д.). Хороша как unified sender, но не как сервер. Можно комбинировать: Apprise → ntfy → телефон.
Shoutrrr
Go-альтернатива Apprise. Тот же подход — единый интерфейс для разных notification backends.
Telegram Bot API
Самый очевидный вариант, и я его тоже использую. Но ntfy выигрывает для self-hosted: нет rate limit от Telegram, нет зависимости от серверов Telegram, полный контроль над данными. Telegram — для мессенджерного опыта. ntfy — для infrastructure alerts.
Ресурсы
На типичном VPS ntfy потребляет ~18 МБ RAM и практически не грузит CPU. За 4 часа работы — 51 сообщение опубликовано, 3 подписчика, 4 активных топика. Это капля в море.
Для сравнения: Prometheus + Alertmanager жрут ~200 МБ. Grafana — ещё ~150 МБ. ntfy с webhook-интеграцией покрывает 80% кейсов мониторинга для хобби-проекта без всей этой инфраструктуры.
Итого
ntfy — это быстро: от apt install до работающих push-уведомлений — минимум настроек. Go-бинарник, systemd-сервис, HTTP API. Ставите, настраиваете Caddy, создаёте пользователя — и у вас свой notification backend, который не зависит ни от кого.
Для self-hosted AI-агента — это must-have. Мониторинг, алерты, уведомления о задачах — всё через один простой протокол.