> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# إشعارات فورية من سيرفرك إلى هاتفك مع ntfy
- URL: https://arabroot.io/articles/اشعارات-فورية-الى-هاتفك-مع-ntfy/
- Published: 2026-10-08T08:45:00.000Z
- Updated: 2026-10-09T13:02:09.000Z
- Description: كيف تشغل سيرفر ntfy خاصاً، وتصلك على هاتفك رسالة عندما يمتلئ القرص أو تفشل النسخة الاحتياطية أو ينتهي النشر.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الاستضافة الذاتية والتطبيقات, التشغيل والنسخ الاحتياطي, DevOps وCI/CD, الاستضافة الذاتية

في مقال [كيف تعمل الإشعارات الفورية Push Notifications](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%A7%D8%B4%D8%B9%D8%A7%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D9%88%D8%B1%D9%8A%D8%A9-push-notifications/) شرحنا الطريق الذي تسلكه الرسالة من سيرفر إلى هاتف، وأن أغلب هذا الطريق يمر عبر Apple وGoogle، أما هذا المقال فعن الحالة التي يكون فيها المستقبل أنت وفريقك: النسخة الاحتياطية الليلية فشلت، والقرص وصل إلى 90%، والنشر إلى الإنتاج انتهى، وخدمة متوقفة منذ عشر دقائق. والطرق المعتادة لإيصال هذه التنبيهات لها مشكلات معروفة، فالبريد يضيع بين الرسائل أو يتأخر ويحتاج إلى سيرفر بريد يعمل، وروبوتات المحادثة في الخدمات التجارية تنقل أسماء سيرفراتك وأخطاءها إلى طرف ثالث، وكتابة تطبيق جوال خاص بالتنبيهات مشروع كامل.

