SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor

راه اندازی سرور ntfy شخصی با Docker Compose

آموزش گام به گام میزبانی ntfy روی VPS با تنظیمات TLS و Docker Compose. یاد بگیرید چگونه با استفاده از ACL و احراز هویت، اعلان‌های cron و systemd را به صورت امن ارسال کنید.

عملکرد یک سرور ntfy خودمیزبان

یک سرور ntfy خودمیزبان، درخواست‌های HTTP POST را به اعلان‌های push روی تلفن همراه شما تبدیل می‌کند. شما با استفاده از curl پیام را منتشر می‌کنید و پیام در اپلیکیشن Android، اپلیکیشن iOS، تب مرورگر یا هر ابزار دیگری که بتواند یک اتصال HTTP را باز نگه دارد، دریافت می‌شود. هیچ کتابخانه کلاینتی برای نصب و هیچ message broker برای اجرا وجود ندارد.

ntfy پیام‌ها را بر اساس topic آدرس‌دهی می‌کند. یک topic نامی در مسیر URL است، مانند https://ntfy.example.com/alerts، و به محض اینکه کسی پیامی در آن منتشر کند، ایجاد می‌شود. در یک نصب پیش‌فرض، هر کسی که آن نام را بداند می‌تواند topic را بخواند یا در آن بنویسد؛ به همین دلیل است که مستندات خود پروژه، نام topic را به یک رمز عبور تشبیه می‌کند. این مدل برای سرویس عمومی ntfy.sh مناسب است، اما برای سروری که گزارش‌های خطای پشتیبان‌گیری شما را حمل می‌کند مناسب نیست؛ بنابراین این راهنما پیش از ارسال اولین پیام، احراز هویت را فعال می‌کند.

پیش‌نیازها برای شروع

شما به یک VPS با سیستم‌عامل Ubuntu 24.04 یا Debian 13 به همراه Docker Engine و پلاگین Compose، یک نام دامنه و مقدار بسیار کمی RAM نیاز دارید. یک رکورد A در DNS (سامانه نام دامنه) ایجاد کنید که ntfy.example.com را به آدرس IP عمومی سرور اشاره دهد، سپس پیش از انجام هر کار دیگری، از صحت resolve شدن آن اطمینان حاصل کنید.

dig +short ntfy.example.com
sudo ufw allow 80,443/tcp
sudo ufw status

dig باید IP سرور شما را نمایش دهد. اگر این دستور خروجی نداشته باشد، صدور گواهی با شکست مواجه می‌شود، زیرا مرجع صدور گواهی (CA) نام دامنه را از خارج شبکه بررسی می‌کند. پورت 80 باید باز بماند، زیرا ACME (محیط مدیریت خودکار گواهی)، پروتکل پشت Let's Encrypt، از این پورت برای چالش HTTP استفاده می‌کند. خود کانتینر ntfy هرگز نیازی به داشتن پورت عمومی ندارد.

نوشتن فایل پیکربندی ntfy

ایمیج Docker شامل فایل پیکربندی نیست، بنابراین باید یکی ایجاد کنید. تمام دستورات بعدی در این راهنما از این فایل استفاده می‌کنند. ابتدا شناسه کاربری (UID) و شناسه گروهی (GID) که کانتینر با آن اجرا می‌شود را پیدا کنید.

id -u
id -g
sudo install -d -o "$(id -u)" -g "$(id -g)" /etc/ntfy /var/cache/ntfy /var/lib/ntfy
sudo nano /etc/ntfy/server.yml
base-url: "https://ntfy.example.com"
listen-http: ":2586"
behind-proxy: true
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
enable-login: true
enable-signup: false

