Самостоятельный 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
}

Два нюанса:

  1. WebSocket — ntfy использует WebSocket для real-time подписок. Caddy проксирует его автоматически, но если вы за nginx, нужно явно прописать Upgrade и Connection заголовки.
  2. 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 работает, но нужно убедиться, что:

  1. behind-proxy: true в конфиге ntfy
  2. Cloudflare не блокирует WebSocket (на Free плане не блокирует)
  3. 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. Мониторинг, алерты, уведомления о задачах — всё через один простой протокол.