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

آموزش گام‌به‌گام تنظیم Reverse Proxy در Nginx

نحوه تنظیم دقیق proxy_pass و هدرهای ضروری برای اپلیکیشن در Nginx را بیاموزید. این راهنما شامل مدیریت Websockets، اسلش‌های انتهایی و محدودیت آپلود روی Ubuntu 24.04 است.

پیکربندی reverse proxy در nginx چه کاری انجام می‌دهد

یک reverse proxy در nginx درخواست‌هایی را که به پورت 80 و 443 می‌رسند دریافت کرده و هر کدام را به برنامه‌ای که از قبل روی یک پورت محلی در حال گوش دادن است تحویل می‌دهد، سپس پاسخ آن برنامه را به مرورگر بازمی‌گرداند. پیکربندی این سرویس شامل یک بلاک server است و این بلاک بسیار کوتاه است. تقریباً تمام پیچیدگی کار در پنج یا شش خط نهفته است که به برنامه شما می‌گوید کلاینت واقعی چه کسی بوده و از چه پروتکلی استفاده کرده است.

تمام موارد زیر از صفر روی Ubuntu 24.04 و با استفاده از پکیج nginx موجود در مخازن توزیع پیاده‌سازی شده‌اند. نقطه شروع، برنامه‌ای است که از قبل روی 127.0.0.1:3000 پاسخ می‌دهد. اگر هنوز در انتخاب پروکسی به نتیجه نرسیده‌اید، مقایسه nginx با Caddy و Traefik اولین مطلبی است که باید مطالعه کنید. آنچه در ادامه می‌آید، ساختار پاسخ nginx به صورت خط به خط است.

این پیکربندی‌ها را روی سرور خود اجرا کنید. پیش از reload کردن، هر تغییر را با sudo nginx -t تست کنید و خروجی آن را بخوانید.

