لنفرض أن لديك سيرفراً تشغل عليه عدة تطبيقات، فسوف تجد أن كل تطبيق يستمع على منفذ Port داخلي خاص به، هذا على 3000 وذاك على 8080، والحل البسيط الذي يخطر على البال هو أن تفتح هذه المنافذ وتعطي المستخدمين العنوان http://203.0.113.10:8080. ولكن هذا الحل غير مناسب إطلاقاً، فأنت لا تريد أن يكتب المستخدم رقم منفذ في المتصفح، ولا أن تفتح عشرة منافذ على الإنترنت، ولا أن تدير شهادة TLS Certificate لكل تطبيق على حدة.
وهذه المشكلات كلها يحلها ال Reverse Proxy، وهو خدمة واحدة تستقبل كل الطلبات على المنفذين 80 و443، فتقرأ اسم النطاق Domain في كل طلب وتوجهه إلى ال Container المقصود، وفي نفس الوقت تتولى عنك HTTPS وتجديد الشهادات Certificate Renewal.
Nginx Proxy Manager واختصاراً NPM يضع سيرفر Nginx خلف واجهة ويب Web UI بسيطة، حيث تضيف النطاق واسم ال Container ومنفذه، ثم تطلب شهادة من Let's Encrypt بنقرة واحدة، وتقصر الوصول على من يملك كلمة مرور أو على عناوين IP محددة. لذلك فهو يناسب من يفضل إدارة النطاقات من المتصفح، والفرق التي لا تريد كتابة ملفات إعداد Configuration Files الخاصة ب Nginx بيدها.
أما إذا كنت تفضل أن يكون الإعداد ملفاً نصياً في Git تراجعه وتنشره آلياً، فأدوات مثل Caddy أو Traefik أنسب لك، فملف Caddy من سطرين يؤدي نفس الغرض.
وسوف نناقش في هذا المقال ما يلي:
- تثبيت NPM على شبكة Docker مشتركة بحيث لا تنشر تطبيقاتك أي منفذ على السيرفر، مع إبقاء واجهة الإدارة بعيدة عن الإنترنت.
- ربط تطبيق بنطاقه عبر Proxy Host، وطلب شهادة من Let's Encrypt بالتحقق عبر HTTP-01 أو عبر DNS-01، ثم شهادة واحدة Wildcard لكل النطاقات الفرعية.
- حماية اللوحات الداخلية بقوائم الوصول Access Lists، وتأمين واجهة NPM نفسها.
- التحقق والنسخ الاحتياطي والتحديث، وأشهر المشكلات وحلولها.
ما تحتاجه قبل أن تبدأ Requirements
- سيرفر Linux عليه Docker وCompose، وإذا لم يكن جاهزاً فاتبع دليل تثبيت Docker على Ubuntu.
- الموارد Resources: يستهلك NPM نحو 100 إلى 150 ميجابايت من الذاكرة RAM، ويكفيه قدر صغير من المساحة للسجلات Logs والشهادات.
- نطاق تتحكم في سجلات DNS Records الخاصة به، حيث يحتاج كل نطاق فرعي Subdomain مثل
app.example.comإلى سجلAيشير إلى العنوان العام Public IP للسيرفر203.0.113.10، وسجلAAAAإن كان لديك IPv6، ومع سجل شامل Wildcard Record مثل*.example.comلا تحتاج إلى سجل لكل خدمة، وقد شرحنا هذه السجلات في شرح DNS وسجلاته للمبتدئين. - المنفذان
80/tcpو443/tcpمفتوحان من الإنترنت إلى السيرفر، ولاحظ أن التحقق عبر HTTP (HTTP Challenge) في Let's Encrypt يحتاج إلى المنفذ 80 حتى لو كانت كل خدماتك تعمل عبر HTTPS. - المنفذ
81/tcpهو منفذ واجهة الإدارة Admin UI، ولن نفتحه على الإنترنت.
التثبيت
سوف نضع NPM وكل التطبيقات التي يخدمها على شبكة Docker مشتركة Docker Network اسمها proxy، وعلى هذه الشبكة يصل NPM إلى كل Container باسمه (whoami، gitea...)، وبالتالي لا تحتاج التطبيقات إلى نشر أي منفذ على السيرفر، ويبقى ال Reverse Proxy هو الطريق الوحيد إليها، وهذا ما يوصي به التوثيق الرسمي. وإذا أردت أن تفهم لماذا نفعل ذلك وما أنواع الشبكات في Docker، ولماذا نضع قاعدة البيانات على شبكة داخلية خاصة بها، فقد شرحنا ذلك بالتفصيل في دليل شبكات Docker وأفضل الممارسات. أنشئ الشبكة مرة واحدة:
docker network create proxyبعد ذلك أنشئ مجلد الخدمة:
sudo mkdir -p /opt/npm
sudo chown $USER: /opt/npm
cd /opt/npmوالملف /opt/npm/compose.yaml سوف يكون كما يلي:
services:
npm:
image: jc21/nginx-proxy-manager:2.16.0
container_name: npm
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "127.0.0.1:81:81"
environment:
TZ: UTC
volumes:
- npm_data:/data
- npm_letsencrypt:/etc/letsencrypt
networks:
- proxy
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 30s
timeout: 5s
retries: 3
volumes:
npm_data:
name: npm_data
npm_letsencrypt:
name: npm_letsencrypt
networks:
proxy:
external: trueفي الإعداد أعلاه لاحظ التالي:
- الإعداد
127.0.0.1:81:81ينشر واجهة الإدارة على السيرفر نفسه فقط، وسوف نصل إليها أولاً عبر نفق SSH Tunnel، ثم نضعها خلف NPM نفسه بنطاق وشهادة وقائمة وصول Access List. - ال Volume
npm_dataفيه قاعدة بيانات Database من نوع SQLite، وملفات Nginx التي يولدها NPM، والسجلات، والشهادات المخصصة Custom Certificates، أماnpm_letsencryptففيه شهادات Let's Encrypt ومفاتيح الحساب Account Keys، لذلك أدرج الاثنين في النسخ الاحتياطي Backup. - ال Image فيها فحص صحة Healthcheck جاهز هو
/usr/bin/check-health، ولكنه معطل افتراضياً، فنفعله هنا. - إذا لم يكن IPv6 مفعلاً على السيرفر فأضف
DISABLE_IPV6: "true"إلىenvironmentكما يشرح التوثيق، والسبب أن Nginx سوف يفشل في الاستماع على[::]بدونه.
الآن شغل الخدمة وانتظر حتى تصبح حالتها healthy:
docker compose up -d
docker compose ps
docker compose logs --tail 20 npmوالمخرج سوف يكون كما يلي:
NAME IMAGE STATUS
npm jc21/nginx-proxy-manager:2.16.0 Up 2 minutes (healthy)
...
[Global ] › ℹ info Backend PID 231 listening on port 3000 ...أول دخول وإنشاء حساب المدير
من جهازك افتح نفق SSH إلى المنفذ 81 على السيرفر، ثم افتح http://localhost:8181 في المتصفح:
ssh -L 8181:127.0.0.1:81 [email protected]إذا كنت قد قرأت شروحات قديمة فسوف تبحث عن الحساب الافتراضي Default Account المعروف [email protected] وكلمة المرور changeme، ولكن الإصدارات الحديثة لم تعد تأتي به، وإنما تظهر بدلاً منه شاشة ترحيب تطلب منك إنشاء حساب المدير Admin Account مباشرة: الاسم والبريد وكلمة مرور قوية.