والأداة التي نستخدمها هنا هي [ntfy](https://ntfy.sh/?ref=arabroot.io) (تنطق notify)، وهي سيرفر صغير مكتوب ب Go يستقبل الرسالة بطلب HTTP عادي ويوصلها إلى تطبيق على الهاتف أو إلى المتصفح، ومعها تطبيق Android وتطبيق iOS وواجهة ويب، والمشروع بترخيص مزدوج [Apache-2.0 وGPLv2](https://github.com/binwiederhier/ntfy?ref=arabroot.io). وسوف نتناول في هذا المقال ما يلي:

- كيف يعمل ntfy: المواضيع Topics والنشر والاشتراك.
- كيف يصل التنبيه إلى هاتفك على Android وiOS، وأين يمر عبر Google وApple.
- تشغيل سيرفر خاص لا يقبل أحداً إلا بحساب، مع المستخدمين والصلاحيات ACL والتوكنات Tokens.
- إرسال التنبيهات من السكربتات ومن Cron ومن خط CI ومن أدوات المراقبة مثل Alertmanager، كلها بتجربة حقيقية.
- من يراقب سيرفر التنبيهات نفسه، وأخطاء شائعة.

## كيف يعمل ntfy؟ المواضيع Topics والنشر والاشتراك

فكرة ntfy هي النشر والاشتراك Publish/Subscribe عبر HTTP: الموضوع Topic اسم في الرابط مثل `/backups`، ومن يريد أن يرسل يكتب طلب POST إلى هذا الرابط وجسمه نص الرسالة، ومن يريد أن يستقبل يشترك في الموضوع نفسه، فيصله كل ما ينشر فيه. ولا يحتاج الموضوع إلى إنشاء مسبق، فأول رسالة تنشئه، والصورة التالية تبين الأطراف:

![على اليسار أربعة مرسلين: سكربت Cron لفحص القرص، وسكربت النسخ الاحتياطي، وخط CI، وAlertmanager وGrafana، ترسل أسهماً برتقالية بطلب HTTP POST إلى سيرفر ntfy في الوسط وتحته ثلاثة مواضيع alerts-prod وbackups وdeploys، وعلى اليمين ثلاثة مشتركين: تطبيق الهاتف وواجهة الويب في المتصفح وcurl أو سكربت، تصلها أسهم خضراء عبر WebSocket وJSON stream وWeb Push](https://arabroot.io/content/images/2026/10/ntfy-01-pubsub.webp)

أي أداة تعرف إرسال طلب HTTP تستطيع أن ترسل تنبيهاً

وهذا التصميم هو سبب انتشار ntfy، فالمرسل لا يحتاج إلى مكتبة ولا SDK، ويكفيه `curl`، والسطر التالي من [توثيق النشر](https://docs.ntfy.sh/publish/?ref=arabroot.io) رسالة كاملة إلى السيرفر العام:

```bash
curl -d "Backup successful 😀" ntfy.sh/mytopic
```

وتستطيع أن تضيف للرسالة عنواناً بالترويسة `Title`، وأولوية من 1 إلى 5 بالترويسة `Priority` تحدد في تطبيق Android الصوت والاهتزاز وهل يظهر الإشعار فوق الشاشة، ووسوماً `Tags` يتحول بعضها إلى رموز تعبيرية، ورابطاً يفتح عند الضغط `Click`، ونصاً بصيغة Markdown، وكل ذلك موثق في [صفحة النشر](https://docs.ntfy.sh/publish/?ref=arabroot.io#message-priority). وللحدود التي تهمك من [قائمة الحدود](https://docs.ntfy.sh/publish/?ref=arabroot.io#limitations): الرسالة حتى 4096 بايت وما زاد يتحول إلى مرفق، والعنوان حتى 1 KB، والسيرفر يحفظ الرسائل افتراضياً 12 ساعة حتى يحصل عليها من كان غير متصل.

⚠️

على السيرفر العام ntfy.sh يعمل اسم الموضوع ككلمة مرور، فكل من يعرف الاسم أو يخمنه يقرأ رسائلك ويرسل فيه، لذلك لا ترسل إلى السيرفر العام أسماء سيرفراتك الداخلية أو عناوينها أو أخطاء فيها بيانات، والسبب أن الموضوع هناك مفتوح لكل الإنترنت. وفي هذا المقال نشغل سيرفراً خاصاً يرفض كل طلب ليس معه حساب.

## كيف يصل التنبيه إلى هاتفك؟

هنا يعود ما شرحناه في المقال السابق عن الطريق إلى الهاتف، وntfy يتعامل مع كل نظام بطريقة مختلفة، والصورة التالية تبين الطريقين:

![لوحتان: في الأعلى Android حيث يحفظ تطبيق ntfy اتصال WebSocket مفتوحاً مع سيرفرك وتصل الرسائل مباشرة دون سيرفر Google، وفي الأسفل iOS حيث يرسل سيرفرك معرف الرسالة فقط إلى ntfy.sh الذي يمرره إلى FCM ثم APNs فيصل الهاتف إشعار عام ثم يجلب التطبيق نص الرسالة من سيرفرك](https://arabroot.io/content/images/2026/10/ntfy-03-phone-delivery.webp)

على Android لا يمر النص على Google، وعلى iOS يمر معرف الرسالة فقط

- **Android:** بحسب [توثيق تطبيق الهاتف](https://docs.ntfy.sh/subscribe/phone/?ref=arabroot.io#instant-delivery) يستخدم التطبيق Firebase فقط مع السيرفر العام ntfy.sh وفي نسخة Google Play وحدها، أما مع سيرفرك فيعمل بالتوصيل الفوري Instant Delivery، أي أن التطبيق يحفظ اتصالاً مع سيرفرك عبر خدمة تعمل في المقدمة Foreground Service تظهر كإشعار دائم صغير، ونسخة [F-Droid](https://f-droid.org/packages/io.heckel.ntfy/?ref=arabroot.io) لا تحتوي Firebase أصلاً. وبهذا يصل نص التنبيه من سيرفرك إلى هاتفك دون أن يمر على Google.
- **iOS:** لا يسمح النظام لتطبيق بحفظ اتصال في الخلفية، لذلك يشرح [توثيق الإعداد](https://docs.ntfy.sh/config/?ref=arabroot.io#ios-instant-notifications) أن السيرفر المستضاف يرسل إلى ntfy.sh طلباً اسمه `poll_request` يحمل معرف الرسالة فقط والهاش SHA-256 لرابط الموضوع، وntfy.sh يمرره عبر Firebase إلى APNs، فيستيقظ التطبيق ويجلب الرسالة الحقيقية من سيرفرك، والنص الذي يمر هو «New message» دائماً. وهذا يتطلب الإعداد `upstream-base-url: "https://ntfy.sh"`، وبدونه تصل الرسائل متأخرة قد تصل إلى ساعات بحسب حالة الهاتف.
- **المتصفح:** واجهة الويب تستقبل الرسائل والصفحة مفتوحة، ومع إعداد Web Push بمفاتيح VAPID تصل والمتصفح مغلق، بالطريقة التي جربناها في المقال السابق.

وفي التجربة التالية نستخدم الاشتراك عبر HTTP بتدفق JSON من [واجهة الاشتراك](https://docs.ntfy.sh/subscribe/api/?ref=arabroot.io)، لأنه يرينا بالضبط ما يصل إلى أي تطبيق.

## تجربة عملية: سيرفر ntfy خاص وتنبيهات من أربعة مصادر

الآن سوف نشغل ntfy 2.28.0 في Container بإعداد خاص لا يقبل أي طلب دون حساب، ثم ننشئ حسابات لكل مرسل ونعطي كل حساب أقل ما يحتاجه، ثم نرسل من Container ثان يمثل سيرفر الإنتاج `web-01`: سكربت يفحص القرص من Cron، وغلاف Wrapper ينبه عند فشل أي أمر، وخطوة في خط CI، وتنبيه بصيغة Alertmanager. والتجربة تحتاج Docker فقط، والمنفذ على `127.0.0.1` فلا يصل إليه أحد من خارج جهازك.

### ملف الإعداد وتشغيل السيرفر

ملف الإعداد `server.yml`:

```yaml
# /etc/ntfy/server.yml
base-url: "http://127.0.0.1:18480"
listen-http: ":80"

# messages are kept 12 hours so a phone that was offline gets them later
cache-file: "/var/lib/ntfy/cache.db"
cache-duration: "12h"
attachment-cache-dir: "/var/lib/ntfy/attachments"

# private server: nobody reads or writes unless an ACL entry allows it
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
enable-login: true
require-login: true
```

في الإعداد أعلاه لاحظ التالي:

- `auth-default-access: "deny-all"` هو أهم سطر، فهو يقرر ماذا يحدث لطلب لا توجد له قاعدة، والقيمة الافتراضية `read-write` أي أن أي شخص يقرأ ويكتب في كل المواضيع، كما في [قسم التحكم في الوصول](https://docs.ntfy.sh/config/?ref=arabroot.io#access-control).
- `base-url` هو الرابط الذي يراه الناس، وفي الإنتاج يكون `https://ntfy.example.com`، وهو مطلوب للمرفقات ولتطبيق iOS.
- `cache-file` يجعل الرسائل المحفوظة 12 ساعة تبقى بعد إعادة التشغيل، وبدونه تحفظ في الذاكرة فقط.
- `require-login` يجعل واجهة الويب تطلب الدخول قبل أي شيء.

نشغل السيرفر على شبكة خاصة بالتجربة، ثم نتأكد أنه يعمل وأنه يرفض الغرباء:

```bash
docker network create lab-notify-net
docker run -d --name lab-notify-ntfy --network lab-notify-net -p 127.0.0.1:18480:80 \
  -v "$PWD/server.yml:/etc/ntfy/server.yml:ro" -v lab-notify-ntfy-data:/var/lib/ntfy \
  binwiederhier/ntfy:v2.28.0 serve
docker logs lab-notify-ntfy
curl -s http://127.0.0.1:18480/v1/health
curl -s -d "test" http://127.0.0.1:18480/alerts-prod
```

```text
2026/10/07 15:29:09 INFO Listening on :80[http], ntfy 2.28.0, log level is INFO (tag=startup)
{"healthy":true}
{"code":40301,"http":403,"error":"forbidden","link":"https://ntfy.sh/docs/publish/#authentication"}
```

السيرفر يعمل، والطلب دون حساب يرفض ب 403، وهذا ما نريده تماماً.

### المستخدمون والصلاحيات ACL والتوكنات

القاعدة التي نتبعها هنا هي أقل صلاحية Least Privilege: كل مرسل حساب مستقل لا يستطيع إلا الكتابة في موضوعه، وفريق التشغيل حساب يقرأ ولا يكتب. والصورة التالية تبين ما سوف ننشئه:

![جدول صلاحيات: الصفوف backup-bot وci-bot وmonitor-bot بتوكن وops بكلمة مرور وanonymous دون شيء، والأعمدة alerts-* وbackups وdeploys، وكل بوت له write-only في موضوع واحد فقط، وops له read-only في الثلاثة، وanonymous مرفوض في الكل، وفوقها auth-default-access deny-all](https://arabroot.io/content/images/2026/10/ntfy-02-acl.webp)

كل حساب يكتب في موضوعه فقط، ومن لا قاعدة له يرفض

ننشئ المستخدمين بالأمر `ntfy user` داخل ال Container، ونمرر كلمة المرور في المتغير `NTFY_PASSWORD` حتى لا يسألنا عنها، والبوتات تأخذ كلمات مرور عشوائية لأنها سوف تستخدم التوكنات فقط:

```bash
docker exec -e NTFY_PASSWORD='Adm1n-Lab-2026' lab-notify-ntfy ntfy user add --role=admin admin
docker exec -e NTFY_PASSWORD='Ops-Lab-2026' lab-notify-ntfy ntfy user add ops
for u in backup-bot ci-bot monitor-bot; do
  docker exec -e NTFY_PASSWORD="$(head -c 18 /dev/urandom | base64)" lab-notify-ntfy ntfy user add $u
done
```

```text
user admin added with role admin
user ops added with role user
user backup-bot added with role user
user ci-bot added with role user
user monitor-bot added with role user
```

ثم القواعد بالأمر `ntfy access`، والنجمة في `alerts-*` تعني كل موضوع يبدأ ب `alerts-`:

```bash
docker exec lab-notify-ntfy ntfy access ops 'alerts-*' read-only
docker exec lab-notify-ntfy ntfy access ops backups read-only
docker exec lab-notify-ntfy ntfy access ops deploys read-only
docker exec lab-notify-ntfy ntfy access backup-bot backups write-only
docker exec lab-notify-ntfy ntfy access ci-bot deploys write-only
docker exec lab-notify-ntfy ntfy access monitor-bot 'alerts-*' write-only
docker exec lab-notify-ntfy ntfy access
```

```text
user admin (role: admin, tier: none)
- read-write access to all topics (admin role)
user backup-bot (role: user, tier: none)
- write-only access to topic backups
user ci-bot (role: user, tier: none)
- write-only access to topic deploys
user monitor-bot (role: user, tier: none)
- write-only access to topic alerts-*
user ops (role: user, tier: none)
- read-only access to topic alerts-*
- read-only access to topic backups
- read-only access to topic deploys
user * (role: anonymous, tier: none)
- no topic-specific permissions
- no access to any (other) topics (server config)
```

ثم توكن لكل بوت، والتوكن الخاص ب CI ينتهي بعد 90 يوماً حتى لا يبقى صالحاً إلى الأبد إذا تسرب من سجلات الخط:

```bash
docker exec lab-notify-ntfy ntfy token add --label="backup on web-01" backup-bot
docker exec lab-notify-ntfy ntfy token add --expires=90d --label="gitea actions" ci-bot
docker exec lab-notify-ntfy ntfy token add --label="alertmanager" monitor-bot
docker exec lab-notify-ntfy ntfy token list
```

```text
user backup-bot
- tk_7ufuemtyvajrn8uqhh0pfozhhizbk (backup on web-01), never expires, accessed from 192.168.176.3 at 07 Oct 26 17:47 UTC
user ci-bot
- tk_dotg9thtddr07sbvgveinsg5rzlsn (gitea actions), expires 05 Jan 27 17:47 UTC, accessed from 192.168.176.3 at 07 Oct 26 17:47 UTC
user monitor-bot
- tk_te8r1278ehop4irfy64f22bvihlcp (alertmanager), never expires, accessed from 192.168.176.3 at 07 Oct 26 17:47 UTC
```

وقد يتساءل البعض: لماذا ثلاثة بوتات وليس حساباً واحداً لكل السكربتات؟ والإجابة في [توثيق التوكنات](https://docs.ntfy.sh/config/?ref=arabroot.io#access-tokens) نفسه، فالتوكن اليوم يعطي صلاحيات المستخدم كاملة، ولا توجد بعد توكنات بصلاحيات أضيق من صاحبها، فإذا تسرب توكن من سكربت النسخ الاحتياطي فأقصى ما يفعله المهاجم أن يكتب في موضوع `backups`، ولا يقرأ تنبيهات الإنتاج ولا يرسل تنبيهاً مزوراً فيها. وإذا كنت تفضل أن يكون كل ذلك في ملف الإعداد بدل الأوامر، فالخيارات `auth-users` و`auth-access` و`auth-tokens` في [مثال السيرفر الخاص](https://docs.ntfy.sh/config/?ref=arabroot.io#example-private-instance) تفعل الشيء نفسه، وكلمات المرور فيها هاش bcrypt يولده الأمر `ntfy user hash`.

### النشر والرفض

نشغل Container صغيراً باسم السيرفر `web-01` على الشبكة نفسها، ونفتح فيه اشتراكاً دائماً بحساب `ops` يكتب كل ما يصل إلى ملف، وهو يمثل تطبيق الهاتف:

```bash
docker run -d --name lab-notify-client --hostname web-01 --network lab-notify-net alpine:3.24 sleep infinity
docker exec lab-notify-client apk add --no-cache curl bash coreutils jq
docker exec -d lab-notify-client sh -c \
  'curl -s -N -u ops:Ops-Lab-2026 "http://lab-notify-ntfy/alerts-prod,backups,deploys/json" > /tmp/stream.jsonl'
docker exec -it lab-notify-client bash
```

ومن داخل ال Container نرسل رسالة بتوكن البوت، ثم نجرب أربعة طلبات يجب أن ترفض:

```bash
BK=tk_7ufuemtyvajrn8uqhh0pfozhhizbk
curl -s -H "Authorization: Bearer $BK" -H "Title: نسخة قاعدة البيانات" -H "Priority: low" \
  -H "Tags: white_check_mark,backup" -d "اكتملت النسخة الليلية: 1.4 GB في 6 دقائق" http://lab-notify-ntfy/backups
curl -s -H "Authorization: Bearer $BK" -d test http://lab-notify-ntfy/alerts-prod     # موضوع ليس له
curl -s -H "Authorization: Bearer $BK" "http://lab-notify-ntfy/backups/json?poll=1"   # يقرأ موضوعه
curl -s -u ops:Ops-Lab-2026 -d test http://lab-notify-ntfy/backups                    # ops يكتب
curl -s -u ops:wrong -d test http://lab-notify-ntfy/backups                           # كلمة مرور خاطئة
```

```text
{"id":"ImdpMNw1ZVkX","time":1791395265,"expires":1791438465,"event":"message","topic":"backups","title":"نسخة قاعدة البيانات","message":"اكتملت النسخة الليلية: 1.4 GB في 6 دقائق","priority":2,"tags":["white_check_mark","backup"]}
{"code":40301,"http":403,"error":"forbidden","link":"https://ntfy.sh/docs/publish/#authentication"}
{"code":40301,"http":403,"error":"forbidden","link":"https://ntfy.sh/docs/publish/#authentication"}
{"code":40301,"http":403,"error":"forbidden","link":"https://ntfy.sh/docs/publish/#authentication"}
{"code":40101,"http":401,"error":"unauthorized","link":"https://ntfy.sh/docs/publish/#authentication"}
```

في المخرج أعلاه لاحظ التالي:

- الرسالة المقبولة تعود بمعرفها ووقتها ووقت انتهائها من ال Cache بعد 12 ساعة بالضبط (43200 ثانية)، والأولوية `low` أصبحت الرقم 2.
- البوت لا يستطيع أن يقرأ حتى موضوعه، لأن صلاحيته `write-only`، فلو تسرب توكنه لا يقرأ به أحد نتائج النسخ الاحتياطي.
- الفرق بين 401 و403 مفيد عند التشخيص: 401 يعني أن السيرفر لم يعرفك، أي كلمة مرور أو توكن خاطئ، و403 يعني أنه عرفك ولكن القواعد لا تسمح.

### من Cron: تنبيه عندما يمتلئ القرص

السكربت التالي يفحص نسبة استخدام الأقراص ويرسل تنبيهاً بأولوية عالية إذا تجاوزت حداً، ويوضع في `/usr/local/bin/check-disk.sh`:

```bash
#!/usr/bin/env bash
# /usr/local/bin/check-disk.sh: alert when a disk passes the limit (run from cron)
set -euo pipefail
NTFY_URL="${NTFY_URL:-https://ntfy.example.com}"
NTFY_TOKEN="${NTFY_TOKEN:?set NTFY_TOKEN}"
LIMIT="${LIMIT:-85}"
MOUNTS="${MOUNTS:-/}"

df -P $MOUNTS | awk 'NR>1 {sub("%","",$5); print $5, $6}' |
while read -r used mount; do
  if [ "$used" -ge "$LIMIT" ]; then
    curl -fsS -o /dev/null \
      -H "Authorization: Bearer $NTFY_TOKEN" \
      -H "Title: $(hostname): القرص $mount ممتلئ" \
      -H "Priority: high" \
      -H "Tags: warning,disk" \
      -d "المستخدم ${used}% والحد ${LIMIT}%" \
      "$NTFY_URL/alerts-prod"
    echo "alert sent for $mount (${used}%)"
  fi
done
```

نشغله مرة بالحد الافتراضي 85% فلا يرسل شيئاً، ثم بحد 10% حتى نرى التنبيه في التجربة:

```bash
export NTFY_URL=http://lab-notify-ntfy NTFY_TOKEN=tk_te8r1278ehop4irfy64f22bvihlcp
check-disk.sh; echo exit=$?
LIMIT=10 check-disk.sh
```

```text
exit=0
alert sent for / (13%)
```

وعلى السيرفر الحقيقي تضعه في Cron كل عشر دقائق، والمتغيرات في أول ملف crontab يقرؤها كل سطر بعدها، والملف لا يقرؤه إلا صاحبه:

```bash
crontab -e
```

```text
NTFY_URL=https://ntfy.example.com
NTFY_TOKEN=tk_te8r1278ehop4irfy64f22bvihlcp
*/10 * * * * /usr/local/bin/check-disk.sh
```

ولاحظ أن هذا السكربت سوف يرسل التنبيه نفسه كل عشر دقائق حتى تحل المشكلة، وهذا مقبول لتنبيه بهذه الأهمية، ولكنه مزعج لتنبيهات أقل أهمية، وهنا تبدأ الحاجة إلى أداة مراقبة حقيقية تتذكر الحالة وترسل عند التغير فقط، كما سوف نرى في [قسم المراقبة](#lab-monitoring). وماذا تفعل عندما يصل التنبيه؟ ذلك في [حالة القرص الممتلئ](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#case-disk) من دليل استكشاف الأخطاء.

### غلاف لأي مهمة: تنبيه عند الفشل فقط

أكثر التنبيهات فائدة هي التي تخبرك أن مهمة مجدولة فشلت، لأن الفشل الصامت للنسخ الاحتياطي يكتشف عادة يوم تحتاج إلى الاستعادة. والغلاف التالي يشغل أي أمر، ويحفظ مخرجه، ولا يرسل شيئاً إذا نجح، وإذا فشل يرسل رمز الخروج وآخر خمسة أسطر بأولوية عاجلة:

```bash
#!/usr/bin/env bash
# /usr/local/bin/notify-on-fail.sh: run any command, send its last lines only when it fails
#   notify-on-fail.sh backups "نسخة قاعدة البيانات" pg_dump ...
TOPIC="$1"; TITLE="$2"; shift 2
LOG=$(mktemp)
"$@" >"$LOG" 2>&1
CODE=$?
if [ "$CODE" -eq 0 ]; then
  rm -f "$LOG"; exit 0
fi
curl -fsS -o /dev/null \
  -H "Authorization: Bearer $NTFY_TOKEN" \
  -H "Title: $(hostname): فشل $TITLE" \
  -H "Priority: urgent" \
  -H "Tags: rotating_light" \
  --data-binary "رمز الخروج $CODE
$(tail -n 5 "$LOG")" \
  "$NTFY_URL/$TOPIC"
rm -f "$LOG"; exit "$CODE"
```

```bash
export NTFY_TOKEN=$BK
notify-on-fail.sh backups 'نسخة الملفات' tar czf /tmp/etc.tgz /etc; echo exit=$?
notify-on-fail.sh backups 'نسخة الملفات' tar czf /tmp/srv.tgz /srv/app/uploads; echo exit=$?
```

```text
exit=0
exit=1
```

الأولى نجحت ولم ترسل شيئاً، والثانية فشلت لأن المجلد غير موجود فأرسلت التنبيه وأعادت رمز الخروج كما هو، حتى يبقى Cron أو أي أداة فوقها تعرف أن المهمة فشلت. وفي سطر Cron يصبح الاستخدام هكذا:

```text
30 2 * * * /usr/local/bin/notify-on-fail.sh backups "نسخة قاعدة البيانات" /usr/local/bin/backup-db.sh
```

وقد شرحنا ما يجب أن يفعله سكربت النسخ نفسه واختبار الاستعادة في [النسخ الاحتياطي بقاعدة 3-2-1](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/).

⚠️

في أول نسخة كتبناها من هذا الغلاف قرأنا رمز الخروج بعد `if "$@"; then … fi`، فوصل التنبيه وفيه «رمز الخروج 0» والأمر قد فشل، والسبب أن `$?` بعد جملة `if` كاملة هو رمز الجملة نفسها وليس رمز الأمر. لذلك احفظ `CODE=$?` في السطر الذي يلي الأمر مباشرة كما في النسخة أعلاه، وجرب الغلاف بأمر يفشل عن قصد قبل أن تعتمد عليه.

### من خط CI: تنبيه بعد النشر

في خط CI على Gitea Actions تضع التوكن في أسرار المستودع Secrets باسم `NTFY_TOKEN`، ثم تضيف في نهاية مهمة النشر خطوتين، واحدة عند النجاح وأخرى بأولوية عالية عند الفشل، وقد شرحنا تشغيل الخطوط على سيرفرك في [Gitea Actions](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/):

```yaml
      - name: إشعار النشر
        if: success()
        env:
          NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }}
        run: |
          curl -fsS -H "Authorization: Bearer $NTFY_TOKEN" \
            -H "Title: نشر $GITHUB_REF_NAME إلى الإنتاج" -H "Tags: rocket" -H "Markdown: yes" \
            -H "Click: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA" \
            -d "**$GITHUB_REPOSITORY** نشره $GITHUB_ACTOR بنجاح" https://ntfy.example.com/deploys
      - name: إشعار فشل النشر
        if: failure()
        env:
          NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }}
        run: |
          curl -fsS -H "Authorization: Bearer $NTFY_TOKEN" -H "Priority: high" -H "Tags: x" \
            -d "فشل نشر $GITHUB_REPOSITORY ($GITHUB_REF_NAME)" https://ntfy.example.com/deploys
```

وفي التجربة شغلنا أمر curl نفسه من ال Container بتوكن `ci-bot` بعد أن وضعنا متغيرات الخط بأيدينا، ورابط Click فيها رابط تجريبي:

```bash
export NTFY_TOKEN=tk_dotg9thtddr07sbvgveinsg5rzlsn GITHUB_SERVER_URL=https://git.example.com \
  GITHUB_REPOSITORY=team/app GITHUB_RUN_ID=42 GITHUB_REF_NAME=v1.8.2 GITHUB_ACTOR=wajdy
curl -fsS -H "Authorization: Bearer $NTFY_TOKEN" -H "Title: نشر $GITHUB_REF_NAME إلى الإنتاج" \
  -H "Tags: rocket" -H "Markdown: yes" \
  -H "Click: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
  -d "**$GITHUB_REPOSITORY** نشره $GITHUB_ACTOR بنجاح" "$NTFY_URL/deploys"
```

```text
{"id":"xnXlgTseDHjJ","time":1791395272,"expires":1791438472,"event":"message","topic":"deploys","title":"نشر v1.8.2 إلى الإنتاج","message":"**team/app** نشره wajdy بنجاح","tags":["rocket"],"click":"https://git.example.com/team/app/actions/runs/42","content_type":"text/markdown"}
```

والخيار `-f` مهم في الخطوط، لأنه يجعل curl يفشل برمز خروج غير صفري إذا رد السيرفر بخطأ، فتعرف من سجل الخط أن التنبيه نفسه لم يرسل. وإذا كان التوكن سراً في الخط فاتبع ما شرحناه في [خط CI/CD آمن](https://arabroot.io/articles/%D8%AE%D8%B7-ci-cd-%D8%A7%D9%85%D9%86-%D9%85%D9%86-%D8%A7%D9%84%D8%A8%D8%AF%D8%A7%D9%8A%D8%A9-%D9%84%D9%84%D9%86%D9%87%D8%A7%D9%8A%D8%A9/) حتى لا يظهر في السجلات.

### من أدوات المراقبة: Alertmanager وGrafana وUptime Kuma

أدوات المراقبة ترسل تنبيهاتها كطلب Webhook بصيغة JSON خاصة بها، وntfy 2.x يحولها إلى رسالة مقروءة بقوالب مدمجة [Pre-defined Templates](https://docs.ntfy.sh/publish/?ref=arabroot.io#pre-defined-templates) اسمها `github` و`grafana` و`alertmanager`، تختار أحدها بإضافة `?template=` إلى الرابط. وفي Alertmanager يكون المستقبل Receiver هكذا، والخيار `authorization` في [إعداد Webhook](https://prometheus.io/docs/alerting/latest/configuration/?ref=arabroot.io#webhook%5Fconfig) يرسل التوكن في ترويسة Bearer:

```yaml
receivers:
  - name: ntfy
    webhook_configs:
      - url: https://ntfy.example.com/alerts-prod?template=alertmanager
        http_config:
          authorization:
            credentials: tk_te8r1278ehop4irfy64f22bvihlcp
```

ولم نشغل Alertmanager نفسه في هذه التجربة، وإنما أرسلنا من ال Container طلباً بصيغة Webhook الخاصة به من الملف التالي:

```json
{
  "version": "4",
  "status": "firing",
  "receiver": "ntfy",
  "groupLabels": { "alertname": "HighErrorRate" },
  "commonLabels": { "alertname": "HighErrorRate", "severity": "critical", "service": "api" },
  "commonAnnotations": { "summary": "API 5xx above 5% for 10 minutes" },
  "externalURL": "http://alertmanager:9093",
  "alerts": [
    {
      "status": "firing",
      "labels": { "alertname": "HighErrorRate", "severity": "critical", "service": "api", "instance": "web-01:8080" },
      "annotations": { "summary": "API 5xx above 5% for 10 minutes", "description": "5xx ratio is 7.3% on web-01" },
      "startsAt": "2026-10-07T15:20:00Z",
      "endsAt": "0001-01-01T00:00:00Z",
      "generatorURL": "http://prometheus:9090/graph?g0.expr=..."
    }
  ]
}
```

```bash
curl -s -H "Authorization: Bearer tk_te8r1278ehop4irfy64f22bvihlcp" --data-binary @/tmp/alert.json \
  "http://lab-notify-ntfy/alerts-prod?template=alertmanager"
```

```text
{"id":"DABmtI8qVmQt","time":1791395273,"expires":1791438473,"event":"message","topic":"alerts-prod","title":"🚨 Alert: HighErrorRate","message":"Status: Firing\nReceiver: ntfy\nAlert: HighErrorRate\nInstance: web-01:8080\nSeverity: critical\nStarts at: 2026-10-07T15:20:00ZEnds at: 0001-01-01T00:00:00Z\nSummary: API 5xx above 5% for 10 minutes\nDescription: 5xx ratio is 7.3% on web-01\nSource: http://prometheus:9090/graph?g0.expr=..."}
```

تحول JSON الطويل إلى عنوان ونص من سطور، ولاحظ أن القالب المدمج في هذا الإصدار يلصق سطر «Starts at» بسطر «Ends at»، وإذا أزعجك ذلك تستطيع أن تضع ملفاً بالاسم نفسه `alertmanager.yml` في مجلد القوالب `/etc/ntfy/templates` فيحل محل المدمج كما في التوثيق. وبالطريقة نفسها يستقبل ntfy تنبيهات Grafana بالقالب `grafana`، أما Uptime Kuma وHealthchecks وNetdata وغيرها فتدعم ntfy مباشرة كنوع إشعار كما في [صفحة التكاملات](https://docs.ntfy.sh/integrations/?ref=arabroot.io)، وكلها مخططة في مرحلة [المراقبة والسجلات](https://arabroot.io/start/#stage-observability) في خارطة الطريق.

### ماذا وصل إلى المشترك؟

الملف الذي كتبه اشتراك `ops` المفتوح منذ البداية فيه كل ما أرسلناه بالترتيب، وأول سطر حدث `open` عند فتح الاتصال:

```bash
cat /tmp/stream.jsonl
```

```text
{"id":"dkCVR5gjtE8l","time":1791395263,"event":"open","topic":"alerts-prod,backups,deploys"}
{"id":"ImdpMNw1ZVkX","time":1791395265,"expires":1791438465,"event":"message","topic":"backups","title":"نسخة قاعدة البيانات","message":"اكتملت النسخة الليلية: 1.4 GB في 6 دقائق","priority":2,"tags":["white_check_mark","backup"]}
{"id":"7Z5pPtzeMA7f","time":1791395269,"expires":1791438469,"event":"message","topic":"alerts-prod","title":"web-01: القرص / ممتلئ","message":"المستخدم 13% والحد 10%","priority":4,"tags":["warning","disk"]}
{"id":"I89EQgC0DSnJ","time":1791395270,"expires":1791438470,"event":"message","topic":"backups","title":"web-01: فشل نسخة الملفات","message":"رمز الخروج 1\ntar: /srv/app/uploads: No such file or directory\ntar: error exit delayed from previous errors","priority":5,"tags":["rotating_light"]}
{"id":"xnXlgTseDHjJ","time":1791395272,"expires":1791438472,"event":"message","topic":"deploys","title":"نشر v1.8.2 إلى الإنتاج","message":"**team/app** نشره wajdy بنجاح","tags":["rocket"],"click":"https://git.example.com/team/app/actions/runs/42","content_type":"text/markdown"}
{"id":"DABmtI8qVmQt","time":1791395273,"expires":1791438473,"event":"message","topic":"alerts-prod","title":"🚨 Alert: HighErrorRate","message":"Status: Firing\nReceiver: ntfy\nAlert: HighErrorRate\nInstance: web-01:8080\nSeverity: critical\nStarts at: 2026-10-07T15:20:00ZEnds at: 0001-01-01T00:00:00Z\nSummary: API 5xx above 5% for 10 minutes\nDescription: 5xx ratio is 7.3% on web-01\nSource: http://prometheus:9090/graph?g0.expr=..."}
```

والهاتف الذي كان مطفأ لا يفقد شيئاً، لأن السيرفر يحفظ الرسائل 12 ساعة، والتطبيق يطلب عند اتصاله ما فاته بالمعامل `since`، والطلب التالي يجلب رسائل النسخ الاحتياطي في آخر ساعة:

```bash
curl -s -u ops:Ops-Lab-2026 "http://lab-notify-ntfy/backups/json?poll=1&since=1h" | jq -c "{time,title,priority}"
```

```text
{"time":1791395265,"title":"نسخة قاعدة البيانات","priority":2}
{"time":1791395270,"title":"web-01: فشل نسخة الملفات","priority":5}
```

وهذا الحفظ له جانب يجب أن تعرفه، فالرسائل مكتوبة على قرص السيرفر في `cache.db`، والتوثيق نفسه في [قسم ال Cache](https://docs.ntfy.sh/publish/?ref=arabroot.io#message-caching) يذكر أن هذا قد يثير مخاوف الخصوصية، فالرسالة التي لا تريد حفظها ترسلها بالترويسة `Cache: no` فتصل إلى المتصلين الآن فقط. وأخيراً سجلنا الدخول إلى واجهة الويب بحساب `ops` واشتركنا في المواضيع الثلاثة، والصورة التالية من التجربة نفسها:

![واجهة ntfy في المتصفح بعد الدخول بحساب ops، في القائمة الجانبية المواضيع alerts-prod وbackups وdeploys، وفي الوسط ثلاثة إشعارات: تنبيه Alertmanager باسم HighErrorRate بسطوره، وإشعار نشر v1.8.2 إلى الإنتاج مع زري نسخ الرابط وفتحه، وإشعار فشل نسخة الملفات بأولوية عاجلة ورمز الخروج 1](https://arabroot.io/content/images/2026/10/ntfy-05-web-app.webp)

واجهة الويب في تجربتنا بحساب يقرأ فقط

### إزالة التجربة

```bash
docker rm -f lab-notify-ntfy lab-notify-client
docker volume rm lab-notify-ntfy-data
docker network rm lab-notify-net
```

## قبل الإنتاج: HTTPS وال Reverse Proxy

في الإنتاج يعمل ntfy خلف Reverse Proxy يتولى الشهادة، وتغير في `server.yml` ثلاثة أشياء: `base-url` يصبح رابط HTTPS، و`listen-http` يستمع على `127.0.0.1` فقط، و`behind-proxy: true` حتى يأخذ عنوان الزائر الحقيقي من `X-Forwarded-For` فيطبق حدود الطلبات على كل زائر وليس على ال Proxy كله، وقد شرحنا هذه المشكلة في [تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/). ومثال Nginx في [التوثيق](https://docs.ntfy.sh/config/?ref=arabroot.io#nginxapache2caddy) يوقف التخزين المؤقت `proxy_buffering off` ويمرر ترويستي `Upgrade` و`Connection` حتى تعمل اتصالات WebSocket التي يعتمد عليها تطبيق Android، ويرفع مهلة القراءة إلى 3 دقائق، أما Caddy فيكفيه سطر واحد لأنه يمرر WebSocket وحده:

```text
ntfy.example.com {
    reverse_proxy 127.0.0.1:2586
}
```

وإذا كان في فريقك من يستخدم iPhone فأضف `upstream-base-url: "https://ntfy.sh"` كما شرحنا في [قسم الهاتف](#phones)، مع العلم أن معرف الرسالة والهاش يمران حينها على ntfy.sh وGoogle وApple.

## من يراقب سيرفر التنبيهات نفسه؟

هناك مشكلة رئيسية في كل نظام تنبيهات: السكون. فإذا توقف السيرفر الذي يشغل Cron وntfy معاً فلن يصلك أي تنبيه، وهذا يشبه تماماً حالة «كل شيء بخير»، والصورة التالية تبين المشكلة والحل:

![لوحتان: على اليسار سيرفر web-01 عليه التطبيق وCron وntfy معاً، والخط إلى الهاتف مقطوع بعلامة حمراء مكتوب عليها السيرفر متوقف يعني السكون، وعلى اليمين جهاز مراقبة على سيرفر آخر يتوقع نبضة من web-01 ويفحص /v1/health في سيرفر ntfy ويرسل إلى الهاتف عبر قناة ثانية مثل البريد](https://arabroot.io/content/images/2026/10/ntfy-04-watch.webp)

غياب التنبيه ليس دليلاً على أن كل شيء يعمل

- **لا تشغل ntfy على السيرفر الذي يراقبه:** ضعه على سيرفر صغير منفصل، أو على الأقل لا تجعله يعتمد على قاعدة البيانات أو القرص نفسه.
- **نبضة منتظمة Heartbeat:** بدل أن تنتظر رسالة الفشل، اجعل المهمة المهمة ترسل «أنا حي» بعد كل نجاح إلى أداة على جهاز آخر تتوقعها، فإذا تأخرت النبضة صرخت الأداة، وهذا ما تفعله Uptime Kuma بنوع Push Monitor وHealthchecks.
- **فحص ntfy نفسه:** المسار `/v1/health` يرد `{"healthy":true}` كما رأينا، وتستطيع أن تفعل مسار `/metrics` لPrometheus بالخيار `enable-metrics` كما في [قسم المراقبة](https://docs.ntfy.sh/config/?ref=arabroot.io#monitoring).
- **قناة ثانية للأهم:** التنبيه الذي لا تحتمل ضياعه يذهب أيضاً إلى البريد، فإذا تعطلت قناة بقيت الأخرى.

وكيف تحقق في المشكلة بعد أن يصل التنبيه؟ ذلك في [استكشاف الأخطاء وتحليل الأداء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، والسجلات التي تحتاجها حينها في [السجلات Logging للمطورين](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/).

## أخطاء شائعة

- **ترك `auth-default-access` على قيمته الافتراضية:** القيمة الافتراضية `read-write`، فالسيرفر الذي على الإنترنت دون هذا السطر يقبل الكتابة والقراءة من أي شخص.
- **توكن واحد لكل السكربتات:** التوكن يحمل صلاحيات صاحبه كاملة، فاجعل لكل مرسل حساباً بصلاحية كتابة في موضوعه فقط.
- **تنبيه كل عشر دقائق للمشكلة نفسها:** الفريق يتعلم تجاهل التنبيهات المتكررة، فاحفظ التنبيهات العاجلة للأمور العاجلة، واستخدم الأولويات `low` و`min` للمعلومات.
- **أسرار في نص التنبيه:** كلمة مرور في سطر خطأ تصل إلى كل هاتف مشترك وتحفظ 12 ساعة على القرص، فانظر ما يكتبه سكربتك قبل أن ترسل آخر سطوره.
- **Reverse Proxy دون WebSocket:** يظهر في تطبيق Android خطأ اتصال أو تتأخر الرسائل، والسبب ترويستا `Upgrade` و`Connection` غير ممررتين أو مهلة قراءة قصيرة.
- **عنوان عربي يظهر علامات استفهام:** بعض المكتبات ولغات البرمجة لا ترسل UTF-8 في الترويسات، والتوثيق يقترح ترميز العنوان بصيغة RFC 2047 مثل `=?UTF-8?B?…?=`، أما curl على Linux فيرسله كما هو كما رأينا.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- ntfy سيرفر نشر واشتراك عبر HTTP، فأي سكربت يرسل تنبيهاً بسطر curl واحد، والمشتركون هم تطبيق الهاتف وواجهة الويب وأي برنامج يقرأ تدفق JSON.
- على Android يصل التنبيه من سيرفرك مباشرة دون Google، وعلى iOS يمر معرف الرسالة فقط عبر ntfy.sh وFCM وAPNs ثم يجلب التطبيق النص من سيرفرك.
- السيرفر الخاص يبدأ ب `auth-default-access: "deny-all"`، ثم حساب لكل مرسل بصلاحية كتابة في موضوعه وتوكن خاص به، وحساب للقراءة لفريق التشغيل.
- أنفع التنبيهات هي فشل المهام المجدولة، وغلاف صغير يرسل عند الفشل فقط ويعيد رمز الخروج كما هو يكفي لكل مهام Cron.
- أدوات المراقبة ترسل إلى ntfy بقوالب جاهزة لAlertmanager وGrafana أو بدعم مباشر، ولكن سيرفر التنبيهات نفسه يحتاج إلى من يراقبه من مكان آخر.

[![](https://arabroot.io/content/images/2026/10/push-basics-cover-cover.webp)دليلكيف تعمل الإشعارات الفورية Push Notifications؟كيف يصل الإشعار إلى تطبيق مغلق عبر خدمات Apple وGoogle، وما الذي تراه هذه الخدمات من بيانات مستخدميك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%A7%D8%B4%D8%B9%D8%A7%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D9%88%D8%B1%D9%8A%D8%A9-push-notifications/)[![](https://arabroot.io/content/images/2026/10/backup-321-cover-cover.webp)دليلالنسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادةكيف تحدد كم بيانات تقبل أن تخسر، وتبني نسخاً لا يستطيع المخترق حذفها، وتتأكد أنك تستطيع استعادتها فعلاً.](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/)[![](https://arabroot.io/content/images/2026/10/troubleshooting-guide-cover-cover.webp)دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/logging-basics-cover-cover.webp)دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/gitea-actions-cover-cover-2.webp)دليلGitea Actions: تشغيل CI/CD وبناء Docker Images على سيرفركاجعل سيرفرك يختبر الكود ويبني Docker Images تلقائياً مع كل push، دون أن يخرج كودك أو أسرارك إلى خدمة CI خارجية.](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ تجربة ntfy 2.28.0 في Docker بسيرفر خاص وخمسة حسابات وثلاثة توكنات، والإرسال من Container على Alpine 3.24 مع curl 8.22.0 من سكربت Cron وغلاف للمهام وخطوة CI وقالب Alertmanager.