چهار مورد از این خطوط اهمیت ویژه‌ای دارند. base-url باید دقیقاً آدرس عمومی HTTPS باشد، زیرا ntfy لینک‌های پیوست و درخواست‌های خودِ وب‌اپلیکیشن را بر اساس آن می‌سازد؛ بنابراین مقدار نادرست باعث می‌شود وب‌اپلیکیشن بارگذاری شود اما در هر عملیاتی با خطا مواجه گردد. listen-http: ":2586" به تمام اینترفیس‌های داخل کانتینر متصل می‌شود که شاید در نگاه اول بی‌دقت به نظر برسد، اما صحیح است: کانتینر فضای نام شبکه (network namespace) اختصاصی خود را دارد، بنابراین اتصال به 127.0.0.1 در آنجا باعث می‌شود پورت از سمت میزبان (host) غیرقابل دسترس باشد و پورت منتشر شده توسط Docker هرگز متصل نشود. auth-default-access: "deny-all" کل وضعیت امنیتی را تعیین می‌کند، زیرا دسترسی خواندن و نوشتن را برای هر کسی که مجوز صریح نداشته باشد، رد می‌کند. behind-proxy: true به ntfy می‌گوید که آدرس کلاینت را از هدر X-Forwarded-For دریافت کند تا محدودیت‌های نرخ (rate limits) بازدیدکنندگان واقعی را بشمارند، نه اینکه reverse proxy را به عنوان یک کلاینت بسیار پرمشغله در نظر بگیرند.

enable-login: true به وب‌اپلیکیشن و اپلیکیشن‌های موبایل اجازه می‌دهد با رمز عبور وارد شوند. enable-signup روی false باقی می‌ماند، زیرا ایجاد حساب کاربری توسط خودِ کاربر در یک سرور خصوصی، عملاً باز گذاشتن در با مراحل اضافه است.

sudo chown "$(id -u):$(id -g)" /etc/ntfy/server.yml
sudo chmod 600 /etc/ntfy/server.yml

اجرای ntfy با Docker Compose

این محتوا را در /opt/ntfy/compose.yaml قرار دهید و 1000:1000 را با دو عدد id -u و id -g که در بالا چاپ شده‌اند، جایگزین کنید.

services:
  ntfy:
    image: binwiederhier/ntfy:v2.27.0
    container_name: ntfy
    command: serve
    user: "1000:1000"
    environment:
      - TZ=UTC
    volumes:
      - /etc/ntfy:/etc/ntfy
      - /var/cache/ntfy:/var/cache/ntfy
      - /var/lib/ntfy:/var/lib/ntfy
    ports:
      - "127.0.0.1:2586:2586"
    restart: unless-stopped
cd /opt/ntfy
sudo docker compose up -d
sudo docker compose logs ntfy
curl -s http://127.0.0.1:2586/v1/health

یک سرور سالم به {"healthy":true} پاسخ می‌دهد. دو جزئیات در آن فایل compose عمدی هستند. ایمیج به نسخه v2.27.0، یعنی آخرین نسخه منتشرشده تا اوت 2026، پین شده است و نه latest؛ زیرا با استفاده از latest، نسخه بعدی docker compose pull، نسخه سرور شما را تغییر می‌دهد و شما پس از آن از طریق changelog متوجه این تغییر می‌شوید. پورت به صورت 127.0.0.1:2586:2586 منتشر شده است، بنابراین کانتینر فقط از طریق آدرس loopback میزبان در دسترس است. اگر به جای آن 2586:2586 بنویسید، Docker قوانین فایروال خود را پیش از قوانین شما اعمال می‌کند؛ این یعنی پورت از طریق اینترنت پاسخ می‌دهد، حتی اگر ufw status بگوید که پورت بسته است.

اگر دستور curl خروجی Connection refused را چاپ کرد، لاگ کانتینر را بخوانید. خطای مجوز در /var/lib/ntfy/user.db به این معنی است که خط user: با مالک آن دایرکتوری‌ها مطابقت ندارد، بنابراین پردازش نمی‌تواند دیتابیس خود را ایجاد کند و متوقف می‌شود. راهنمای اصول Docker Compose برای VPS مالکیت volume و سیاست‌های restart را با جزئیات بیشتری پوشش می‌دهد.

استفاده از Caddy برای پیاده‌سازی TLS

Caddy به‌صورت خودکار گواهی‌ها را درخواست و تمدید می‌کند که سریع‌ترین مسیر برای دستیابی به TLS (امنیت لایه انتقال) فعال است.

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
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 chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

محتوای فایل /etc/caddy/Caddyfile را با سه خط زیر جایگزین کنید.

ntfy.example.com {
    reverse_proxy 127.0.0.1:2586
}
sudo systemctl reload caddy
curl -s https://ntfy.example.com/v1/health

