SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor

شرح إعداد nginx كوكيل عكسي خطوة بخطوة

تعلّم إعداد كتلة خادم nginx على Ubuntu 24.04، مع proxy_pass والرؤوس الأربعة وWebSocket والشرطات المائلة ورفع الملفات، واختبره باستخدام nginx -t.

ما الذي يفعله إعداد nginx للـReverse Proxy

يستقبل nginx للـReverse Proxy الطلبات الواردة على المنفذين 80 و443، ويمرّر كل طلب إلى تطبيق يستمع مسبقاً على منفذ محلي، ثم يعيد إجابة ذلك التطبيق إلى المتصفح. يتكوّن الإعداد من كتلة واحدة من نوع server، وهذه الكتلة قصيرة. تتركز معظم الصعوبة في خمسة أو ستة أسطر تحدد للتطبيق هوية العميل الفعلية والبروتوكول الذي استخدمه ذلك العميل.

تُبنى جميع الخطوات التالية من الصفر على Ubuntu 24.04، باستخدام حزمة nginx التي يوفرها التوزيع. نقطة البداية هي تطبيق يستجيب مسبقاً على 127.0.0.1:3000. إذا لم تكن قد اخترت Reverse Proxy بعد، فاقرأ أولاً مقارنة nginx مع Caddy وTraefik. يوضح ما يلي شكل إعداد nginx، سطراً بعد سطر.

شغّل هذه الإعدادات على خادمك الخاص. اختبر كل تغيير باستخدام sudo nginx -t قبل إعادة التحميل، واقرأ الناتج الذي يعرضه.

موقع حفظ nginx لملفات الإعداد في Ubuntu

sudo apt update
sudo apt install -y nginx
ls -l /etc/nginx/sites-enabled/