وإذا كنت تنشر NPM آلياً وتريد تخطي هذه الشاشة، فيمكنك إنشاء المدير عند التشغيل الأول من متغيري البيئة Environment Variables INITIAL_ADMIN_EMAIL وINITIAL_ADMIN_PASSWORD كما يبين التوثيق الرسمي. وبعد الدخول فعل التحقق بخطوتين Two-Factor Authentication من إعدادات حسابك.
كيف تربط تطبيقاً بنطاقه عبر Proxy Host؟
لنأخذ مثالاً عملياً، وسوف نشغل تطبيقاً صغيراً على نفس الشبكة هو whoami، وهذا التطبيق يعيد تفاصيل الطلب كما وصلته، وبالتالي سوف ترى الترويسات Headers التي يمررها ال Reverse Proxy. وهذا ملفه /opt/whoami/compose.yaml، ولاحظ أنه لا ينشر أي منفذ:
services:
whoami:
image: traefik/whoami:v1.12.0
container_name: whoami
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: truecd /opt/whoami
docker compose up -dفي NPM افتح Hosts ثم Proxy Hosts ثم Add Proxy Host، واملأ تبويب Details كما يلي:
| الحقل | القيمة | السبب |
|---|---|---|
| Domain Names | app.example.com | اكتب الاسم ثم اضغط Enter، ويمكنك إضافة أكثر من اسم لنفس المضيف Host |
| Scheme | http | البروتوكول Protocol بين NPM وال Container، وليس بين المتصفح وNPM |
| Forward Hostname / IP | whoami | اسم ال Container على شبكة proxy |
| Forward Port | 80 | المنفذ داخل ال Container، وليس المنفذ المنشور على السيرفر |
| Block Common Exploits | مفعل | يحجب أنماط الهجوم الشائعة في الروابط |
| Websockets Support | مفعل | تحتاجه تطبيقات المحادثة واللوحات الحية والطرفيات Terminals |