مشاهده همان {"healthy":true} از طریق HTTPS به این معناست که کل مسیر به‌درستی کار می‌کند. دریافت یک 502 از سمت Caddy به این معناست که ntfy در حال گوش دادن نیست: وضعیت را با sudo ss -lntp | grep 2586 بررسی کنید. خطای گواهی معمولاً به این معناست که رکورد DNS اشتباه است یا پورت 80 مسدود شده است، و sudo journalctl -u caddy -n 50 مشخص می‌کند کدام‌یک از این موارد است.

اگر از قبل nginx را اجرا می‌کنید، تنظیمات پروکسی که در مستندات ntfy آمده است را کپی کنید: proxy_http_version 1.1، proxy_buffering off، proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for، و مقادیر read و send timeout را حداقل روی سه دقیقه تنظیم کنید. یک مشترک (subscriber) تا زمانی که در حال گوش دادن است، یک اتصال HTTP را باز نگه می‌دارد و nginx به‌صورت پیش‌فرض اتصال‌های upstream غیرفعال را پس از 60 ثانیه می‌بندد؛ در نتیجه مشترکین در یک حلقه مدام دوباره متصل می‌شوند و پیام‌هایی که در این فاصله ارسال می‌شوند، از دست می‌روند.

ایجاد کاربران و محدودسازی دسترسی به تاپیک‌ها

احراز هویت فعال است و در حال حاضر هیچ‌کس به هیچ‌چیز دسترسی ندارد؛ این دقیقاً همان هدفی است که دنبال می‌کردیم. یک حساب کاربری مدیر برای خودتان و یک حساب ماشینی برای اسکریپت‌ها ایجاد کنید. این دستورات /etc/ntfy/server.yml را از داخل کانتینر می‌خوانند، به همین دلیل است که فایل پیکربندی به صورت یک volume mount متصل شده است.

sudo docker compose exec ntfy ntfy user add --role=admin admin
sudo docker compose exec ntfy ntfy user add robot
sudo docker compose exec ntfy ntfy user list

هر کدام از این دستورات، رمز عبور را از شما می‌پرسند. یک مدیر، لیست دسترسی را نادیده می‌گیرد و می‌تواند هر تاپیکی را بخواند یا در آن بنویسد؛ بنابراین این حساب را برای خودتان و اپلیکیشن موبایل نگه دارید. robot یک کاربر عادی است که تا زمانی که به او دسترسی ندهید، هیچ مجوزی ندارد.

sudo docker compose exec ntfy ntfy access robot alerts write
sudo docker compose exec ntfy ntfy access robot "alerts_*" write
sudo docker compose exec ntfy ntfy access

هر ورودی در ACL (لیست کنترل دسترسی) شامل یک کاربر، یک تاپیک و یک سطح دسترسی است. تاپیک می‌تواند یک نام مشخص یا یک الگو باشد که در آن * با هر چیزی مطابقت دارد؛ بنابراین alerts_* شامل alerts_backup و alerts_db می‌شود و نیازی نیست برای هر هاست یک دستور جداگانه وارد کنید. سطح دسترسی write به معنای فقط انتشار (publish) است؛ بنابراین اگر توکنی از یک cron job سرقت شود، مهاجم نمی‌تواند در حالت subscribe قرار بگیرد و داده‌هایی که ارسال شده را بخواند. نام کاربری ویژه everyone تعیین می‌کند که یک بازدیدکننده احراز هویت‌نشده چه کاری می‌تواند انجام دهد؛ شما فقط زمانی از آن استفاده می‌کنید که بخواهید چیزی را عمداً عمومی کنید، مانند ntfy access everyone status read.

اسکریپت‌ها باید از توکن استفاده کنند، نه رمز عبور شما.

sudo docker compose exec ntfy ntfy token add robot

این دستور توکنی را چاپ می‌کند که با tk_ شروع می‌شود. یک توکن دقیقاً همان دسترسی‌های کاربری را به ارث می‌برد که به آن تعلق دارد؛ بنابراین این توکن فقط می‌تواند در تاپیک‌های alerts منتشر کند و هیچ کار دیگری انجام ندهد. دستور ntfy token list موارد موجود را نمایش می‌دهد و ntfy token remove یک توکن را بدون تغییر در رمز عبور کاربر، باطل می‌کند.