محل نگهداری فایل‌های پیکربندی Nginx در اوبونتو

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/* را فراخوانی می‌کند. در اوبونتو و دبیان، شما برای هر سایت یک فایل در /etc/nginx/sites-available/ می‌نویسید و با ایجاد یک symlink در /etc/nginx/sites-enabled/ آن را فعال می‌کنید. حذف symlink باعث غیرفعال شدن سایت می‌شود و فایل اصلی همچنان باقی می‌ماند.

دو دستورالعملی که بعداً استفاده می‌شوند، فقط در context http کار می‌کنند و هرگز نباید درون بلوک server قرار گیرند: map و upstream. آن‌ها را در فایلی مجزا در مسیر /etc/nginx/conf.d/ قرار دهید، زیرا این دایرکتوری در سطح http فراخوانی می‌شود.

این بسته نرم‌افزاری به‌صورت پیش‌فرض سایتی فعال به نام default دارد. این سایت با برچسب default_server مشخص شده است، به این معنی که به هر درخواستی که هدر Host آن با هیچ server_name موجود در پیکربندی شما مطابقت نداشته باشد، پاسخ می‌دهد. تا زمانی که این سایت فعال باشد، درخواستی که با نام‌های دامنه شما همخوانی نداشته باشد، به‌جای برنامه شما به این سایت هدایت می‌شود. پس از اینکه سایت خودتان به‌درستی کار کرد، symlink مربوط به آن را حذف کنید.

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 برای سرور شما برمی‌گرداند، با خطای connection refused مواجه می‌شود، در حالی که کاربران IPv4 سایت را به‌درستی می‌بینند. گزارش خطایی که دریافت می‌کنید این خواهد بود: "برای من کار می‌کند".

server_name با هدر Host که مرورگر ارسال می‌کند مطابقت داده می‌شود. می‌توانید چندین نام را با فاصله از هم لیست کنید. اگر هیچ بلوکی مطابقت نداشته باشد، nginx از بلوکی استفاده می‌کند که default_server است؛ به همین دلیل است که سایت پیش‌فرض بسته‌بندی‌شده باید حذف شود.

location / یک تطبیق پیشوندی روی مسیر درخواست است و / با هر مسیری مطابقت دارد. proxy_pass آدرسی است که nginx به آن متصل می‌شود. برنامه را روی 127.0.0.1 محدود نگه دارید تا تنها راه ورود، از طریق nginx باشد. اگر برنامه در یک container اجرا می‌شود، آن را به صورت 127.0.0.1:3000:3000 منتشر کنید و نه 3000:3000، زیرا Docker قوانین خاص خود را می‌نویسد و پورت‌ها را مستقیماً از ufw عبور می‌دهد؛ بنابراین یک پورت منتشرشدهٔ خام، فارغ از تنظیمات فایروال شما، از اینترنت قابل دسترسی خواهد بود.

خط curl هدر Host صحیح را از خود سرور ارسال می‌کند تا بتوانید پیش از آنکه DNS به جایی اشاره کند، بلوک را تست کنید.

آنچه Nginx در صورت عدم تنظیمات اضافی به سمت upstream می‌فرستد

proxy_pass به‌تنهایی چهار مورد را از دید برنامه شما پنهان می‌کند.

Nginx به‌صورت پیش‌فرض با backend از طریق HTTP/1.0 صحبت می‌کند و Connection: close می‌فرستد؛ بنابراین هر درخواست یک اتصال upstream جدید باز می‌کند و امکان ارتقای پروتکل (protocol upgrade) وجود ندارد.

هدر Host با مقداری که در proxy_pass تعیین شده بازنویسی می‌شود که همان 127.0.0.1:3000 است. برنامه‌ای که لینک‌های مطلق را بر اساس Host می‌سازد، اکنون لینک‌هایی تولید می‌کند که هیچ‌کس خارج از سرور قادر به باز کردن آن‌ها نیست.

اتصالی که به برنامه می‌رسد از سمت Nginx است، بنابراین برنامه آدرس کلاینت را 127.0.0.1 می‌بیند. در نتیجه، هر خط لاگ و هر rate limit داخل برنامه، به جای بازدیدکننده، پروکسی را ثبت می‌کند.

برنامه نمی‌تواند تشخیص دهد که مرورگر از HTTPS استفاده کرده است، زیرا اتصالی که دریافت کرده، یک HTTP ساده روی آدرس loopback است.

چهار خط زیر تمام این مشکلات را برطرف می‌کند.

چهار هدر اصلی برای تنظیم و تأثیر هر یک بر دیدگاه backend

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های مطلق صحیح را می‌سازد: مانند تغییر مسیر (redirect) پس از ورود به سیستم، یا لینک موجود در ایمیل بازنشانی رمز عبور. اگر این مورد را حذف کنید، آن URLها به 127.0.0.1:3000 اشاره خواهند کرد و در نتیجه، ورود به سیستم، مرورگر را به آدرسی می‌فرستد که اتصال را رد می‌کند. اگر برنامه شما به پورت نیز نیاز دارد (چون آن را روی 8080 ارائه می‌دهید)، از $http_host استفاده کنید که دقیقاً همان هدری است که کلاینت ارسال کرده است.

X-Real-IP یک مقدار را حمل می‌کند: $remote_addr، یعنی آدرسی که nginx اتصال را از آن پذیرفته است. برنامه‌ها این مقدار را برای لاگ‌های دسترسی و محدودسازی نرخ (rate limiting) خود می‌خوانند.

X-Forwarded-For یک لیست را حمل می‌کند. $proxy_add_x_forwarded_for مقدار $remote_addr را به آنچه کلاینت قبلاً در آن هدر قرار داده اضافه می‌کند، بنابراین مقدار با کاما جدا می‌شود و ورودی اضافه شده توسط nginx آخرین مورد خواهد بود. این جزئیات تعیین می‌کند که آیا می‌توان به هدر اعتماد کرد یا خیر: کلاینت می‌تواند هر X-Forwarded-For که بخواهد ارسال کند، بنابراین برنامه‌ای که اولین ورودی را می‌خواند ممکن است هر آدرسی را به عنوان آدرس واقعی بپذیرد. زمانی که nginx سرور لبه (edge server) است، از $remote_addr استفاده کنید و نسخه کلاینت را نادیده بگیرید. زمانی که یک CDN یا پروکسی دیگر در مقابل قرار دارد، از 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 حذف می‌شوند. بنابراین، همه آن‌ها را در یک سطح نگه دارید یا در هر location که عمل proxy را انجام می‌دهد، قطعه کد (snippet) مربوطه را include کنید.

چرا برنامه WebSocket من متصل می‌شود و سپس قطع می‌گردد؟

زیرا تنظیمات پیش‌فرض اجازه ارتقا (upgrade) را نمی‌دهند و timeout پیش‌فرض خواندن، تونل‌های غیرفعال را پس از 60 ثانیه می‌بندد. یک WebSocket به عنوان یک درخواست HTTP حاوی Upgrade: websocket و Connection: Upgrade آغاز می‌شود. این‌ها هدرهای hop-by-hop هستند؛ به این معنی که انتظار می‌رود پروکسی آن‌ها را مصرف کند و نه اینکه مستقیماً عبور دهد، و HTTP/1.0 نیز هیچ مکانیزم ارتقایی ندارد. هر دو هدر باید به‌صورت دستی بازگردانده شوند.

این نگاشت (map) در context 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 است، بنابراین هدر ارسال‌شده به سمت upstream برابر با Connection: upgrade خواهد بود. هاردکد کردن proxy_set_header Connection "upgrade"; باعث می‌شود آن هدر در هر درخواست صفحه معمولی نیز ارسال شود و برخی backendها به چنین درخواستی با خطای 400 پاسخ می‌دهند.

proxy_read_timeout همان چیزی است که باعث گزارش‌های «بارگذاری می‌شود، اما به‌روزرسانی متوقف می‌گردد» می‌شود. مقدار پیش‌فرض آن 60 ثانیه است و فاصله بین دو عملیات خواندن از backend را اندازه‌گیری می‌کند، نه طول عمر کل اتصال را. یک WebSocket که به مدت 60 ثانیه ساکت بماند توسط nginx بسته می‌شود و کنسول مرورگر بسته شدن سوکت با کد 1006 را نشان می‌دهد. برنامه‌هایی که ضربان قلب (heartbeat) خود را با فواصل کمتر از یک دقیقه ارسال می‌کنند، هرگز متوجه این موضوع نمی‌شوند. برنامه‌هایی که این کار را نمی‌کنند، پس از یک دقیقه قطع می‌شوند. ویرایشگرهای زنده و داشبوردها اولین جاهایی هستند که این مشکل در آن‌ها ظاهر می‌شود؛ یک نمونه 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 برای آن‌ها مطابقت پیدا نمی‌کند و صفحه بدون استایل‌دهی نمایش داده می‌شود. در تب Network مرورگر، درخواست‌های این منابع با خطای 404 دیده می‌شوند. راه حل، استفاده از تنظیمات base-path خودِ برنامه یا تعریف یک location /static/ دوم است که به همان بک‌اند اشاره کند.

یک location از نوع regex نمی‌تواند شامل 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 را داشته باشد که از طریق location / پروکسی می‌شود. استفاده از زیرمسیرها (sub-paths) تنها زمانی ارزش دردسرش را دارد که امکان افزودن رکوردهای DNS را نداشته باشید.

چگونه می‌توان بیش از یک backend را پشت یک نام قرار داد؟

با استفاده از یک بلوک upstream. این بلوک متعلق به context مربوط به 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 هر درخواست را به backendای می‌فرستد که کمترین اتصالات فعال را دارد؛ این روش برای درخواست‌هایی با طول نامتوازن مناسب است. دستور ip_hash یک آدرس کلاینت را به یک backend خاص متصل می‌کند. شما به ip_hash نیاز دارید زمانی که برنامه، نشست‌ها (sessions) را در حافظه داخلی خود نگه می‌دارد؛ زیرا در حالت round robin بین دو backend، کاربران به‌طور تصادفی از سیستم خارج می‌شوند، چون درخواست‌هایشان به نمونه‌ای می‌رسد که از نشست آن‌ها بی‌اطلاع است. انتقال نشست‌ها به یک فضای ذخیره‌سازی مشترک، راهکار بهتری است.

عبارت max_fails=3 fail_timeout=30s به این معناست که سه تلاش ناموفق در مدت 30 ثانیه، آن سرور را برای 30 ثانیه از مدار خارج می‌کند. هنگامی که تمام سرورهای موجود در بلوک در این وضعیت باشند، کلاینت‌ها خطای 502 دریافت می‌کنند و لاگ خطا عبارت no live upstreams while connecting to upstream را ثبت می‌کند.

دستور keepalive 32 تا 32 اتصال غیرفعال (idle) به backendها را برای هر worker process باز نگه می‌دارد که باعث حذف سربار TCP handshake از اکثر درخواست‌ها می‌شود. این قابلیت فقط با proxy_http_version 1.1 و بدون استفاده از Connection: close در مسیر upstream کار می‌کند. اگر همان location از نقشه WebSocket نیز استفاده می‌کند، حالت خالی را از close به یک رشته خالی تغییر دهید تا درخواست‌های معمولی فاقد هدر Connection باشند و اتصال pool شده مجدداً استفاده شود.

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

نام‌های داخل یک بلوک upstream هنگام شروع به کار nginx تحلیل (resolve) می‌شوند. اگر backend شما یک container باشد که هنگام restart آدرس جدیدی دریافت می‌کند، nginx تا زمانی که آن را reload نکنید، به استفاده از آدرس قدیمی ادامه می‌دهد. در داخل یک شبکه Docker، می‌توانید با استفاده از resolver تعبیه‌شده، تحلیل آدرس را به زمان ارسال درخواست منتقل کنید.

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

زمانی که containerها آن‌قدر سریع ایجاد و حذف می‌شوند که مجبورید مدام nginx را ویرایش کنید، استفاده از یک proxy که برچسب‌های (labels) containerها را می‌خواند، ابزار بهتری است. استفاده از Traefik در مقابل چندین برنامه Docker Compose مسیرهای خود را مستقیماً از روی خودِ containerها می‌سازد.

چرا آپلودها با خطای 413 Request Entity Too Large شکست می‌خورند؟

client_max_body_size به‌صورت پیش‌فرض روی 1 مگابایت تنظیم شده است. بدنهٔ درخواست‌های بزرگ‌تر توسط nginx پیش از آنکه به برنامهٔ شما برسند رد می‌شوند و لاگ خطا، client intended to send too large body را ثبت می‌کند. این مقدار را در server block یا در location مربوط به آپلودها افزایش دهید.

client_max_body_size 512m;

مقدار 0 این بررسی را به‌طور کامل غیرفعال می‌کند. از آنجا که برنامهٔ شما نیز محدودیت‌های خاص خود را دارد، اگر پس از این تغییر همچنان با خطای 413 مواجه شدید، منشأ آن backend است و باید تنظیمات آپلود خودِ برنامه را بررسی کنید.

به‌صورت پیش‌فرض، nginx پیش از باز کردن اتصال upstream، کل بدنهٔ درخواست را می‌خواند و فایل‌های حجیم را ابتدا در یک فایل موقت روی دیسک ذخیره می‌کند. این کار برنامه را در برابر کلاینت‌های کند محافظت می‌کند، زیرا backend آپلود را با حداکثر سرعت محلی دریافت می‌کند. برای آپلودهای بسیار حجیم، می‌توانید از قابلیت streaming استفاده کنید.

proxy_request_buffering off;

در این حالت، backend بدنهٔ درخواست را همزمان با رسیدن دریافت می‌کند و باید توانایی مدیریت آن را داشته باشد. همچنین در این وضعیت، nginx قابلیت تلاش مجدد (retry) درخواست روی یک upstream دیگر را از دست می‌دهد، زیرا بدنهٔ درخواست دیگر در دسترس نیست.

client_body_timeout که به‌صورت پیش‌فرض 60 ثانیه است، به فاصلهٔ بین دو خواندن متوالی از بدنه اعمال می‌شود، نه به کل زمان آپلود. آپلودهای کند اما پیوسته با این تنظیم مشکلی ندارند، اما آپلودهای متوقف‌شده (stalled) قطع خواهند شد.

بافر کردن پاسخ و تنظیمی که خروجی زنده را مختل می‌کند

proxy_buffering به‌صورت پیش‌فرض فعال است و معمولاً همان چیزی است که به آن نیاز دارید. Nginx پاسخ را با حداکثر سرعتی که برنامه شما می‌تواند بنویسد دریافت کرده، آن را نگه می‌دارد و سپس با سرعت خودِ کلاینتِ کُند، به او تحویل می‌دهد. در این حالت، worker برنامه زودتر آزاد می‌شود و لازم نیست برای کل مدت دانلودِ کُند، درگیر بماند.

این قابلیت، پاسخ‌های streaming را مختل می‌کند. در Server-sent events و خروجی زنده لاگ‌ها، تا زمانی که بافر پر نشود، چیزی به کاربر نمایش داده نمی‌شود. بافرینگ را فقط در همان location خاص غیرفعال کنید.

proxy_buffering off;

اگر کنترل برنامه را در دست دارید، راهکار بهتر این است که هدر X-Accel-Buffering: no را فقط برای پاسخ‌های streaming ارسال کنید. 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 (امنیت لایه انتقال) در پروکسی خاتمه می‌یابد و اتصال از Nginx به برنامه، به صورت HTTP ساده روی آدرس loopback باقی می‌ماند؛ جایی که هیچ‌چیز دیگری در شبکه نمی‌تواند آن را بخواند. برنامه از طریق X-Forwarded-Proto، که چهارمین مورد از چهار هدر است، متوجه می‌شود که بازدیدکننده از HTTPS استفاده کرده است.

مسیرهای گواهی را به‌صورت دستی وارد نکنید. رکورد DNS را به سمت سرور تنظیم کنید، فایروال را باز کنید و اجازه دهید Certbot همین بلوک سرور را ویرایش کند: این ابزار خط listen 443 ssl را به همراه مسیرهای ssl_certificate اضافه می‌کند و یک redirect از پورت 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 رفتار متفاوتی دارد و وضعیت بدتری ایجاد می‌کند، زیرا restart ابتدا سرور در حال اجرا را متوقف می‌کند؛ بنابراین خطای پیکربندی باعث می‌شود که Nginx اصلاً اجرا نشود. به‌صورت پیش‌فرض از reload استفاده کنید و restart را برای تغییرات نادری که به آن نیاز دارند، نگه دارید.

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 در حال گوش دادن نیست. برنامه متوقف شده است، یا روی پورت دیگری تنظیم شده، یا به آدرس داخلی کانتینری متصل است که میزبان (host) به آن دسترسی ندارد.

خطای 502 همراه با no live upstreams while connecting to upstream. تمام سرورهای موجود در بلوک upstream در حال حاضر توسط max_fails به عنوان شکست‌خورده علامت‌گذاری شده‌اند. بک‌اِندها را تعمیر کنید. nginx پس از انقضای fail_timeout دوباره آن‌ها را امتحان می‌کند.

خطای 504 Gateway Time-out، همراه با upstream timed out (110: Connection timed out) while reading response header from upstream. بک‌اِند اتصال را پذیرفته اما برای مدت proxy_read_timeout ثانیه هیچ پاسخی ارسال نکرده است. افزایش زمان timeout برای گزارش‌های واقعاً کُند صحیح است، اما برای برنامه‌ای که دچار قفل‌شدگی (stuck) شده، راه‌حل اشتباهی است.

تمام مسیرها از سمت برنامه خطای 404 برمی‌گردانند. قانون اسلش انتهایی (trailing slash)، مسیر را بازنویسی کرده است. مسیری که برنامه لاگ می‌کند را با مسیری که درخواست کرده‌اید مقایسه کنید.

سایت دیگری پاسخ می‌دهد. مقدار server_name با هدر Host مطابقت ندارد، بنابراین درخواست به بلوک default_server ارجاع داده شده است.

صفحه بارگذاری می‌شود، اما رابط کاربری پس از حدود یک دقیقه متوقف می‌شود. این مربوط به مورد WebSocket است: مدیریت Upgrade وجود ندارد، یا مقدار proxy_read_timeout همچنان روی 60 ثانیه تنظیم شده است.

FAQ

چرا nginx پس از افزودن proxy_pass خطای 502 Bad Gateway برمی‌گرداند؟

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 ثانیه قرار دارد که فاصله بین دو خواندن از سمت backend را اندازه‌گیری می‌کند. سوکت ساکت توسط nginx بسته می‌شود و کنسول مرورگر کد خطای 1006 را گزارش می‌دهد. مقدار proxy_http_version 1.1 را تنظیم کنید، Upgrade و Connection را با استفاده از map روی $http_upgrade عبور دهید و proxy_read_timeout را به مقداری مانند 3600s افزایش دهید. بدون هدر Upgrade، عملیات ارتقا (upgrade) هرگز انجام نمی‌شود و برنامه به حالت polling بازمی‌گردد یا هیچ به‌روزرسانی زنده‌ای نشان نمی‌دهد.

آیا اسلش انتهایی در proxy_pass اهمیت دارد؟

بله، این اسلش مسیری را که backend دریافت می‌کند تغییر می‌دهد. با location /app/ و proxy_pass http://127.0.0.1:3000/، درخواستی برای /app/status به صورت /status به backend می‌رسد، زیرا هر URI پس از host و port جایگزین پیشوند location منطبق می‌شود. آن اسلش نهایی را حذف کنید تا همان درخواست به صورت /app/status ارسال شود. حذف پیشوند اغلب باعث خرابی لینک‌های asset خود برنامه می‌شود، زیرا آن‌ها مطلق باقی می‌مانند و در ریشه سایت خطای 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 استفاده کرده است. اگر backend روی میزبان دیگری در شبکه‌ای قرار دارد که آن را کنترل نمی‌کنید، آن بخش از مسیر نیاز به محافظت اختصاصی دارد؛ یا از طریق HTTPS به سمت backend یا با استفاده از یک تونل خصوصی بین دو ماشین.