احفظ، فيولد NPM ملف Nginx الخاص بالمضيف ويعيد التحميل Reload خلال ثانية. وقد يتساءل البعض: هل يجب أن أنتظر انتشار DNS Propagation حتى أختبر؟ والإجابة لا، فيمكنك الاختبار من السيرفر نفسه بتمرير اسم النطاق في الترويسة:
curl -s -H 'Host: app.example.com' http://127.0.0.1/والمخرج سوف يكون كما يلي:
Hostname: 570cbf988f52
...
Host: app.example.com
X-Forwarded-For: 172.25.0.1
X-Forwarded-Proto: http
X-Forwarded-Scheme: http
X-Real-Ip: 172.25.0.1ومن الترويسات X-Forwarded-For وX-Real-IP وX-Forwarded-Proto يعرف التطبيق عنوان الزائر والبروتوكول الأصلي، وسوف نعود إليها في قسم المشكلات.
كيف تحصل على شهادة TLS لكل نطاق؟
افتح المضيف للتعديل من القائمة ⋮ بجانبه ثم Edit، وانتقل إلى تبويب SSL، ومن حقل SSL Certificate اختر Request a new Certificate. بعد ذلك فعل Force SSL حتى يتحول كل طلب HTTP إلى HTTPS، وفعل HTTP/2 Support أيضاً.
أما خيار HSTS فلا تفعله إلا بعد أن تتأكد أن الشهادة تعمل وتتجدد، والسبب أنه يطلب من المتصفح ألا يستخدم HTTP مع هذا النطاق مدة طويلة، وهي سنتان في إعداد NPM، والتراجع عنه صعب.
ولاحظ أن هذا الخيار يضيف preload دائماً، وتجد كيف تكتب HSTS بقيمتك أنت مع بقية ترويسات الأمان في دليل ترويسات الأمان بالتفصيل: HSTS وCSP.

التحقق عبر HTTP (HTTP-01 Challenge): الخيار الافتراضي
لن تمنحك Let's Encrypt شهادة قبل أن تثبت أنك تتحكم في النطاق، ففي طريقة التحقق HTTP-01 يضع Certbot المدمج في NPM ملفاً مؤقتاً، ثم تطلبه سيرفرات Let's Encrypt من http://app.example.com/.well-known/acme-challenge/.... ولهذه الطريقة شروط وحدود:
- أن يشير سجل DNS للنطاق إلى هذا السيرفر فعلاً وأن يكون قد انتشر، وتحقق من ذلك بالأمر
dig +short app.example.com. - أن يكون المنفذ 80 مفتوحاً من الإنترنت كله، وأن يصل إلى NPM وليس إلى جهاز آخر في الطريق.
- لا تصلح هذه الطريقة لشهادات Wildcard مثل
*.example.com.
وهذه أبسط طرق التحقق وتكفي في أغلب الحالات، لذلك اضغط Save، فيطلب NPM الشهادة ويجددها تلقائياً قبل انتهائها بثلاثين يوماً.
التحقق من ملكية النطاق عبر ال DNS (DNS-01 Challenge): للشهادات الشاملة والسيرفرات الداخلية
في طريقة التحقق DNS-01 يثبت Certbot ملكيتك للنطاق بسجل TXT باسم _acme-challenge.example.com يضيفه عبر ال API الخاص بمزود DNS Provider، وهذه الطريقة لا تحتاج إلى أي منفذ مفتوح، لذلك هي الخيار الوحيد للشهادات الشاملة Wildcard Certificates، وللسيرفرات الداخلية أو المنزلية التي لا يصل إليها الإنترنت. فعل Use DNS Challenge واختر المزود، ثم الصق بيانات الاعتماد Credentials بالصيغة التي تعرضها الواجهة، وتجد إضافات DNS Plugins المتاحة في توثيق NPM الخاص ب Certbot:

npm_data تحتوي هذا الرمز أيضاً.هل لديك نطاقات فرعية كثيرة؟ شهادة واحدة تكفي (Wildcard Certificate)
لنفرض أن لديك عشر خدمات على السيرفر، لكل منها نطاق فرعي مثل git.example.com وwiki.example.com وstatus.example.com، فالطريقة التي رأيناها حتى الآن تطلب شهادة مستقلة لكل Proxy Host، وهذا يعمل، ولكن مع كثرة الخدمات سوف تجد في صفحة Certificates عشر شهادات تتجدد كل منها في موعد مختلف، وكل خدمة جديدة تعني طلباً جديداً إلى Let's Encrypt يحسب من حدود عدد الطلبات. والحل الأنسب هنا هو الشهادة الشاملة Wildcard Certificate، وهي شهادة واحدة باسم *.example.com تغطي كل نطاق فرعي تحت النطاق الرئيسي Root Domain، فتطلبها مرة واحدة ثم تختارها في كل Proxy Host.
ولكن لاحظ أمرين قبل أن تطلبها، الأول أن النجمة تغطي مستوى واحداً فقط، فالشهادة *.example.com تصلح لـ a.example.com ولا تصلح لـ a.b.example.com، ولا تغطي النطاق الرئيسي example.com نفسه، لذلك نضيفه إلى الشهادة نفسها كاسم ثان. والثاني أن Let's Encrypt لا تصدر شهادات Wildcard إلا عبر DNS-01، والسبب أن الشهادة تغطي كل الأسماء تحت النطاق، فلا يكفي أن تثبت أنك تتحكم في سيرفر واحد يرد على اسم واحد، وإنما يجب أن تثبت أنك تتحكم في سجلات DNS للنطاق نفسه.
الطريقة الأولى: شهادة من Let's Encrypt عبر DNS Challenge. سوف نأخذ Cloudflare مثالاً، والخطوات نفسها مع أي مزود آخر في القائمة، وما يتغير هو صيغة بيانات الاعتماد فقط:
- في لوحة Cloudflare افتح My Profile ثم API Tokens، وأنشئ رمزاً API Token بصلاحية
Zone:DNS:Editعلى منطقة النطاقexample.comوحدها، كما يطلب توثيق إضافة certbot-dns-cloudflare، ولا تستخدم المفتاح العام Global API Key، والسبب أنه يفتح حسابك كله لمن يحصل عليه. - في NPM افتح Certificates ثم Add Certificate، واختر Let's Encrypt via DNS.
- في حقل Domain Names اكتب
*.example.comثم اضغط Enter، ثمexample.comثم Enter. - من حقل DNS Provider اختر Cloudflare، أو مزودك إن كان غيره، فالقائمة فيها عشرات المزودين.
- في حقل Credentials File Content ضع السطر
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567بعد أن تستبدل القيمة بالرمز الذي أنشأته. - اترك Propagation Seconds فارغاً حتى تستخدم الإضافة قيمتها الافتراضية، وإذا فشل التحقق لأن السجل لم ينتشر بعد فاكتب فيه عدد ثوان أكبر وحاول مرة أخرى، ثم اضغط Save.
في الإعداد أعلاه لاحظ التالي:
- الاسمان في شهادة واحدة، فتغطي الشهادة النطاق الرئيسي وكل نطاق فرعي من مستوى واحد تحته.
- تعرض الواجهة تحت حقل بيانات الاعتماد العبارة This data will be stored as plaintext in the database and in a file!، أي أن الرمز محفوظ نصاً غير مشفر، لذلك ينطبق عليه التحذير السابق عن أضيق صلاحية ممكنة.
- يجدد NPM الشهادة تلقائياً قبل انتهائها، ويستخدم كل مضيف مرتبط بها الشهادة الجديدة دون أي خطوة منك.
بعد ذلك افتح كل Proxy Host ثم تبويب SSL، واختر من حقل SSL Certificate الشهادة الشاملة بدلاً من Request a new Certificate، ثم فعل Force SSL كما سبق، ومن الآن فإن أي خدمة جديدة تحتاج فقط إلى Proxy Host تختار فيه الشهادة نفسها، دون طلب جديد إلى Let's Encrypt.
وعلى جانب DNS أضف سجلاً شاملاً من نوع A باسم *، أي *.example.com، يشير إلى 203.0.113.10، فيصل أي نطاق فرعي إلى السيرفر دون أن تضيف سجلاً لكل خدمة، أما النطاق الرئيسي فيحتاج إلى سجل خاص به. وإذا كان النطاق على Cloudflare مع تفعيل ال Proxy (السحابة البرتقالية) فاجعل وضع التشفير SSL/TLS Encryption Mode هو Full (strict)، والسبب أن Cloudflare يتحقق عندها من شهادة سيرفرك، أما وضع Flexible فيسبب حلقة التحويل التي نشرحها في قسم المشكلات، وطريقة الحصول على عنوان الزائر الحقيقي خلف Cloudflare تجدها هناك أيضاً.
الطريقة الثانية: رفع شهادة مخصصة Custom Certificate. إذا كانت كل نطاقاتك تمر دائماً عبر Cloudflare مع تفعيل ال Proxy، فيمكنك أن تصدر من لوحة Cloudflare، من SSL/TLS ثم Origin Server ثم Create Certificate، شهادة Origin CA تغطي *.example.com وexample.com، وتصل مدتها إلى 15 سنة فلا تحتاج إلى تجديد، ولا تحتاج إلى وضع أي رمز API داخل NPM. ولكن هذه الشهادة لا يثق بها إلا Cloudflare، والمتصفحات لا تعرفها، لذلك إذا أوقفت ال Proxy على أي نطاق فرعي أو أوقفت Cloudflare مؤقتاً فسوف يرى الزوار خطأ شهادة غير موثوقة، كما ينبه توثيق Cloudflare. ويمكنك كذلك أن تشتري شهادة Wildcard من جهة إصدار تجارية Certificate Authority، فتعمل في كل المتصفحات، ولكنك تتولى تجديدها ورفعها بنفسك قبل انتهائها.
ولرفع أي منهما افتح في NPM صفحة Certificates ثم Add Certificate ثم Custom Certificate، واكتب اسماً للشهادة في Name، ثم ارفع ملف المفتاح الخاص في Certificate Key، وملف الشهادة في Certificate، وملف الشهادات الوسيطة في Intermediate Certificate إن وجد، ولاحظ أن NPM لا يقبل مفتاحاً محمياً بعبارة مرور Passphrase كما تنبه الواجهة. بعد ذلك تختارها في تبويب SSL لكل Proxy Host كما في الطريقة الأولى.
وقد يتساءل البعض: أي الطريقتين أختار؟ والإجابة أن Let's Encrypt عبر DNS Challenge هي الأنسب في أغلب الحالات، لأنها تعمل في كل المتصفحات وتتجدد وحدها، أما شهادة Origin CA فتناسبك إذا كانت كل نطاقاتك خلف Cloudflare دائماً ولا تريد أن تضع رمز API في NPM.
حدود عدد الطلبات Rate Limits في Let's Encrypt
تضع Let's Encrypt حدوداً على عدد الشهادات وعلى المحاولات الفاشلة لكل نطاق، لذلك إذا فشل الطلب فلا تكرر الضغط على Save على أمل أن ينجح في المرة الخامسة، وإنما اقرأ السبب أولاً في Logs أو في سجل ال Container ثم أصلح DNS أو المنفذ، وسوف تجد رسالة Certbot كاملة بالأمر docker compose logs npm | grep -i -A5 certbot.
كيف تحمي اللوحات الداخلية بقوائم الوصول Access Lists؟
لوحات الإدارة وأدوات المراقبة Monitoring وبيئات الاختبار Staging يجب ألا تكون متاحة للعموم، وقائمة الوصول Access List في NPM تجمع لحمايتها طبقتين: مستخدمين بكلمات مرور عبر HTTP Basic Auth، وقواعد IP للسماح والمنع (allow/deny). افتح Access Lists ثم Add Access List:
- تبويب Details: اكتب الاسم (
team-only). وخيار Satisfy Any يعني أن إحدى الطبقتين تكفي وفق توجيه satisfy في Nginx، فمن جاء من عنوان مسموح يدخل مباشرة، ومن جاء من غيره يطلب منه NPM كلمة المرور، وبدون هذا الخيار يلزم الزائر أن يستوفي الشرطين معاً. أما Pass Auth to Upstream فيمرر ترويسة Authorization إلى التطبيق، لذلك اتركه معطلاً ما لم يحتج التطبيق إليها. - تبويب Authorizations: أضف المستخدمين وكلمات المرور.
- تبويب Rules: أضف العناوين أو الشبكات المسموحة مثل شبكة المكتب أو ال VPN، ولاحظ أنك بمجرد أن تضيف قاعدة واحدة يضيف NPM القاعدة
deny allفي النهاية.