ارسال اولین پیام و اطمینان از عملکرد قفل

ابتدا بررسی کنید که در بسته است.

curl -s -o /dev/null -w '%{http_code}\n' -d "hello" https://ntfy.example.com/alerts

این دستور 403 را چاپ می‌کند و 403 پاسخ صحیح است: auth-default-access: "deny-all" انتشار ناشناس را نمی‌پذیرد. اکنون یک پیام واقعی ارسال کنید.

curl -H "Authorization: Bearer tk_REPLACE_WITH_YOUR_TOKEN" \
  -H "Title: Nightly backup finished" \
  -H "Priority: default" \
  -H "Tags: white_check_mark" \
  -d "42 GB copied in 11 minutes" \
  https://ntfy.example.com/alerts

سرور با پیام ذخیره‌شده در قالب JSON پاسخ می‌دهد؛ این نشان می‌دهد که پیام پذیرفته شده و نادیده گرفته نشده است. Title خط اول پررنگ است. Priority از 1 تا 5 یا با نام از min تا urgent مقدار می‌گیرد و تعیین می‌کند که آیا گوشی صدا پخش کند یا خیر. Tags در صورتی که نام با یک کد کوتاه ایموجی شناخته‌شده مطابقت داشته باشد، به ایموجی در اعلان تبدیل می‌شوند و در غیر این صورت به صورت متن ساده باقی می‌مانند.

برای نظارت بر یک موضوع از طریق ترمینال، آن را stream کنید:

curl -s -u admin https://ntfy.example.com/alerts/raw

دستور curl رمز عبور را درخواست می‌کند. هر پیام به صورت یک خط دریافت می‌شود و خطوط خالی که گاهی ظاهر می‌شوند، keepalive هستند. باز کردن https://ntfy.example.com در مرورگر و ورود با همان حساب کاربری، نسخه وب همان stream را در اختیار شما قرار می‌دهد.

تنظیم محدودیت نرخ برای جلوگیری از اشباع سرور توسط یک اسکریپت

به‌صورت پیش‌فرض، هر بازدیدکننده یک سهمیه 60 درخواستی دارد که هر 5 ثانیه یک درخواست به آن اضافه می‌شود. این مقدار برای یک سرور شخصی سخاوتمندانه است، اما یک اسکریپت که در حلقه تلاش مجدد (retry loop) گیر کرده باشد، تمام آن را مصرف می‌کند. محدودیت‌ها را به server.yml اضافه کنید.

visitor-request-limit-burst: 30
visitor-request-limit-replenish: "10s"
visitor-message-daily-limit: 500
sudo docker compose restart ntfy

بازدیدکننده‌ای که از حد مجاز فراتر رود، به‌جای دریافت پیام، کد وضعیت HTTP 429 را دریافت می‌کند. این محدودیت بر اساس آدرس بازدیدکننده محاسبه می‌شود؛ به همین دلیل است که behind-proxy: true اهمیت زیادی دارد: بدون آن، ntfy فقط آدرس Caddy را می‌بیند، در نتیجه همه کلاینت‌ها به عنوان یک بازدیدکننده واحد شمارش می‌شوند و یک اسکریپت پرمصرف، سهمیه‌ای را که گوشی شما و سایر سرورهایتان به اشتراک می‌گذارند، تمام می‌کند.

هشدار در صورت شکست یک cron job

توکن را در خط فرمان قرار ندهید. ps aux خط فرمان کامل هر پردازش در حال اجرا را به تمام کاربران سیستم نشان می‌دهد، بنابراین توکنی که با -H ارسال شود، تا زمانی که curl در حال اجراست برای هر حساب کاربری محلی قابل خواندن خواهد بود. استفاده از یک فایل پیکربندی curl از این مشکل جلوگیری می‌کند.

sudo install -d -m 700 /etc/ntfy-alert
printf 'header = "Authorization: Bearer tk_REPLACE_WITH_YOUR_TOKEN"\n' | sudo tee /etc/ntfy-alert/curlrc
sudo chmod 600 /etc/ntfy-alert/curlrc