الملف الرئيسي هو /etc/nginx/nginx.conf. يضبط الخيارات العامة داخل كتلة http { }، ثم يضم محتويات الدليلين /etc/nginx/conf.d/*.conf و/etc/nginx/sites-enabled/*. في Ubuntu وDebian، تنشئ ملفاً واحداً لكل موقع داخل /etc/nginx/sites-available/، ثم تفعّله بإنشاء رابط رمزي له داخل /etc/nginx/sites-enabled/. يؤدي حذف الرابط الرمزي إلى تعطيل الموقع مع الإبقاء على الملف.

لا تعمل التوجيهات المستخدمة لاحقاً إلا في سياق http، ولا تعمل مطلقاً داخل كتلة server: map وupstream. ضع هذه التوجيهات في ملف مستقل داخل /etc/nginx/conf.d/، لأن هذا الدليل مُضمَّن على مستوى http.

تأتي الحزمة مع موقع مفعّل اسمه default. وهو معلَّم بوسم default_server، ما يعني أنه يستجيب لأي طلب لا يطابق رأس Host قيمة server_name في أي مكان ضمن إعداداتك. ما دام مفعّلاً، ستصل الطلبات التي لا تطابق أسماء نطاقك إلى هذا الموقع بدلاً من تطبيقك. احذف الرابط الرمزي بعد التأكد من عمل موقعك.

sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

أصغر كتلة خادم لتمرير الطلبات إلى تطبيق واحد

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

احفظها باسم /etc/nginx/sites-available/app.example.com، ثم فعّلها وحمّلها.

sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -sI -H 'Host: app.example.com' http://127.0.0.1/

يربط listen 80; على IPv4، بينما يربط listen [::]:80; على IPv6. إذا حذفت السطر الثاني، فسيتلقى الزائر الذي يعيد DNS (نظام أسماء النطاقات) لديه سجل AAAA لخادمك رسالة رفض الاتصال، في حين يرى جميع مستخدمي IPv4 موقعاً يعمل. وسيقول تقرير الخطأ الذي تتلقاه: «يعمل لدي».

تتم مطابقة server_name مع الترويسة Host التي يرسلها المتصفح. يمكنك إدراج عدة أسماء مفصولة بمسافات. إذا لم تطابق أي كتلة الطلب، يستخدم nginx الكتلة التي تحمل default_server، ولذلك كان يجب إزالة الموقع المضمّن مع الحزمة.

تمثل location / مطابقة بادئة لمسار الطلب، بينما تطابق / كل مسار. تشير proxy_pass إلى العنوان الذي يفتح nginx اتصالاً به. أبقِ التطبيق مرتبطاً بـ127.0.0.1 حتى يمر المسار الوحيد إلى التطبيق عبر nginx. إذا كان التطبيق يعمل داخل حاوية، فانشره على 127.0.0.1:3000:3000 وليس على 3000:3000، لأن يكتب Docker قواعده الخاصة وينشر المنافذ متجاوزاً ufw مباشرة، ولذلك يمكن الوصول إلى منفذ منشور عارٍ من الإنترنت مهما كانت إعدادات جدارك الناري.

يرسل السطر curl ترويسة Host الصحيحة من الخادم نفسه، حتى تتمكن من اختبار الكتلة قبل أن يشير DNS إلى أي مكان.

ما يرسله nginx إلى الخادم الخلفي عند عدم كتابة أي إعدادات أخرى

يخفي proxy_pass بمفرده أربعة أمور عن تطبيقك.

يتحدث nginx افتراضياً مع الخادم الخلفي باستخدام HTTP/1.0، ويرسل Connection: close، لذلك يفتح كل طلب اتصالاً جديداً بالخادم الخلفي، ولا يمكن إجراء ترقية للبروتوكول.

تُعاد كتابة ترويسة Host إلى القيمة الموجودة في proxy_pass، وهي 127.0.0.1:3000. إذا كان التطبيق ينشئ روابط مطلقة استناداً إلى Host، فسوف ينتج روابط لا يستطيع أي مستخدم خارج الخادم فتحها.

يصل الاتصال إلى التطبيق من nginx، لذلك يرى التطبيق عنوان العميل على أنه 127.0.0.1. تسجل كل أسطر السجل وكل حدود المعدل داخل التطبيق عنوان الخادم الوكيل بدلاً من عنوان الزائر.

لا يستطيع التطبيق معرفة أن المتصفح استخدم HTTPS، لأن الاتصال الذي استلمه هو HTTP عادي على عنوان loopback.

تُصلح أربعة أسطر كل ذلك.

الرؤوس الأربعة التي يجب ضبطها، وما الذي يتيح كل منها للتطبيق الخلفي رؤيته

location / {
    proxy_pass http://127.0.0.1:3000;

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Host يحمل الاسم الذي كتبه الزائر. $host هو الاسم الوارد في الطلب، بعد إزالة المنفذ وتحويل الأحرف إلى صغيرة. اضبطه لكي ينشئ تطبيقك عناوين URL مطلقة صحيحة، مثل إعادة التوجيه بعد تسجيل الدخول أو الرابط داخل رسالة البريد الإلكتروني لإعادة تعيين كلمة المرور. إذا حذفته، فستشير هذه العناوين إلى 127.0.0.1:3000، وعندها يرسل تسجيل الدخول المتصفح إلى عنوان يرفض الاتصال. إذا كان تطبيقك يحتاج إلى المنفذ أيضاً، لأنك تقدّمه عبر 8080، فاستخدم $http_host، وهو الرأس كما أرسله العميل تماماً.

X-Real-IP يحمل قيمة واحدة: $remote_addr، وهو العنوان الذي قبل nginx الاتصال منه. تقرأه التطبيقات في سجلات الوصول الخاصة بها ولتطبيق حدود المعدل الخاصة بها.

X-Forwarded-For يحمل قائمة. يضيف $proxy_add_x_forwarded_for قيمة $remote_addr إلى ما وضعه العميل مسبقاً في ذلك الرأس، ولذلك تكون القيمة مفصولة بفواصل وتكون الإضافة التي أجراها nginx هي الأخيرة. تحدد هذه التفاصيل ما إذا كان يمكن الوثوق بالرأس: يستطيع العميل إرسال أي X-Forwarded-For يريده، ولذلك يمكن إخبار تطبيق يقرأ الإدخال الأول بأي عنوان على الإطلاق. عندما يكون nginx خادم الحافة، اكتب $remote_addr بدلاً منه وتجاهل نسخة العميل. عندما تكون هناك CDN أو خدمة Proxy أخرى أمامه، استخدم set_real_ip_from وreal_ip_header من وحدة realip، لكي يصبح $remote_addr نفسه عنوان العميل الحقيقي.

X-Forwarded-Proto يحمل http أو https. تقرأه أطر العمل لتقرر ما إذا كانت ستضع علامة Secure على ملفات تعريف الارتباط، وما إذا كانت ستفرض إعادة التوجيه إلى HTTPS. إذا حذفته من موقع يستخدم TLS، فسيرى تطبيق مضبوط لفرض HTTPS القيمة http، ويرد بإعادة التوجيه إلى عنوان HTTPS، ثم يتلقى الطلب التالي عبر nginx، ويظل يرى http، ويعيد التوجيه مرة أخرى. عندها يتوقف المتصفح عن المحاولة ويعرض ERR_TOO_MANY_REDIRECTS.

يؤدي تكرار هذه الأسطر الأربعة في كل location إلى اختلافها مع الوقت. ضعها في ملف واحد واستخدم include له.

# /etc/nginx/snippets/proxy-headers.conf
proxy_set_header Host              $host;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location / {
    include snippets/proxy-headers.conf;
    proxy_pass http://127.0.0.1:3000;
}

ينطوي التوريث هنا على مشكلة. يرث location توجيهات proxy_set_header من كتلة server الخاصة به فقط ما دام لا يعرّف أي توجيهات خاصة به. أضف proxy_set_header واحداً داخل location، فتُحذف كل الرؤوس المعرّفة على مستوى server من ذلك location. لذلك أبقِها كلها على مستوى واحد، أو استخدم include في كل location ينفّذ proxy.

لماذا يتصل تطبيق WebSocket لديّ ثم ينقطع؟

لأن الإعدادات الافتراضية تمنع الترقية، ولأن مهلة القراءة الافتراضية تغلق النفق الخامل بعد 60 ثانية. يبدأ WebSocket كطلب HTTP يحمل Upgrade: websocket وConnection: Upgrade. هذان رأسا HTTP من نوع hop-by-hop، أي يُفترض أن يستهلكهما الـproxy بدلاً من تمريرهما، كما أن HTTP/1.0 لا يوفّر آلية للترقية أصلاً. لذلك يجب إعادتهما يدوياً.

يوضع map في سياق http، داخل ملف مستقل.

# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

ثم يأتي location.

location / {
    include snippets/proxy-headers.conf;
    proxy_pass http://127.0.0.1:3000;

    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

يوجد map حتى يتمكن location واحد من خدمة نوعي حركة المرور. في الطلب العادي يكون $http_upgrade فارغاً، لذلك يصبح $connection_upgrade هو close. أما في طلب الترقية، فيحتوي على websocket، ولذلك يكون الرأس المرسل إلى الخادم الخلفي هو Connection: upgrade. يؤدي تثبيت proxy_set_header Connection "upgrade"; إلى إرسال هذا الرأس مع كل طلب صفحة عادي أيضاً، وقد يرد بعض الخوادم الخلفية على هذا الطلب بالرمز 400.

إن proxy_read_timeout هو سبب ظهور رسائل من نوع «يُحمّل التطبيق، ثم يتوقف عن التحديث». تبلغ قيمته الافتراضية 60 ثانية، وهو يقيس الفاصل بين قراءتين من الخادم الخلفي، لا مدة اتصال الاتصال. إذا ظل WebSocket هادئاً لمدة 60 ثانية، يغلقه nginx، وتعرض وحدة تحكم المتصفح إغلاق المقبس بالرمز 1006. التطبيقات التي ترسل نبضة keepalive خاصة بها أكثر من مرة كل دقيقة لا تلاحظ المشكلة. أما التطبيقات التي لا تفعل ذلك، فتنقطع بعد دقيقة واحدة. تظهر المشكلة أولاً في المحررات المباشرة ولوحات المعلومات، ومن الأمثلة الشائعة مثيل n8n مستضاف ذاتياً خلف HTTPS.

لماذا تغيّر الشرطة المائلة النهائية في proxy_pass عناوين URL؟

القاعدة في جملة واحدة: إذا انتهى proxy_pass بمسار URI، حتى إذا كان مجرد /، فإن nginx يزيل جزء مسار الطلب الذي طابق البادئة location، ويضع URI مكانه. أما إذا توقف proxy_pass عند المضيف والمنفذ، فيمرّر مسار الطلب كما هو دون تغيير.

location /app/ {
    proxy_pass http://127.0.0.1:3000/;
}

يصل طلب /app/status إلى الواجهة الخلفية بصيغة /status.

location /app/ {
    proxy_pass http://127.0.0.1:3000;
}

يصل طلب /app/status إلى الواجهة الخلفية بصيغة /app/status.

يعتمد الشكل المطلوب على التطبيق. يحتاج التطبيق الذي يضبط base-path أو المجلد الفرعي إلى الشكل الثاني، مع تعريف /app في هذا الإعداد. أما التطبيق الذي لا يعرف شيئاً عن البادئات فيحتاج إلى الشكل الأول. وللشكل الأول كلفة تظهر مباشرة: يظل HTML الذي يعيده التطبيق متضمناً مسارات مطلقة مثل /static/main.css، فيطلبها المتصفح من جذر الموقع، ولا يطابقها أي location، فتُعرض الصفحة من دون تنسيق. تعرض علامة تبويب الشبكة في المتصفح طلبات هذه الملفات مع الاستجابة 404. يتمثل الإصلاح في ضبط base-path الخاص بالتطبيق، أو إضافة location /static/ ثانية تشير إلى الواجهة الخلفية نفسها.

لا يمكن لـlocation الذي يستخدم تعبيراً نمطياً أن يتضمن URI في proxy_pass. يرفض sudo nginx -t الإعداد ويذكر السبب: "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block.

تختفي هذه الفئة كاملة من المشكلات عندما يحصل كل تطبيق على اسم خاص به، وهو app.example.com، ويُمرَّر عبر proxy من location /. لا تستحق المسارات الفرعية هذا التعقيد إلا عندما يتعذر عليك إضافة سجلات DNS.

كيف أضع أكثر من خادم خلفي خلف اسم واحد؟

باستخدام كتلة upstream. تنتمي هذه الكتلة إلى سياق http، لذا اكتبها قبل كتلة server في الملف نفسه، أو داخل /etc/nginx/conf.d/.

upstream app_backend {
    least_conn;
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

تسمّيها كتلة location بهذه الطريقة: proxy_pass http://app_backend;.

الطريقة الافتراضية هي round robin. يرسل least_conn كل طلب إلى الخادم الخلفي الذي لديه أقل عدد من الاتصالات النشطة، وهذا يناسب الطلبات ذات المدد غير المتساوية. يربط ip_hash عنوان عميل واحد بخادم خلفي واحد. تحتاج إلى ip_hash عندما يحتفظ التطبيق بالجلسات في ذاكرته الخاصة، لأن تطبيق round robin على خادمين من هذا النوع يؤدي إلى تسجيل خروج المستخدمين عشوائياً عندما تصل طلباتهم إلى النسخة التي لم تتعامل معها من قبل. نقل الجلسات إلى وحدة تخزين مشتركة هو الحل الأفضل.

يعني max_fails=3 fail_timeout=30s أن 3 محاولات فاشلة خلال 30 ثانية تخرج ذلك الخادم من الخدمة لمدة 30 ثانية. عندما تكون جميع الخوادم في الكتلة بهذه الحالة، يحصل العملاء على الخطأ 502، ويسجل سجل الأخطاء no live upstreams while connecting to upstream.

تُبقي keepalive 32 ما يصل إلى 32 اتصالاً خاملاً مفتوحاً مع الخوادم الخلفية لكل عملية worker، مما يلغي مصافحة TCP من معظم الطلبات. تعمل هذه الميزة فقط مع proxy_http_version 1.1، ومن دون تمرير أي Connection: close إلى الخادم الخلفي. إذا كان الموقع نفسه يستخدم أيضاً خريطة WebSocket، فغيّر الحالة الفارغة من close إلى سلسلة فارغة، حتى لا تحمل الطلبات العادية رأس Connection، وحتى يُعاد استخدام الاتصال المجمّع.

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      '';
}

تُحل الأسماء داخل كتلة upstream عند بدء nginx. إذا كان الخادم الخلفي حاوية تحصل على عنوان جديد عند إعادة تشغيلها، فسيواصل nginx استخدام العنوان القديم حتى تعيد تحميل إعداداته. داخل شبكة Docker، يمكنك نقل عملية البحث إلى وقت الطلب باستخدام محلّل الأسماء المضمّن.

resolver 127.0.0.11 valid=10s;
set $backend http://app:3000;
proxy_pass $backend;

عندما تبدأ الحاويات بالظهور والاختفاء بوتيرة تجعلك تعدّل nginx باستمرار لمواكبتها، يكون استخدام proxy يقرأ تسميات الحاويات خياراً أفضل. ينشئ Traefik أمام عدة تطبيقات Docker Compose مساراته من الحاويات نفسها.

لماذا تفشل عمليات الرفع مع الخطأ 413 Request Entity Too Large؟

يكون الإعداد client_max_body_size مضبوطاً افتراضياً على 1 ميغابايت. يرفض nginx جسم الطلب الأكبر قبل أن يرى تطبيقك أي جزء منه، ويسجل سجل الأخطاء client intended to send too large body. ارفع القيمة في كتلة server، أو في location الذي تحدث فيه عمليات الرفع.

client_max_body_size 512m;

تؤدي قيمة 0 إلى تعطيل هذا الفحص بالكامل. يفرض التطبيق حداً خاصاً به أيضاً، لذلك إذا استمر ظهور الخطأ 413 بعد هذا التغيير، فهو صادر من الواجهة الخلفية. عندئذٍ يكون إعداد الرفع الخاص بالتطبيق هو المكان التالي الذي يجب فحصه.

يقرأ nginx جسم الطلب بالكامل افتراضياً قبل أن يفتح الاتصال بالواجهة الخلفية، ويكتب الأجزاء الكبيرة أولاً إلى ملف مؤقت على القرص. يحمي ذلك التطبيق من العملاء البطيئين، لأن الواجهة الخلفية تستقبل الرفع بالسرعة المحلية الكاملة. ويمكنك بدلاً من ذلك بث عمليات الرفع الكبيرة جداً.

proxy_request_buffering off;

تستقبل الواجهة الخلفية جسم الطلب أثناء وصوله، ولذلك يجب أن تكون قادرة على معالجته بهذه الطريقة. ويفقد nginx أيضاً إمكانية إعادة محاولة الطلب مع واجهة خلفية أخرى، لأن الجسم يكون قد استُهلك بالفعل.

ينطبق client_body_timeout، وقيمته الافتراضية 60 ثانية، بين عمليتي قراءة متتاليتين لجسم الطلب، وليس على عملية الرفع بأكملها. تنجح عملية الرفع البطيئة والمستقرة. أما العملية المتوقفة فتُسقط.

التخزين المؤقت للاستجابات، والإعداد الذي يعطّل الإخراج المباشر

يكون proxy_buffering مفعّلاً افتراضياً، وهو عادةً الإعداد المطلوب. يقرأ nginx الاستجابة من تطبيقك بالسرعة التي يستطيع التطبيق الكتابة بها، ويحتفظ بها، ثم يرسلها إلى العميل البطيء وفق سرعته. ينهي عامل التطبيق عمله مبكراً بدلاً من أن يظل مشغولاً طوال عملية التنزيل البطيئة.

لكن هذا يعطّل الاستجابات المتدفقة. لا تعرض Server-sent events وإخراج السجلات المباشر أي شيء للقارئ إلى أن يمتلئ أحد المخازن المؤقتة. عطّل التخزين المؤقت في ذلك الموضع فقط.

proxy_buffering off;

إذا كنت تتحكم في التطبيق، فالأفضل أن يرسل الترويسة X-Accel-Buffering: no مع الاستجابات المتدفقة فقط. يقرأ nginx هذه الترويسة لكل استجابة، ويعطّل التخزين المؤقت لها وحدها، بينما تحتفظ الصفحات العادية بفائدته.

عندما يعرض سجل الأخطاء upstream sent too big header while reading response header from upstream، فهذا يعني أن ترويسات الاستجابة لم تتسع لها ذاكرة مؤقتة واحدة. تكون قيمة proxy_buffer_size افتراضياً بحجم صفحة ذاكرة واحدة، أي 4 أو 8 كيلوبايت وفقاً للمنصة، وقد تتجاوزها ملفات تعريف ارتباط طويلة أو ترويسات مصادقة كبيرة. ارفع القيمتين.

proxy_buffer_size 16k;
proxy_buffers 8 16k;

أين يجب وضع TLS في هذا الإعداد؟

في Nginx، أمام كل ما سبق. تنتهي جلسة TLS (أمن طبقة النقل) عند الـproxy، ويبقى الاتصال من Nginx إلى التطبيق عبر HTTP عادي باستخدام عنوان loopback، حيث لا يمكن لأي جهة أخرى على الشبكة قراءته. يعرف التطبيق أن الزائر استخدم HTTPS من خلال X-Forwarded-Proto، وهو الرابع ضمن الرؤوس الأربعة.

لا تكتب مسارات الشهادات يدوياً. وجّه سجل DNS إلى الخادم، وافتح المنفذ في جدار الحماية، ودَع Certbot يعدّل كتلة الخادم نفسها: سيضيف السطر listen 443 ssl مع المسارات ssl_certificate، إضافةً إلى إعادة التوجيه من المنفذ 80. يشرح إصدار شهادة Let's Encrypt لـNginx باستخدام Certbot عملية الإصدار ومؤقت التجديد.

sudo ufw allow 'Nginx Full'
sudo ufw status

Nginx Full هو ملف تعريف لتطبيق يثبّته حزمة Nginx، ويفتح المنفذين 80 و443 معاً. يجب أن يبقى المنفذ 80 مفتوحاً لتحدي التجديد HTTP-01، حتى بعد إعادة توجيه كل زائر إلى HTTPS.

اختبر الإعدادات، ثم أعد التحميل

sudo nginx -t
sudo systemctl reload nginx

يفحص nginx -t كل ملف مضمّن، ثم يعرض نجاح الاختبار أو يطبع اسم الملف ورقم السطر الذي توقف عنده. اقرأ هذه النتيجة قبل إعادة التحميل. لا تُطبّق إعادة التحميل إعدادات معطّلة؛ يواصل nginx تقديم الخدمة باستخدام الإعدادات السابقة، لذلك يبقى الموقع متاحاً بينما لا يُطبَّق التغيير بصمت. يعمل systemctl restart بطريقة مختلفة وأسوأ، لأن إعادة التشغيل توقف الخادم العامل أولاً، ولذلك قد يتركك خطأ الإعدادات مع توقف nginx بالكامل. استخدم إعادة التحميل افتراضياً، واحتفظ بإعادة التشغيل للتغييرات النادرة التي تتطلبها.

sudo tail -f /var/log/nginx/error.log
sudo ss -lntp | grep -E ':(80|443|3000)'

يوضح السطر ss العملية التي تشغل كل منفذ، ولذلك يمكنك التأكد من أن التطبيق يستمع فعلاً على العنوان الذي يشير إليه proxy_pass.

حالات الفشل التي ستواجهها فعلياً

502 Bad Gateway، مع ظهور connect() failed (111: Connection refused) while connecting to upstream في سجل الأخطاء. لا توجد أي خدمة تستمع على العنوان المحدد في proxy_pass. قد يكون التطبيق متوقفاً، أو مرتبطاً بمنفذ آخر، أو مرتبطاً بعنوان داخلي لحاوية لا يستطيع المضيف الوصول إليه.

502 مع ظهور no live upstreams while connecting to upstream. يضع max_fails حالياً علامة فشل على كل خادم في الكتلة upstream. أصلح الخوادم الخلفية. يعاود nginx محاولة الاتصال بها بعد انتهاء fail_timeout.

504 Gateway Time-out، مع ظهور upstream timed out (110: Connection timed out) while reading response header from upstream. قبل الخادم الخلفي الاتصال، ثم لم يرسل أي بيانات طوال proxy_read_timeout ثانية. تكون زيادة مهلة الانتظار صحيحة عند إنشاء تقرير بطيء فعلاً، لكنها ليست الحل لتطبيق متعطل.

يعيد التطبيق 404 لكل مسار. أعادت قاعدة الشرطة المائلة اللاحقة كتابة المسار. قارن المسار الذي يسجله التطبيق بالمسار الذي طلبته.

يستجيب موقع مختلف. لا يطابق server_name ترويسة Host، لذلك انتقل الطلب إلى كتلة default_server.

تُحمّل الصفحة، ثم تتجمد الواجهة بعد نحو دقيقة. هذه حالة WebSocket: إعداد التعامل مع Upgrade مفقود، أو أن proxy_read_timeout ما زال مضبوطاً على 60 ثانية.

FAQ

لماذا يعرض nginx الخطأ 502 Bad Gateway بعد إضافة proxy_pass؟

تعذّر على nginx فتح اتصال بالعنوان الموجود في proxy_pass. يوضح سجل الأخطاء في /var/log/nginx/error.log السبب: تعني connect() failed (111: Connection refused) while connecting to upstream أنه لا توجد عملية تستمع على ذلك العنوان، وتعني no live upstreams أن كل خادم في كتلة upstream تم وضع علامة فشل عليه. شغّل sudo ss -lntp | grep 3000 لمعرفة العملية التي تشغل المنفذ والعنوان المرتبط به. يؤدي ربط التطبيق بعنوان داخلي للحاوية، أو بمنفذ يختلف عن المنفذ الذي كتبته، إلى هذا الخطأ في كل مرة.

لماذا ينقطع اتصال تطبيقي بعد نحو دقيقة خلف nginx؟

الاتصال هو WebSocket، وما يزال proxy_read_timeout مضبوطاً على قيمته الافتراضية البالغة 60 ثانية. تقيس هذه المهلة الفاصل بين قراءتين من الواجهة الخلفية. يغلق nginx المقبس الخامل، ويعرض console المتصفح رمز الإغلاق 1006. اضبط proxy_http_version 1.1، ومرّر Upgrade وConnection باستخدام map على $http_upgrade، وارفع proxy_read_timeout إلى قيمة مثل 3600s. من دون ترويسة Upgrade، لا تتم الترقية إطلاقاً، لذلك يعود التطبيق إلى polling أو لا يعرض تحديثات مباشرة.

هل تهم الشرطة المائلة اللاحقة في proxy_pass؟

نعم، فهي تغيّر المسار الذي تستقبله الواجهة الخلفية. مع location /app/ وproxy_pass http://127.0.0.1:3000/، يصل الطلب إلى /app/status في الواجهة الخلفية بالشكل /status، لأن أي URI يأتي بعد المضيف والمنفذ يستبدل بادئة location المطابقة. احذف الشرطة المائلة الأخيرة، وسيصل الطلب نفسه بالشكل /app/status. يؤدي حذف البادئة غالباً إلى كسر روابط الأصول الخاصة بالتطبيق. تبقى هذه الروابط مطلقة، ثم تعرض 404 من جذر الموقع. لذلك يكون الشكل الذي يمرر المسار كما هو أنسب لتطبيق يملك إعداد base-path.

لماذا يسجل تطبيقي 127.0.0.1 على أنه عنوان IP لكل زائر؟

لأن الاتصال الذي يستقبله التطبيق يأتي فعلاً من nginx عبر عنوان loopback. لا يصل عنوان الزائر إلى التطبيق إلا في ترويسة تضبطها أنت: proxy_set_header X-Real-IP $remote_addr; لقيمة واحدة، وproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; للسلسلة المضافة. يجب بعد ذلك إعداد التطبيق لكي يثق بهذه الترويسات. تذكّر أن بإمكان العميل إرسال X-Forwarded-For الخاص به. لذلك، عندما يكون nginx هو خادم edge، استبدل قيمته بـ$remote_addr بدلاً من إضافة قيمة إليها.

هل أحتاج إلى TLS على الاتصال بين nginx وتطبيقي؟

ليس عندما يعمل التطبيق على الخادم نفسه ويرتبط بـ127.0.0.1، لأن هذه الحركة لا تغادر الجهاز. أنهِ TLS عند nginx، وأبقِ proxy_pass على HTTP عادي عبر loopback، وأرسل X-Forwarded-Proto $scheme لكي يعرف التطبيق أن الزائر استخدم HTTPS. إذا كانت الواجهة الخلفية على مضيف مختلف عبر شبكة لا تتحكم فيها، فيحتاج ذلك المسار إلى حماية مستقلة، إما باستخدام HTTPS إلى الواجهة الخلفية أو نفق خاص بين الجهازين.