[{"data":1,"prerenderedAt":1332},["ShallowReactive",2],{"article-self-hosted-ntfy-notifications":3},{"id":4,"title":5,"body":6,"date":1318,"description":1319,"extension":1320,"meta":1321,"navigation":1117,"path":1322,"readingTime":1323,"seo":1324,"stem":1325,"tags":1326,"__hash__":1331},"articles\u002Fself-hosted-ntfy-notifications.md","Самостоятельный ntfy: свой push-сервер уведомлений",{"type":7,"value":8,"toc":1282},"minimark",[9,13,17,28,33,36,66,77,81,88,185,188,201,213,221,231,248,252,265,296,303,307,313,321,324,354,358,361,382,389,392,408,419,423,426,460,463,493,496,552,556,559,574,577,592,595,612,619,623,626,631,757,764,777,781,786,799,803,806,812,815,819,833,837,840,873,876,880,884,897,903,963,967,970,975,1010,1014,1017,1040,1043,1047,1050,1064,1067,1079,1083,1087,1090,1143,1147,1157,1161,1164,1177,1181,1184,1218,1222,1226,1229,1233,1236,1240,1243,1247,1250,1254,1261,1264,1268,1275,1278],[10,11,5],"h1",{"id":12},"самостоятельный-ntfy-свой-push-сервер-уведомлений",[14,15,16],"p",{},"На моём сервере крутится AI-агент (Hermes), мониторинг, блог, и куча мелких сервисов. Мне нужно было получать уведомления от всего этого хозяйства на телефон — не через Telegram Bot API (с его rate limit и зависимостью от серверов Telegram), а через свой канал, который я контролирую.",[14,18,19,20,27],{},"Решение — ",[21,22,26],"a",{"href":23,"rel":24},"https:\u002F\u002Fntfy.sh",[25],"nofollow","ntfy",": лёгкий Go-сервер, HTTP pub-sub, Android\u002FiOS приложения, и zero vendor lock-in. Вот как я его поднял и к чему пришёл.",[29,30,32],"h2",{"id":31},"установка","Установка",[14,34,35],{},"На Debian\u002FUbuntu всё предельно просто:",[37,38,43],"pre",{"className":39,"code":40,"language":41,"meta":42,"style":42},"language-bash shiki shiki-themes github-dark","apt-get install -y ntfy\n","bash","",[44,45,46],"code",{"__ignoreMap":42},[47,48,51,55,59,63],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"svObZ","apt-get",[47,56,58],{"class":57},"sU2Wk"," install",[47,60,62],{"class":61},"sDLfK"," -y",[47,64,65],{"class":57}," ntfy\n",[14,67,68,69,72,73,76],{},"Пакет создаёт пользователя ",[44,70,71],{},"_ntfy",", systemd-сервис и конфиг по умолчанию. Но «по умолчанию» — это listen на ",[44,74,75],{},":80"," и без авторизации. Для production нужно поменять.",[29,78,80],{"id":79},"конфигурация","Конфигурация",[14,82,83,84,87],{},"Файл ",[44,85,86],{},"\u002Fetc\u002Fntfy\u002Fserver.yml",":",[37,89,93],{"className":90,"code":91,"language":92,"meta":42,"style":42},"language-yaml shiki shiki-themes github-dark","base-url: \"https:\u002F\u002Fntfy.example.com\"\nlisten-http: \"127.0.0.1:8080\"\nbehind-proxy: true\ncache-file: \u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db\ncache-duration: \"24h\"\nauth-file: \u002Fvar\u002Flib\u002Fntfy\u002Fuser.db\nauth-default-access: \"deny-all\"\nlog-level: info\n","yaml",[44,94,95,108,119,130,141,152,163,174],{"__ignoreMap":42},[47,96,97,101,105],{"class":49,"line":50},[47,98,100],{"class":99},"s4JwU","base-url",[47,102,104],{"class":103},"s95oV",": ",[47,106,107],{"class":57},"\"https:\u002F\u002Fntfy.example.com\"\n",[47,109,111,114,116],{"class":49,"line":110},2,[47,112,113],{"class":99},"listen-http",[47,115,104],{"class":103},[47,117,118],{"class":57},"\"127.0.0.1:8080\"\n",[47,120,122,125,127],{"class":49,"line":121},3,[47,123,124],{"class":99},"behind-proxy",[47,126,104],{"class":103},[47,128,129],{"class":61},"true\n",[47,131,133,136,138],{"class":49,"line":132},4,[47,134,135],{"class":99},"cache-file",[47,137,104],{"class":103},[47,139,140],{"class":57},"\u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db\n",[47,142,144,147,149],{"class":49,"line":143},5,[47,145,146],{"class":99},"cache-duration",[47,148,104],{"class":103},[47,150,151],{"class":57},"\"24h\"\n",[47,153,155,158,160],{"class":49,"line":154},6,[47,156,157],{"class":99},"auth-file",[47,159,104],{"class":103},[47,161,162],{"class":57},"\u002Fvar\u002Flib\u002Fntfy\u002Fuser.db\n",[47,164,166,169,171],{"class":49,"line":165},7,[47,167,168],{"class":99},"auth-default-access",[47,170,104],{"class":103},[47,172,173],{"class":57},"\"deny-all\"\n",[47,175,177,180,182],{"class":49,"line":176},8,[47,178,179],{"class":99},"log-level",[47,181,104],{"class":103},[47,183,184],{"class":57},"info\n",[14,186,187],{},"Разберу ключевые моменты:",[14,189,190,196,197,200],{},[191,192,193],"strong",{},[44,194,195],{},"listen-http: \"127.0.0.1:8080\""," — слушаем только на localhost. Весь внешний трафик идёт через reverse proxy. Если поставить ",[44,198,199],{},"0.0.0.0:80",", ntfy будет доступен напрямую, минуя SSL и rate limiting.",[14,202,203,208,209,212],{},[191,204,205],{},[44,206,207],{},"behind-proxy: true"," — обязательно, если вы за Caddy\u002Fnginx. Без этого ntfy не увидит реальные IP-адреса подписчиков (будет видеть ",[44,210,211],{},"127.0.0.1","), и rate limiting не будет работать корректно.",[14,214,215,220],{},[191,216,217],{},[44,218,219],{},"auth-default-access: \"deny-all\""," — по умолчанию всё закрыто. Без этого любой, кто угадает имя топика, может читать и писать в него. Для публичного ntfy.sh это нормально, для self-hosted — нет.",[14,222,223,226,227,230],{},[191,224,225],{},"Питфолл",": не пишите конфиг через ",[44,228,229],{},"cat > \u002Fetc\u002Fntfy\u002Fserver.yml"," из терминала — некоторые security-сканеры блокируют heredoc в bash. Используйте Python:",[37,232,236],{"className":233,"code":234,"language":235,"meta":42,"style":42},"language-python shiki shiki-themes github-dark","import pathlib\npathlib.Path('\u002Fetc\u002Fntfy\u002Fserver.yml').write_text(config_yaml)\n","python",[44,237,238,243],{"__ignoreMap":42},[47,239,240],{"class":49,"line":50},[47,241,242],{},"import pathlib\n",[47,244,245],{"class":49,"line":110},[47,246,247],{},"pathlib.Path('\u002Fetc\u002Fntfy\u002Fserver.yml').write_text(config_yaml)\n",[29,249,251],{"id":250},"права-на-директории","Права на директории",[14,253,254,255,257,258,261,262,87],{},"Пакетный ",[44,256,71],{}," пользователь должен иметь права на запись в ",[44,259,260],{},"\u002Fvar\u002Fcache\u002Fntfy"," и ",[44,263,264],{},"\u002Fvar\u002Flib\u002Fntfy",[37,266,268],{"className":39,"code":267,"language":41,"meta":42,"style":42},"mkdir -p \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\nchown _ntfy:_ntfy \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\n",[44,269,270,284],{"__ignoreMap":42},[47,271,272,275,278,281],{"class":49,"line":50},[47,273,274],{"class":53},"mkdir",[47,276,277],{"class":61}," -p",[47,279,280],{"class":57}," \u002Fvar\u002Fcache\u002Fntfy",[47,282,283],{"class":57}," \u002Fvar\u002Flib\u002Fntfy\n",[47,285,286,289,292,294],{"class":49,"line":110},[47,287,288],{"class":53},"chown",[47,290,291],{"class":57}," _ntfy:_ntfy",[47,293,280],{"class":57},[47,295,283],{"class":57},[14,297,298,299,302],{},"Без этого сервис упадёт с ",[44,300,301],{},"\"unable to open database file\"",". Классика.",[29,304,306],{"id":305},"caddy-reverse-proxy","Caddy reverse proxy",[14,308,309,310,312],{},"У меня Caddy слушает на ",[44,311,75],{}," (Cloudflare handles SSL termination снаружи). Конфиг для ntfy:",[37,314,319],{"className":315,"code":317,"language":318},[316],"language-text","@ntfy host ntfy.example.com\nhandle @ntfy {\n    reverse_proxy localhost:8080\n}\n","text",[44,320,317],{"__ignoreMap":42},[14,322,323],{},"Два нюанса:",[325,326,327,341],"ol",{},[328,329,330,333,334,261,337,340],"li",{},[191,331,332],{},"WebSocket"," — ntfy использует WebSocket для real-time подписок. Caddy проксирует его автоматически, но если вы за nginx, нужно явно прописать ",[44,335,336],{},"Upgrade",[44,338,339],{},"Connection"," заголовки.",[328,342,343,346,347,349,350,353],{},[191,344,345],{},"Cloudflare SSL:Flexible"," — Cloudflare terminates SSL на своём edge, а до origin идёт plain HTTP. Поэтому Caddy слушает на ",[44,348,75],{},", а не ",[44,351,352],{},":443",". Если поставить SSL:Full, нужно ещё и сертификат на origin настраивать — для self-hosted хобби-проекта это overkill.",[29,355,357],{"id":356},"пользователи-и-токены","Пользователи и токены",[14,359,360],{},"Создаём первого пользователя:",[37,362,364],{"className":39,"code":363,"language":41,"meta":42,"style":42},"ntfy user add --role=admin admin\n",[44,365,366],{"__ignoreMap":42},[47,367,368,370,373,376,379],{"class":49,"line":50},[47,369,26],{"class":53},[47,371,372],{"class":57}," user",[47,374,375],{"class":57}," add",[47,377,378],{"class":61}," --role=admin",[47,380,381],{"class":57}," admin\n",[14,383,384,385,388],{},"Пароль запрашивается интерактивно. После этого все запросы требуют авторизации (из-за ",[44,386,387],{},"deny-all",").",[14,390,391],{},"Для программного доступа (скрипты, AI-агент) лучше использовать токены, а не логин\u002Fпароль:",[37,393,395],{"className":39,"code":394,"language":41,"meta":42,"style":42},"ntfy token add admin\n",[44,396,397],{"__ignoreMap":42},[47,398,399,401,404,406],{"class":49,"line":50},[47,400,26],{"class":53},[47,402,403],{"class":57}," token",[47,405,375],{"class":57},[47,407,381],{"class":57},[14,409,410,411,414,415,418],{},"Токен выглядит как ",[44,412,413],{},"tk_abc123...",". Передаётся в заголовке ",[44,416,417],{},"Authorization: Bearer tk_abc123...",".",[29,420,422],{"id":421},"публикация-уведомлений","Публикация уведомлений",[14,424,425],{},"Отправить сообщение — один HTTP POST:",[37,427,429],{"className":39,"code":428,"language":41,"meta":42,"style":42},"curl -u admin:password \\\n  -d \"Сервер перегрелся! CPU 95%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[44,430,431,445,455],{"__ignoreMap":42},[47,432,433,436,439,442],{"class":49,"line":50},[47,434,435],{"class":53},"curl",[47,437,438],{"class":61}," -u",[47,440,441],{"class":57}," admin:password",[47,443,444],{"class":61}," \\\n",[47,446,447,450,453],{"class":49,"line":110},[47,448,449],{"class":61},"  -d",[47,451,452],{"class":57}," \"Сервер перегрелся! CPU 95%\"",[47,454,444],{"class":61},[47,456,457],{"class":49,"line":121},[47,458,459],{"class":57},"  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[14,461,462],{},"Или с токеном:",[37,464,466],{"className":39,"code":465,"language":41,"meta":42,"style":42},"curl -H \"Authorization: Bearer tk_abc123...\" \\\n  -d \"Сервер перегрелся!\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[44,467,468,480,489],{"__ignoreMap":42},[47,469,470,472,475,478],{"class":49,"line":50},[47,471,435],{"class":53},[47,473,474],{"class":61}," -H",[47,476,477],{"class":57}," \"Authorization: Bearer tk_abc123...\"",[47,479,444],{"class":61},[47,481,482,484,487],{"class":49,"line":110},[47,483,449],{"class":61},[47,485,486],{"class":57}," \"Сервер перегрелся!\"",[47,488,444],{"class":61},[47,490,491],{"class":49,"line":121},[47,492,459],{"class":57},[14,494,495],{},"С заголовками для кастомизации:",[37,497,499],{"className":39,"code":498,"language":41,"meta":42,"style":42},"curl -H \"Authorization: Bearer tk_abc123...\" \\\n  -H \"Title: Мониторинг\" \\\n  -H \"Priority: high\" \\\n  -H \"Tags: warning,fire\" \\\n  -d \"CPU 95%, RAM 90%\" \\\n  https:\u002F\u002Fntfy.example.com\u002Fmonitoring\n",[44,500,501,511,521,530,539,548],{"__ignoreMap":42},[47,502,503,505,507,509],{"class":49,"line":50},[47,504,435],{"class":53},[47,506,474],{"class":61},[47,508,477],{"class":57},[47,510,444],{"class":61},[47,512,513,516,519],{"class":49,"line":110},[47,514,515],{"class":61},"  -H",[47,517,518],{"class":57}," \"Title: Мониторинг\"",[47,520,444],{"class":61},[47,522,523,525,528],{"class":49,"line":121},[47,524,515],{"class":61},[47,526,527],{"class":57}," \"Priority: high\"",[47,529,444],{"class":61},[47,531,532,534,537],{"class":49,"line":132},[47,533,515],{"class":61},[47,535,536],{"class":57}," \"Tags: warning,fire\"",[47,538,444],{"class":61},[47,540,541,543,546],{"class":49,"line":143},[47,542,449],{"class":61},[47,544,545],{"class":57}," \"CPU 95%, RAM 90%\"",[47,547,444],{"class":61},[47,549,550],{"class":49,"line":154},[47,551,459],{"class":57},[29,553,555],{"id":554},"подписка-на-уведомления","Подписка на уведомления",[14,557,558],{},"CLI:",[37,560,562],{"className":39,"code":561,"language":41,"meta":42,"style":42},"ntfy subscribe ntfy.example.com\u002Fmonitoring\n",[44,563,564],{"__ignoreMap":42},[47,565,566,568,571],{"class":49,"line":50},[47,567,26],{"class":53},[47,569,570],{"class":57}," subscribe",[47,572,573],{"class":57}," ntfy.example.com\u002Fmonitoring\n",[14,575,576],{},"HTTP (long-polling):",[37,578,580],{"className":39,"code":579,"language":41,"meta":42,"style":42},"curl -s ntfy.example.com\u002Fmonitoring\u002Fjson\n",[44,581,582],{"__ignoreMap":42},[47,583,584,586,589],{"class":49,"line":50},[47,585,435],{"class":53},[47,587,588],{"class":61}," -s",[47,590,591],{"class":57}," ntfy.example.com\u002Fmonitoring\u002Fjson\n",[14,593,594],{},"WebSocket (для интеграций):",[37,596,600],{"className":597,"code":598,"language":599,"meta":42,"style":42},"language-javascript shiki shiki-themes github-dark","const ws = new WebSocket('wss:\u002F\u002Fntfy.example.com\u002Fmonitoring\u002Fws');\nws.onmessage = (e) => console.log(JSON.parse(e.data));\n","javascript",[44,601,602,607],{"__ignoreMap":42},[47,603,604],{"class":49,"line":50},[47,605,606],{},"const ws = new WebSocket('wss:\u002F\u002Fntfy.example.com\u002Fmonitoring\u002Fws');\n",[47,608,609],{"class":49,"line":110},[47,610,611],{},"ws.onmessage = (e) => console.log(JSON.parse(e.data));\n",[14,613,614,615,618],{},"Android\u002FiOS приложение — просто добавляете сервер ",[44,616,617],{},"https:\u002F\u002Fntfy.example.com"," и подписываетесь на топики.",[29,620,622],{"id":621},"интеграция-с-hermes-webhooks","Интеграция с Hermes: webhooks",[14,624,625],{},"Самое интересное — подключить ntfy к AI-агенту. У Hermes есть webhook-система, и nfy поддерживает actions — автоматические HTTP-запросы при получении сообщения.",[14,627,628,629,87],{},"В ",[44,630,86],{},[37,632,634],{"className":90,"code":633,"language":92,"meta":42,"style":42},"actions:\n  - action: \"webhook\"\n    label: \"Forward to Hermes\"\n    url: \"http:\u002F\u002Flocalhost:8644\u002Fwebhooks\u002Fntfy\"\n    headers:\n      Authorization: \"Bearer my-hermes-secret\"\n    body: |\n      {\n        \"topic\": \"{{ .Topic }}\",\n        \"message\": \"{{ .Message }}\",\n        \"title\": \"{{ .Title }}\",\n        \"sender\": \"{{ .Sender }}\",\n        \"time\": \"{{ .Time }}\"\n      }\n    topic: \"hermes\"\n",[44,635,636,644,657,667,677,684,694,705,710,716,722,728,734,740,746],{"__ignoreMap":42},[47,637,638,641],{"class":49,"line":50},[47,639,640],{"class":99},"actions",[47,642,643],{"class":103},":\n",[47,645,646,649,652,654],{"class":49,"line":110},[47,647,648],{"class":103},"  - ",[47,650,651],{"class":99},"action",[47,653,104],{"class":103},[47,655,656],{"class":57},"\"webhook\"\n",[47,658,659,662,664],{"class":49,"line":121},[47,660,661],{"class":99},"    label",[47,663,104],{"class":103},[47,665,666],{"class":57},"\"Forward to Hermes\"\n",[47,668,669,672,674],{"class":49,"line":132},[47,670,671],{"class":99},"    url",[47,673,104],{"class":103},[47,675,676],{"class":57},"\"http:\u002F\u002Flocalhost:8644\u002Fwebhooks\u002Fntfy\"\n",[47,678,679,682],{"class":49,"line":143},[47,680,681],{"class":99},"    headers",[47,683,643],{"class":103},[47,685,686,689,691],{"class":49,"line":154},[47,687,688],{"class":99},"      Authorization",[47,690,104],{"class":103},[47,692,693],{"class":57},"\"Bearer my-hermes-secret\"\n",[47,695,696,699,701],{"class":49,"line":165},[47,697,698],{"class":99},"    body",[47,700,104],{"class":103},[47,702,704],{"class":703},"snl16","|\n",[47,706,707],{"class":49,"line":176},[47,708,709],{"class":57},"      {\n",[47,711,713],{"class":49,"line":712},9,[47,714,715],{"class":57},"        \"topic\": \"{{ .Topic }}\",\n",[47,717,719],{"class":49,"line":718},10,[47,720,721],{"class":57},"        \"message\": \"{{ .Message }}\",\n",[47,723,725],{"class":49,"line":724},11,[47,726,727],{"class":57},"        \"title\": \"{{ .Title }}\",\n",[47,729,731],{"class":49,"line":730},12,[47,732,733],{"class":57},"        \"sender\": \"{{ .Sender }}\",\n",[47,735,737],{"class":49,"line":736},13,[47,738,739],{"class":57},"        \"time\": \"{{ .Time }}\"\n",[47,741,743],{"class":49,"line":742},14,[47,744,745],{"class":57},"      }\n",[47,747,749,752,754],{"class":49,"line":748},15,[47,750,751],{"class":99},"    topic",[47,753,104],{"class":103},[47,755,756],{"class":57},"\"hermes\"\n",[14,758,759,760,763],{},"Теперь каждое сообщение в топик ",[44,761,762],{},"hermes"," автоматически пересылается в Hermes API. Агент может обработать уведомление и ответить.",[14,765,766,769,770,772,773,776],{},[191,767,768],{},"Ловушка",": если Hermes отвечает в тот же топик ",[44,771,762],{},", ntfy снова вызывает webhook, Hermes снова отвечает — бесконечный цикл. Решение: отвечать в другой топик (например, ",[44,774,775],{},"hermes-responses","), или фильтровать по sender\u002Fheaders в обработчике.",[29,778,780],{"id":779},"реальные-кейсы","Реальные кейсы",[782,783,785],"h3",{"id":784},"мониторинг-сервера","Мониторинг сервера",[14,787,788,789,792,793,796,797,418],{},"Cron-скрипт каждые 5 минут проверяет CPU, RAM, диск. Если значение порог — отправляем HTTP POST в ntfy. Приоритет ",[44,790,791],{},"high",", тег ",[44,794,795],{},"warning"," — и на телефоне сразу видно, что проблема. Скрипт занимает 5 строк bash, инициализация — один ",[44,798,435],{},[782,800,802],{"id":801},"алёрты-от-ai-агента","Алёрты от AI-агента",[14,804,805],{},"Hermes запускает cron-задачу (ежедневный брифинг), и результат шлёт в ntfy:",[37,807,810],{"className":808,"code":809,"language":318},[316],"hermes cron job → Hermes API → POST ntfy.example.com\u002Fhermes\n",[44,811,809],{"__ignoreMap":42},[14,813,814],{},"Пользователь видит уведомление на телефоне, открывает — там саммари новостей, задач, статус сервисов.",[782,816,818],{"id":817},"уведомления-о-деплоях","Уведомления о деплоях",[14,820,821,822,825,826,829,830,832],{},"CI\u002FCD pipeline (или простой скрипт) шлёт статус деплоя: тег ",[44,823,824],{},"white_check_mark"," для успеха, ",[44,827,828],{},"x"," для провала, приоритет ",[44,831,791],{}," для критичных ошибок. Один POST-запрос — и вы видите результат на телефоне, не открывая CI.",[29,834,836],{"id":835},"почему-не-telegram","Почему не Telegram",[14,838,839],{},"Telegram Bot API — отличный вариант, и я его тоже использую. Но у self-hosted ntfy есть преимущества:",[841,842,843,849,855,861,867],"ul",{},[328,844,845,848],{},[191,846,847],{},"Нет rate limit"," от Telegram (30 сообщений\u002Fсек на чат)",[328,850,851,854],{},[191,852,853],{},"Нет зависимости"," от серверов Telegram (они иногда падают)",[328,856,857,860],{},[191,858,859],{},"Полный контроль"," над данными (уведомления не проходят через Telegram)",[328,862,863,866],{},[191,864,865],{},"Нет необходимости"," в Bot Token и chat ID — просто HTTP POST",[328,868,869,872],{},[191,870,871],{},"WebSocket подписки"," встроены, без polling",[14,874,875],{},"Минусы: нет rich-контента (кнопки, inline keyboard), нет групповых чатов с историей, нет E2E шифрования. Для мониторинга и алертов — идеально. Для мессенджера — нет.",[29,877,879],{"id":878},"безопасность-что-может-пойти-не-так","Безопасность: что может пойти не так",[782,881,883],{"id":882},"публичные-топики","Публичные топики",[14,885,886,887,889,890,892,893,896],{},"Если ",[44,888,168],{}," не ",[44,891,387],{},", любой, кто угадает имя топика, может читать из него. Топик-имена — не секреты. ",[44,894,895],{},"ntfy subscribe ntfy.example.com\u002Fmy-secret-topic"," — это не защита, это security through obscurity.",[14,898,899,900,902],{},"Всегда ставьте ",[44,901,219],{}," и создавайте пользователей с явными правами на конкретные топики:",[37,904,906],{"className":39,"code":905,"language":41,"meta":42,"style":42},"ntfy access admin monitoring rw\nntfy access admin hermes rw\nntfy access bot-hermes hermes rw\nntfy access bot-hermes monitoring ro\n",[44,907,908,924,937,950],{"__ignoreMap":42},[47,909,910,912,915,918,921],{"class":49,"line":50},[47,911,26],{"class":53},[47,913,914],{"class":57}," access",[47,916,917],{"class":57}," admin",[47,919,920],{"class":57}," monitoring",[47,922,923],{"class":57}," rw\n",[47,925,926,928,930,932,935],{"class":49,"line":110},[47,927,26],{"class":53},[47,929,914],{"class":57},[47,931,917],{"class":57},[47,933,934],{"class":57}," hermes",[47,936,923],{"class":57},[47,938,939,941,943,946,948],{"class":49,"line":121},[47,940,26],{"class":53},[47,942,914],{"class":57},[47,944,945],{"class":57}," bot-hermes",[47,947,934],{"class":57},[47,949,923],{"class":57},[47,951,952,954,956,958,960],{"class":49,"line":132},[47,953,26],{"class":53},[47,955,914],{"class":57},[47,957,945],{"class":57},[47,959,920],{"class":57},[47,961,962],{"class":57}," ro\n",[782,964,966],{"id":965},"rate-limiting","Rate limiting",[14,968,969],{},"ntfy имеет встроенный rate limiting (по умолчанию 250 сообщений в день на visitor). Для self-hosted с 1-2 пользователями — более чем достаточно. Но если вы шлете алерты каждые 10 секунд, можете упереться лимит.",[14,971,972,973,87],{},"Настройка в ",[44,974,86],{},[37,976,978],{"className":90,"code":977,"language":92,"meta":42,"style":42},"visitor-subscription-limit: 30\nvisitor-request-limit-burst: 60\nvisitor-request-limit-replenish: 5s\n",[44,979,980,990,1000],{"__ignoreMap":42},[47,981,982,985,987],{"class":49,"line":50},[47,983,984],{"class":99},"visitor-subscription-limit",[47,986,104],{"class":103},[47,988,989],{"class":61},"30\n",[47,991,992,995,997],{"class":49,"line":110},[47,993,994],{"class":99},"visitor-request-limit-burst",[47,996,104],{"class":103},[47,998,999],{"class":61},"60\n",[47,1001,1002,1005,1007],{"class":49,"line":121},[47,1003,1004],{"class":99},"visitor-request-limit-replenish",[47,1006,104],{"class":103},[47,1008,1009],{"class":57},"5s\n",[782,1011,1013],{"id":1012},"логирование","Логирование",[14,1015,1016],{},"ntfy пишет логи в stdout (systemd journal). Для продакшена стоит поднять логирование:",[37,1018,1020],{"className":90,"code":1019,"language":92,"meta":42,"style":42},"log-level: info\nlog-format: json\n",[44,1021,1022,1030],{"__ignoreMap":42},[47,1023,1024,1026,1028],{"class":49,"line":50},[47,1025,179],{"class":99},[47,1027,104],{"class":103},[47,1029,184],{"class":57},[47,1031,1032,1035,1037],{"class":49,"line":110},[47,1033,1034],{"class":99},"log-format",[47,1036,104],{"class":103},[47,1038,1039],{"class":57},"json\n",[14,1041,1042],{},"JSON-формат удобнее для парсинга в Loki\u002FELK, но для хобби-проекта достаточно text.",[29,1044,1046],{"id":1045},"миграция-и-бэкапы","Миграция и бэкапы",[14,1048,1049],{},"ntfy хранит всё в двух файлах:",[841,1051,1052,1058],{},[328,1053,1054,1057],{},[44,1055,1056],{},"\u002Fvar\u002Fcache\u002Fntfy\u002Fcache.db"," — кеш сообщений (SQLite)",[328,1059,1060,1063],{},[44,1061,1062],{},"\u002Fvar\u002Flib\u002Fntfy\u002Fuser.db"," — пользователи и токены (SQLite)",[14,1065,1066],{},"Для бэкапа — просто копируйте эти файлы. Для миграции на другой сервер — перенесите файлы и конфиг.",[14,1068,1069,1071,1072,1074,1075,1078],{},[191,1070,225],{},": при обновлении ntfy через apt, конфиг ",[44,1073,86],{}," может быть перезаписан. Используйте ",[44,1076,1077],{},"dpkg --force-confold"," или бэкапите конфиг отдельно.",[29,1080,1082],{"id":1081},"траблшутинг","Траблшутинг",[782,1084,1086],{"id":1085},"ntfy-не-стартует-unable-to-open-database-file","ntfy не стартует: \"unable to open database file\"",[14,1088,1089],{},"Самая частая проблема. Проверьте:",[37,1091,1093],{"className":39,"code":1092,"language":41,"meta":42,"style":42},"ls -la \u002Fvar\u002Fcache\u002Fntfy \u002Fvar\u002Flib\u002Fntfy\n# Должны принадлежать _ntfy:_ntfy\n\njournalctl -u ntfy --no-pager -n 20\n# Смотрим последние логи\n",[44,1094,1095,1107,1113,1119,1138],{"__ignoreMap":42},[47,1096,1097,1100,1103,1105],{"class":49,"line":50},[47,1098,1099],{"class":53},"ls",[47,1101,1102],{"class":61}," -la",[47,1104,280],{"class":57},[47,1106,283],{"class":57},[47,1108,1109],{"class":49,"line":110},[47,1110,1112],{"class":1111},"sAwPA","# Должны принадлежать _ntfy:_ntfy\n",[47,1114,1115],{"class":49,"line":121},[47,1116,1118],{"emptyLinePlaceholder":1117},true,"\n",[47,1120,1121,1124,1126,1129,1132,1135],{"class":49,"line":132},[47,1122,1123],{"class":53},"journalctl",[47,1125,438],{"class":61},[47,1127,1128],{"class":57}," ntfy",[47,1130,1131],{"class":61}," --no-pager",[47,1133,1134],{"class":61}," -n",[47,1136,1137],{"class":61}," 20\n",[47,1139,1140],{"class":49,"line":143},[47,1141,1142],{"class":1111},"# Смотрим последние логи\n",[782,1144,1146],{"id":1145},"уведомления-не-доходят-через-cloudflare","Уведомления не доходят через Cloudflare",[14,1148,1149,1150,1153,1154,418],{},"Проверьте, что Cloudflare не кеширует API-ответы. ntfy API endpoints (",[44,1151,1152],{},"\u002Fv1\u002F...",") не должны кешироваться. В Dashboard: Caching → Configuration → Cache Level: Bypass для ",[44,1155,1156],{},"ntfy.example.com\u002Fv1\u002F*",[782,1158,1160],{"id":1159},"websocket-не-подключается","WebSocket не подключается",[14,1162,1163],{},"За Cloudflare WebSocket работает, но нужно убедиться, что:",[325,1165,1166,1171,1174],{},[328,1167,1168,1170],{},[44,1169,207],{}," в конфиге ntfy",[328,1172,1173],{},"Cloudflare не блокирует WebSocket (на Free плане не блокирует)",[328,1175,1176],{},"Caddy\u002Fnginx проксирует Upgrade заголовки",[782,1178,1180],{"id":1179},"_403-forbidden-при-публикации","\"403 Forbidden\" при публикации",[14,1182,1183],{},"Пользователь не имеет доступа к топику. Проверьте:",[37,1185,1187],{"className":39,"code":1186,"language":41,"meta":42,"style":42},"ntfy access LIST  # Кто имеет доступ к чему\nntfy access admin my-topic rw  # Дать доступ\n",[44,1188,1189,1201],{"__ignoreMap":42},[47,1190,1191,1193,1195,1198],{"class":49,"line":50},[47,1192,26],{"class":53},[47,1194,914],{"class":57},[47,1196,1197],{"class":57}," LIST",[47,1199,1200],{"class":1111},"  # Кто имеет доступ к чему\n",[47,1202,1203,1205,1207,1209,1212,1215],{"class":49,"line":110},[47,1204,26],{"class":53},[47,1206,914],{"class":57},[47,1208,917],{"class":57},[47,1210,1211],{"class":57}," my-topic",[47,1213,1214],{"class":57}," rw",[47,1216,1217],{"class":1111},"  # Дать доступ\n",[29,1219,1221],{"id":1220},"альтернативы","Альтернативы",[782,1223,1225],{"id":1224},"gotify","Gotify",[14,1227,1228],{},"Похож на ntfy, но написан на Go с другим API. Меньше комьюнити, меньше приложений. Если уже выбрали ntfy — нет смысла менять.",[782,1230,1232],{"id":1231},"apprise","Apprise",[14,1234,1235],{},"Python-библиотека для отправки уведомлений в 80+ сервисов (Telegram, Slack, Discord, ntfy, и т.д.). Хороша как unified sender, но не как сервер. Можно комбинировать: Apprise → ntfy → телефон.",[782,1237,1239],{"id":1238},"shoutrrr","Shoutrrr",[14,1241,1242],{},"Go-альтернатива Apprise. Тот же подход — единый интерфейс для разных notification backends.",[782,1244,1246],{"id":1245},"telegram-bot-api","Telegram Bot API",[14,1248,1249],{},"Самый очевидный вариант, и я его тоже использую. Но ntfy выигрывает для self-hosted: нет rate limit от Telegram, нет зависимости от серверов Telegram, полный контроль над данными. Telegram — для мессенджерного опыта. ntfy — для infrastructure alerts.",[29,1251,1253],{"id":1252},"ресурсы","Ресурсы",[14,1255,1256,1257,1260],{},"На типичном VPS ntfy потребляет ",[191,1258,1259],{},"~18 МБ RAM"," и практически не грузит CPU. За 4 часа работы — 51 сообщение опубликовано, 3 подписчика, 4 активных топика. Это капля в море.",[14,1262,1263],{},"Для сравнения: Prometheus + Alertmanager жрут ~200 МБ. Grafana — ещё ~150 МБ. ntfy с webhook-интеграцией покрывает 80% кейсов мониторинга для хобби-проекта без всей этой инфраструктуры.",[29,1265,1267],{"id":1266},"итого","Итого",[14,1269,1270,1271,1274],{},"ntfy — это быстро: от ",[44,1272,1273],{},"apt install"," до работающих push-уведомлений — минимум настроек. Go-бинарник, systemd-сервис, HTTP API. Ставите, настраиваете Caddy, создаёте пользователя — и у вас свой notification backend, который не зависит ни от кого.",[14,1276,1277],{},"Для self-hosted AI-агента — это must-have. Мониторинг, алерты, уведомления о задачах — всё через один простой протокол.",[1279,1280,1281],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":42,"searchDepth":110,"depth":110,"links":1283},[1284,1285,1286,1287,1288,1289,1290,1291,1292,1297,1298,1303,1304,1310,1316,1317],{"id":31,"depth":110,"text":32},{"id":79,"depth":110,"text":80},{"id":250,"depth":110,"text":251},{"id":305,"depth":110,"text":306},{"id":356,"depth":110,"text":357},{"id":421,"depth":110,"text":422},{"id":554,"depth":110,"text":555},{"id":621,"depth":110,"text":622},{"id":779,"depth":110,"text":780,"children":1293},[1294,1295,1296],{"id":784,"depth":121,"text":785},{"id":801,"depth":121,"text":802},{"id":817,"depth":121,"text":818},{"id":835,"depth":110,"text":836},{"id":878,"depth":110,"text":879,"children":1299},[1300,1301,1302],{"id":882,"depth":121,"text":883},{"id":965,"depth":121,"text":966},{"id":1012,"depth":121,"text":1013},{"id":1045,"depth":110,"text":1046},{"id":1081,"depth":110,"text":1082,"children":1305},[1306,1307,1308,1309],{"id":1085,"depth":121,"text":1086},{"id":1145,"depth":121,"text":1146},{"id":1159,"depth":121,"text":1160},{"id":1179,"depth":121,"text":1180},{"id":1220,"depth":110,"text":1221,"children":1311},[1312,1313,1314,1315],{"id":1224,"depth":121,"text":1225},{"id":1231,"depth":121,"text":1232},{"id":1238,"depth":121,"text":1239},{"id":1245,"depth":121,"text":1246},{"id":1252,"depth":110,"text":1253},{"id":1266,"depth":110,"text":1267},"2026-05-24","Как поднять свой ntfy-сервер для push-уведомлений: Caddy reverse proxy, токены доступа, интеграция с AI-агентом через webhooks. Мониторинг, алерты и ловушка с бесконечным циклом.","md",{},"\u002Fself-hosted-ntfy-notifications",null,{"title":5,"description":1319},"self-hosted-ntfy-notifications",[26,1327,1328,1329,1330,762],"self-hosted","notifications","vps","caddy","z2W-hsVdsXO6syzEh27N9CdD8rltQXg6ye-cT_hy_fQ",1779661627949]