حالا job را بسته‌بندی کنید. این فایل را با نام /usr/local/bin/backup-with-alert.sh ذخیره کرده و آن را chmod 750 کنید.

#!/bin/bash
out=$(/usr/local/bin/backup.sh 2>&1)
code=$?
if [ "$code" -ne 0 ]; then
  printf '%s' "$out" | tail -c 1000 | curl -K /etc/ntfy-alert/curlrc \
    -H "Title: backup.sh failed with exit $code" \
    -H "Priority: high" \
    -H "Tags: warning" \
    --data-binary @- \
    https://ntfy.example.com/alerts
fi
exit "$code"
17 3 * * * /usr/local/bin/backup-with-alert.sh >> /var/log/backup-alert.log 2>&1

$? بلافاصله پس از دستور ثبت می‌شود، زیرا دستور بعدی که اجرا شود آن را بازنویسی می‌کند. خروجی از طریق tail -c 1000 فیلتر می‌شود، زیرا ntfy محدودیت حداکثر اندازه پیام دارد و اعلان (notification) جایگزین نمایشگر لاگ نیست. دستور exit "$code" در انتها، وضعیت اصلی (exit status) را حفظ می‌کند تا هر ابزار دیگری که این job را نظارت می‌کند، همچنان شکست آن را تشخیص دهد. کل فرآیند را با تغییر مسیر اسکریپت به /bin/false برای یک بار اجرا تست کنید.

شاخه شکست (failure branch) که هرگز اجرا نمی‌شود، از نبود هشدار بدتر است، زیرا این تصور را ایجاد می‌کند که سکوت به معنای موفقیت است. Cron محیطی تقریباً خالی و PATH بسیار کوتاه‌تری نسبت به shell ورود شما در اختیار job قرار می‌دهد، بنابراین اسکریپتی که هنگام اجرای دستی کار می‌کند، ممکن است پیش از رسیدن به خط curl متوقف شود. راهنمای دلایل اجرا نشدن یک cron job این تله‌های محیطی را پوشش می‌دهد. در همه جا از مسیرهای مطلق (absolute paths) استفاده کنید و پس از اولین اجرای زمان‌بندی‌شده، به جای فرض کردن موفقیت، فایل لاگ را مطالعه کنید.

هشدار هنگام شکست یک واحد systemd

Cron برای کارهای زمان‌بندی‌شده مناسب است. سرویس‌های طولانی‌مدت به OnFailure= نیاز دارند که systemd هر زمان که یک واحد وارد وضعیت failed شود، آن را اجرا می‌کند. یک واحد الگو (template) بسازید و از آن برای هر سرویس روی سیستم استفاده کنید. این فایل را با نام /etc/systemd/system/ntfy-unit-failed@.service ذخیره کنید.

[Unit]
Description=Send an ntfy alert because %i failed

[Service]
Type=oneshot
ExecStart=/usr/local/bin/ntfy-unit-failed %i

سپس /usr/local/bin/ntfy-unit-failed را با مجوز 750 ایجاد کنید:

#!/bin/bash
unit="$1"
journalctl -u "$unit" -n 15 --no-pager -o cat | tail -c 1000 | curl -K /etc/ntfy-alert/curlrc \
  -H "Title: $unit failed on $(hostname -s)" \
  -H "Priority: urgent" \
  -H "Tags: rotating_light" \
  --data-binary @- \
  https://ntfy.example.com/alerts

آن را با استفاده از یک drop-in به سرویس متصل کنید تا ارتقای بسته‌ها، تغییرات شما را بازنویسی نکند.

sudo systemctl edit myapp.service
[Unit]
OnFailure=ntfy-unit-failed@%n.service

%n به نام کامل واحد بسط می‌یابد، بنابراین نمونه به ntfy-unit-failed@myapp.service تبدیل می‌شود و %i در داخل الگو، myapp.service را به عنوان اولین آرگومان به اسکریپت می‌فرستد. این همان چیزی است که اجازه می‌دهد یک الگو برای همه واحدها استفاده شود. با یک واحد که عمداً شکست می‌خورد و با نام /etc/systemd/system/ntfy-selftest.service ذخیره شده است، عملکرد آن را آزمایش کنید.