بعد ذلك اربط القائمة بالمضيف من حقل Access List في تبويب Details، ثم اختبر من عنوان خارج الشبكة المسموحة، مرة بدون كلمة المرور ومرة معها:
curl -s -o /dev/null -w '%{http_code}\n' https://app.example.com/
curl -s -o /dev/null -w '%{http_code}\n' -u 'team:كلمة-المرور' https://app.example.com/والمخرج سوف يكون كما يلي:
401
200وسوف تعرض قائمة المضيفين وجهة كل مضيف ونوع شهادته وقائمة الوصول المرتبطة به وحالته:

Websockets
تطبيقات مثل Mattermost وUptime Kuma والطرفيات في Portainer تفتح اتصال WebSocket يبقى مفتوحاً، وحتى يعمل هذا الاتصال يجب أن يمرر ال Reverse Proxy الترويستين Upgrade وConnection، وهذا ما يفعله خيار Websockets Support. وللتحقق أرسل طلب ترقية Upgrade Request يدوياً، والنتيجة المتوقعة هي 101 Switching Protocols:
curl -s -i -N --max-time 3 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' https://app.example.com/echo | head -n 1HTTP/1.1 101 Switching Protocolsوإذا عاد الرمز 400 أو 426، أو انقطع الاتصال بعد 60 ثانية، فالخيار غير مفعل، أو أن في الطريق Proxy آخر لا يمرر الترقية.
كيف تؤمن واجهة NPM نفسها؟
فتح نفق SSH في كل مرة يصبح مرهقاً بسرعة، لذلك أضف مضيفاً لواجهة الإدارة بالنطاق npm.example.com، واجعل Forward Hostname هو npm، وهو اسم ال Container نفسه، والمنفذ 81، ثم أضف شهادة وقائمة وصول مقصورة على عناوين الفريق أو ال VPN. وبعد أن يعمل أبق المنفذ 81 منشوراً على 127.0.0.1 فقط احتياطاً.
وهنا قد يبدو خيار Satisfy Any مريحاً، ولكن لا تفعله في قائمة هذه الواجهة، وإنما اطلب العنوان المسموح وكلمة المرور معاً، والسبب أن NPM يثق بترويسة X-Real-IP إذا جاء الطلب من شبكة Docker الداخلية (172.16.0.0/12). فإذا كان للنطاق سجل AAAA وشبكة Docker لا تدعم IPv6، يمرر Docker طلبات IPv6 عبر ال userland proxy، فتصل إلى NPM من عنوان داخلي، وعندها يستطيع أي زائر أن يرسل عنواناً مسموحاً في هذه الترويسة فيتجاوز قواعد IP.
كيف تتأكد أن كل شيء يعمل؟ Verification
- الأمر
docker compose psيعرض ال Containernpmبحالةhealthy. - الأمر
curl -I http://app.example.comيعود بالرمز301إلىhttps://. - الأمر
curl -I https://app.example.comيعود بالرمز200، أو بالرمز401إن كانت هناك قائمة وصول. - الأمر
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -issuer -datesيعرض تفاصيل الشهادة، ومنها جهة الإصدار Issuer وهي Let's Encrypt وتاريخ الانتهاء. - العنوان
http://203.0.113.10:81لا يستجيب من خارج السيرفر. - الشهادة تظهر في صفحة Certificates وتنتهي بعد نحو 90 يوماً، ثم تتجدد بعد شهرين دون تدخل منك.
النسخ الاحتياطي والاستعادة Restore
حالة NPM كلها محفوظة في ال Volumes npm_data وnpm_letsencrypt، لذلك أوقف ال Container لحظات حتى تحصل على نسخة متسقة من قاعدة SQLite:
cd /opt/npm
docker compose stop npm
docker run --rm -v npm_data:/data:ro -v npm_letsencrypt:/letsencrypt:ro -v "$PWD":/backup alpine:3.24 tar czf /backup/npm-$(date +%F).tar.gz -C / data letsencrypt
docker compose start npmوللاستعادة على سيرفر جديد أنشئ الشبكة وال Volumes، ثم فك النسخة داخلها وشغل الخدمة بنفس الملف:
docker network create proxy
docker volume create npm_data
docker volume create npm_letsencrypt
docker run --rm -v npm_data:/data -v npm_letsencrypt:/letsencrypt -v "$PWD":/backup alpine:3.24 tar xzf /backup/npm-2026-09-26.tar.gz -C /
docker compose up -dتذكر: النسخة تحتوي المفاتيح الخاصة Private Keys للشهادات وأي رموز DNS API، لذلك خزنها مشفرة Encrypted خارج السيرفر.
التحديث Upgrade إلى إصدار أحدث
- اقرأ ملاحظات الإصدار Release Notes أولاً، فمثلاً الإصدار 2.16.0 حدث Certbot ونبه إلى أن بعض إضافات DNS قد تحتاج إلى تعديل، لذلك إذا كنت تعتمد على DNS Challenge فاختبر التجديد بعد التحديث.
- خذ نسخة احتياطية كما سبق.
- غير الوسم Tag في
compose.yamlإلى رقم الإصدار الجديد كاملاً وليسlatest، ثم نفذ:
cd /opt/npm
docker compose pull
docker compose up -d
docker compose logs --tail 30 npmوعند الإقلاع سوف يطبق NPM ترحيلات Migrations قاعدة البيانات، وتظهر في السجل بالوسم [Migrate]، ثم يعيد توليد ملفات Nginx، فافتح بضعة مضيفين وتأكد أنها تعمل. وإذا احتجت إلى الرجوع Rollback فأعد الوسم القديم، واستعد النسخة الاحتياطية إذا كانت الترحيلات قد غيرت قاعدة البيانات.
مشكلات شائعة وحلولها
502 Bad Gateway
هذا الخطأ يعني أن الطلب وصل إلى NPM ولم يصل إلى التطبيق، وسجل المضيف يخبرك بالسبب: docker exec npm tail -n 20 /data/logs/proxy-host-1_error.log، والرقم فيه هو معرف المضيف. فإذا كان ال Container متوقفاً سوف ترى رسالة مثل whoami could not be resolved. والأسباب الشائعة هي:
- ال Container متوقف أو يعيد التشغيل باستمرار:
docker ps -a. - ال Container ليس على شبكة
proxy:docker network inspect proxy. - كتبت المنفذ المنشور على السيرفر بدلاً من منفذ ال Container الداخلي.
- Scheme خاطئ: التطبيق يستمع داخلياً عبر HTTPS (مثل Portainer على 9443) وأنت اخترت
http، أو العكس. - التطبيق يستمع على
127.0.0.1داخل ال Container الخاص به، والصحيح أن يستمع على0.0.0.0.
ERR_TOO_MANY_REDIRECTS
سوف ترى هذا الخطأ عادةً عندما يقف أمام NPM Proxy آخر ينهي TLS Termination، مثل Cloudflare بوضع Flexible أو Load Balancer، حيث يصل الطلب إلى NPM عبر HTTP فيحوله Force SSL إلى HTTPS، ثم يعود عبر HTTP من جديد، وتتكرر الدورة بلا نهاية. وللمشكلة حلان بهذا الترتيب: الأول أن تجعل وضع Cloudflare Full (strict) حتى يتصل ب NPM عبر HTTPS، كما يوضح توثيق Cloudflare، والثاني أن تفعل في تبويب SSL ضمن Advanced الخيار Trust Upstream Forwarded Proto Headers، فيثق NPM بترويسة X-Forwarded-Proto: https القادمة من ال Proxy الأمامي.