[Unit]
Description=Deliberately failing unit
OnFailure=ntfy-unit-failed@%n.service

[Service]
Type=oneshot
ExecStart=/bin/false
sudo systemctl daemon-reload
sudo systemctl start ntfy-selftest.service

دستور شروع با کد خروجی غیر صفر پایان می‌یابد و Job for ntfy-selftest.service failed because the control process exited with error code را چاپ می‌کند؛ گوشی شما باید حدود یک ثانیه بعد هشدار دهد. پس از آزمایش، واحد تست را حذف کنید.

یک نکته مهم وجود دارد. OnFailure= تنها زمانی اجرا می‌شود که یک واحد به وضعیت failed برسد، و سرویسی که دارای Restart=always است ممکن است هرگز به آن وضعیت نرسد، زیرا systemd مدام آن را ری‌استارت می‌کند. واحد تنها زمانی شکست‌خورده محسوب می‌شود که از StartLimitBurst ری‌استارت در بازه StartLimitIntervalSec فراتر رود. این دو مقدار را برای هر سرویسی که می‌خواهید از وضعیت آن مطلع شوید تنظیم کنید، در غیر این صورت چرخه خرابی ممکن است روزها بی‌صدا ادامه یابد. تایمرها جایگزین تمیزتری برای الگوی cron در بالا هستند، زیرا واحد سرویسِ یک تایمر به‌طور خودکار از OnFailure= بهره‌مند می‌شود و راهنمای سرویس‌ها و تایمرهای systemd روی VPS مراحل تبدیل آن را توضیح می‌دهد.

اتصال یک مانیتور آپ‌تایم به همان موضوع

Uptime Kuma، مانیتور وضعیت self-hosted، دارای یک نوع اعلان داخلی برای ntfy است. به بخش Settings، سپس Notifications و بعد Setup Notification بروید، گزینه Ntfy را انتخاب کنید، آدرس سرور را روی https://ntfy.example.com و موضوع (topic) را روی alerts تنظیم کنید، یک اولویت انتخاب کرده و توکن دسترسی robot را وارد کنید. پیش از ذخیره، یک اعلان آزمایشی ارسال کنید؛ زیرا نام موضوع اشتباه در یک دسترسی write که آن موضوع را پوشش نمی‌دهد، بدون نمایش خطا شکست می‌خورد.

محدودیت واقعی این چیدمان: مانیتوری که روی همان VPS اجرا می‌شود نمی‌تواند به شما اطلاع دهد که VPS از دسترس خارج شده است و ntfy نیز نمی‌تواند خبر از کار افتادن خود ntfy را ارسال کند. مانیتور را روی دستگاه دیگری اجرا کنید و برای مانیتوری که خود ntfy را پایش می‌کند، یک کانال اعلان دوم مانند ایمیل در نظر بگیرید. نوع مانیتور Push در Uptime Kuma نقطه کور دیگر را پوشش می‌دهد: cron job شما پس از اجرای موفقیت‌آمیز، یک URL از نوع push را فراخوانی می‌کند و Kuma زمانی که این فراخوانی‌ها متوقف شوند، هشدار می‌دهد. شاخه شکست تنها زمانی فعال می‌شود که job اجرا شود، بنابراین این روش درباره jobهایی که هرگز شروع نشده‌اند، اطلاعاتی نمی‌دهد.

آیا ntfy خودمیزبان (self-hosted) روی Android و iPhone کار می‌کند؟

در Android، بله، بدون هیچ قید و شرطی. برنامه را از Google Play یا F-Droid نصب کنید، تنظیمات را باز کنید، سرور پیش‌فرض را روی https://ntfy.example.com تنظیم کنید، حساب خود را در صفحه مدیریت کاربر اضافه کنید و سپس در alerts مشترک شوید. تحویل فوری (Instant delivery) یک سرویس پیش‌زمینه را فعال نگه می‌دارد تا پیام‌ها حتی زمانی که گوشی در حالت doze است دریافت شوند؛ اعلان دائمی که همراه آن نمایش داده می‌شود، الزام سیستم‌عامل Android برای سرویس‌های پیش‌زمینه است و نه یک باگ. نسخه F-Droid هیچ کدی از Firebase ندارد، بنابراین تمام اشتراک‌ها از تحویل فوری استفاده می‌کنند. ntfy همچنین می‌تواند به عنوان یک توزیع‌کننده UnifiedPush عمل کند که جایگزینی متن‌باز برای سرویس push گوگل است، بنابراین سایر برنامه‌هایی که از UnifiedPush پشتیبانی می‌کنند نیز می‌توانند از طریق سرور شما پیام ارسال کنند.

در iOS، این کار با یک وابستگی که نمی‌توانید آن را حذف کنید انجام می‌شود. اپل یک برنامه در پس‌زمینه را فقط از طریق APNs (سرویس اعلان push اپل) بیدار می‌کند و فقط طرفی که اعتبارنامه‌های امضای برنامه را در اختیار دارد می‌تواند به آن پیام بفرستد، بنابراین سرور شما راهی برای دسترسی مستقیم به برنامه ندارد. ntfy این مشکل را با یک رله حل می‌کند: سرور شما یک poll_request حاوی شناسه پیام را به ntfy.sh می‌فرستد، که آن را از طریق Firebase و APNs برای بیدار کردن برنامه ارسال می‌کند و سپس برنامه بدنه پیام را از سرور شما دریافت می‌کند.

upstream-base-url: "https://ntfy.sh"

در مورد هزینه‌های این کار شفاف باشید. محتوای پیام روی سرور شما باقی می‌ماند، اما این واقعیت که پیامی دریافت شده و شناسه آن، از زیرساختی عبور می‌کند که شما آن را مدیریت نمی‌کنید. بدون این تنظیم، اعلان‌ها در iPhone از یک سرور خودمیزبان با تأخیر یا اصلاً دریافت نمی‌شوند، زیرا هیچ عاملی برنامه را بیدار نمی‌کند. تنها راه برای حذف رله، ساخت و انتشار برنامه iOS توسط خودتان با حساب توسعه‌دهنده اپل و کلیدهای APNs اختصاصی خودتان است که به معنای پرداخت هزینه سالانه و بازسازی برنامه برای هر به‌روزرسانی است. اگر استفاده از رله برای شما قابل قبول نیست، هشدارها را روی Android یا برنامه وب دسکتاپ نگه دارید.

پشتیبان‌گیری، ارتقا و پین کردن ایمیج

دو مسیر وجود دارند که قابل بازسازی نیستند: /etc/ntfy/server.yml و /var/lib/ntfy/user.db. دومی شامل تمام کاربران، هش رمزهای عبور، ورودی‌های ACL و توکن‌ها است، بنابراین با آن مانند یک کلید خصوصی رفتار کنید.

sudo tar czf ntfy-backup.tgz -C / etc/ntfy var/lib/ntfy
sudo chmod 600 ntfy-backup.tgz

آن فایل را از سرور کپی کرده و به جای امنی منتقل کنید. cache.db فقط شامل پیام‌های اخیر است، یعنی 12 ساعت پیام با تنظیم cache-duration در بالا؛ بنابراین از دست دادن آن هزینه یا خسارتی ندارد. ارتقا به معنای ویرایش تگ در فایل compose و اجرای دستور pull است.

sudo docker compose pull
sudo docker compose up -d
curl -s https://ntfy.example.com/v1/health

ابتدا یادداشت‌های انتشار (release notes) را مطالعه کنید. دیتابیس‌های SQLite هنگام شروع برنامه مهاجرت (migrate) می‌شوند، بنابراین بازگشت به یک تگ قدیمی‌تر پس از تغییر اسکیما ایمن نیست. پشتیبانی که تهیه کرده‌اید را تا زمانی که نسخه جدید به مدت یک روز بدون مشکل اجرا شود، نزد خود نگه دارید.

Gotify و Apprise