قبل تفعيل الخيار يعود طلب HTTP يحمل X-Forwarded-Proto: https بالرمز 301، وبعد تفعيله يصل إلى التطبيق. ولكن لا تفعله إلا إذا كان أمام NPM Proxy تثق به، والسبب أن أي عميل Client يستطيع إرسال هذه الترويسة. وإذا استمرت الحلقة Redirect Loop فتأكد أن التطبيق نفسه لا يفرض HTTPS بطريقته، كأن يكون APP_URL أو ROOT_URL مضبوطاً على http://.
لماذا فشل إصدار الشهادة؟ المنفذ 80 مغلق
رسائل مثل Timeout during connect أو Connection refused في سجل Certbot تعني أن Let's Encrypt لم تصل إلى المنفذ 80، لذلك راجع جدار الحماية Firewall لدى المزود وعلى السيرفر، وتأكد أن المنفذ ليس محجوزاً لخدمة أخرى (sudo ss -ltnp | grep ':80 '). ولاحظ أن بعض مزودي الإنترنت المنزلي ISP يحجبون المنفذ 80 الوارد، وفي هذه الحالة استخدم DNS Challenge، أو اتبع دليل تشغيل خادمك من إنترنت المنزل. وإذا كان النطاق يمر عبر Cloudflare وخيار ال Proxy (السحابة البرتقالية Orange Cloud) مفعل، فتأكد أن الطلب يصل فعلاً إلى سيرفرك.
التطبيق يرى عنوان Docker بدلاً من عنوان الزائر
التطبيق الذي يعمل خلف Reverse Proxy يرى في الاتصال عنوان ال Proxy (172.x.x.x) دائماً، أما العنوان الحقيقي فيصله في الترويستين X-Real-IP وX-Forwarded-For. والحل في إعداد التطبيق نفسه، حيث تخبره أن يثق بال Proxy ويقرأ الترويسة عبر إعداد مثل TRUSTED_PROXIES أو trusted_proxies أو REMOTE_IP_HEADER بحسب التطبيق، واجعل الشبكة الموثوقة مقصورة على شبكة Docker.
وإذا كان أمام NPM Proxy آخر مثل Cloudflare، فإن NPM يثق مسبقاً بنطاقات عناوين Cloudflare وCloudFront التي يجلبها عند الإقلاع، ولكنه يقرأ العنوان من الترويسة X-Real-IP، وCloudflare لا يرسلها، وبالتالي يصل عنوان Cloudflare إلى تطبيقك. والحل أن تضيف السطر real_ip_header CF-Connecting-IP; في تبويب Advanced الخاص بال Proxy Host، وتجد الشرح الكامل مع إعداد كل إطار عمل في دليل تطبيقك خلف Reverse Proxy. وإذا كان سيرفرك لا يتصل بالإنترنت عند الإقلاع، فيمكنك تعطيل هذا الجلب بالمتغير IP_RANGES_FETCH_ENABLED: "false".
413 Request Entity Too Large
حد حجم الطلب Request Size Limit في NPM افتراضياً هو 2000m، لذلك إذا ظهر هذا الخطأ فالأرجح أن الحد مفروض في التطبيق نفسه (مثل upload_max_filesize في PHP) أو في Proxy أمامي آخر. وإذا احتجت إلى حد أكبر لمضيف معين، فأضف في تبويب Advanced الخاص به (رمز الترس) سطراً مثل client_max_body_size 5g;، وهو توجيه Nginx المعروف.
المضيف يظهر Offline بعد إعداد مخصص
السبب خطأ في إعداد Advanced يمنع Nginx من إعادة التحميل، ومنذ الإصدار 2.16 يحتفظ NPM بالملف الفاشل بالامتداد .conf.err حتى تقرأه: docker exec npm ls /data/nginx/proxy_host/، فأصلح الإعداد واحفظ من جديد.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- ضع NPM وتطبيقاتك على شبكة Docker مشتركة، فلا تنشر التطبيقات أي منفذ ويبقى ال Reverse Proxy هو الطريق الوحيد إليها.
- انشر واجهة الإدارة على
127.0.0.1فقط، ثم ضعها خلف NPM بقائمة وصول تطلب العنوان المسموح وكلمة المرور معاً. - التحقق عبر HTTP-01 يكفي في أغلب الحالات ويحتاج المنفذ 80، أما شهادات Wildcard والسيرفرات الداخلية فتحتاج التحقق عبر DNS-01 برمز API بأضيق صلاحية.
- إذا كانت لديك نطاقات فرعية كثيرة فاطلب شهادة Wildcard واحدة لـ
*.example.comوexample.com، ثم اخترها في كل Proxy Host، فيتجدد الجميع معها. - لا تفعل HSTS إلا بعد أن تتأكد أن الشهادة تعمل وتتجدد.
- انسخ ال Volumes
npm_dataوnpm_letsencryptوخزن النسخة مشفرة خارج السيرفر، لأنها تحتوي المفاتيح الخاصة.
سجل التحديثات
- سبتمبر 2026: كتابة الدليل واختباره على Nginx Proxy Manager 2.16.0.
- أكتوبر 2026: إضافة قسم الشهادة الشاملة Wildcard Certificate، إما من Let's Encrypt عبر DNS Challenge وإما شهادة مخصصة Custom مثل Cloudflare Origin CA.