Gotify گزینه سبک‌تری است: یک فایل اجرایی واحد با رابط کاربری وب و یک اپلیکیشن اندروید. این ابزار از wildcard برای موضوعات (topics) پشتیبانی نمی‌کند و کلاینت رسمی برای iOS ندارد، که برای سرورهای شخصی که تنها هدف آن‌ها دستگاه‌های اندرویدی است، مناسب می‌باشد. Apprise یک کتابخانه پایتون و ابزار خط فرمان است و نه یک سرور؛ این ابزار یک پیام واحد را به بیش از صد سرویس مختلف از جمله ntfy ارسال می‌کند که برای اسکریپت‌هایی که باید همزمان به چندین مقصد اطلاع‌رسانی کنند، کاربردی است. ntfy ابزاری است که سرور، HTTP API و اپلیکیشن‌هایی برای هر دو پلتفرم موبایل ارائه می‌دهد و به همین دلیل، معمول‌ترین پاسخ برای ارسال هشدار از یک سرور اجاره‌ای است.

FAQ

چرا انتشار پیام در سرور ntfy من با خطای 403 مواجه می‌شود؟

با وجود auth-default-access: "deny-all" در server.yml، انتشار ناشناس رد می‌شود و این رفتار مورد انتظار است. اعتبارنامه‌ها را با -u user:pass یا -H "Authorization: Bearer tk_..." ارسال کنید. اگر در حال حاضر توکن ارسال می‌کنید و همچنان 403 دریافت می‌کنید، کاربری که پشت آن توکن قرار دارد، هیچ ورودی ACL منطبقی برای آن موضوع (topic) ندارد. دستور ntfy access را اجرا کنید تا لیست کامل را مشاهده کنید. به یاد داشته باشید که مجوز write اجازه اشتراک (subscribe) را نمی‌دهد، بنابراین حسابی که به درستی منتشر می‌کند، همچنان هنگام تلاش برای خواندن همان موضوع رد خواهد شد.

آیا اعلان‌ها در آیفون با سرور ntfy شخصی‌سازی‌شده (self-hosted) کار می‌کنند؟

آن‌ها از طریق یک رله (relay) کار می‌کنند که نمی‌توانید از آن اجتناب کنید. اپل برنامه‌ها را فقط از طریق APNs (سرویس اعلان اپل) بیدار می‌کند و فقط ناشر برنامه می‌تواند به آن ارسال کند، بنابراین ntfy یک poll_request حاوی شناسه پیام را به ntfy.sh می‌فرستد که آن را به دستگاه رله می‌کند. مقدار upstream-base-url: "https://ntfy.sh" را در server.yml تنظیم کرده و کانتینر را مجدداً راه‌اندازی کنید. بدنه اصلی پیام همچنان از سرور شما دریافت می‌شود. بدون این تنظیم، اعلان‌های iOS با تأخیر مواجه شده یا هرگز ظاهر نمی‌شوند.

چرا هشدار ntfy مربوط به cron job من هرگز نرسید؟

ابتدا خط دستور curl را به تنهایی اجرا کنید تا مطمئن شوید توکن و موضوع درست هستند. اگر به صورت دستی کار می‌کند اما در cron نه، شکست در بالادست هشدار است: cron کارها را با یک محیط حداقلی و یک PATH کوتاه اجرا می‌کند، بنابراین اسکریپتی که یک دستور را با نام ساده فراخوانی می‌کند، ممکن است پیش از رسیدن به خط curl متوقف شود. از مسیرهای مطلق استفاده کنید، خروجی کار را به یک فایل لاگ هدایت کنید و پس از اجرای بعدی، آن فایل را بخوانید. پاسخ 429 به جای تحویل پیام، به این معنی است که محدودیت نرخ (rate limit) فعال است و اسکریپت شما بیش از حد سریع تلاش مجدد می‌کند.

آیا باید ntfy را در اینترنت عمومی قرار دهم؟

برنامه‌های تلفن همراه باید از طریق شبکه‌های موبایل به آن دسترسی داشته باشند، بنابراین یک نقطه پایانی HTTPS عمومی با auth-default-access: "deny-all" و ACLهای مبتنی بر موضوع، تنظیمات استاندارد است و تا زمانی که هیچ موضوعی توسط everyone قابل خواندن نباشد، ایمن است. یک نمونه فقط-VPN زمانی منطقی است که هر مشترک، ماشینی باشد که شما کنترل می‌کنید. این گزینه برای تلفن‌ها مناسب نیست، زیرا برنامه فقط زمانی که تونل برقرار است پیام دریافت می‌کند، بنابراین هشدارها تا زمانی که تلفن دوباره متصل شود، در صف باقی می‌مانند.