توی این درس یاد میگیری یک اپلیکیشن (اینجا یک اپ Node.js) را پشت Nginx بگذاری: اپ روی 127.0.0.1:3000 و دور از اینترنت، و Nginx روی پورت ۸۰ با proxy_pass درخواستها را به آن برساند. میبینی بدون تنظیم درست، اپ IP همهی کاربران را 127.0.0.1 و دامنه را 127.0.0.1:3000 میبیند؛ و با هدرهای Host، X-Real-IP، X-Forwarded-For و X-Forwarded-Proto درستش میکنی. رفتار عجیب اسلش آخر proxy_pass را آزمایش میکنی، timeout ها را تنظیم میکنی و فرق 502 Bad Gateway و 504 Gateway Timeout را از نزدیک میبینی.
مسئله: اپ تو اینترنت را نمیشناسد
Section titled “مسئله: اپ تو اینترنت را نمیشناسد”اپهای Node، Python، Go یا PHP یک سرور HTTP داخلی دارند و میشود مستقیم روی پورت ۸۰ گذاشتشان. ولی آنها برای «برنامهنویسی منطق کسبوکار» ساخته شدهاند، نه برای اینترنت: HTTPS، فایل استاتیک، فشردهسازی، کاربران با اینترنت کند، حملهها و چند دامنه. الگوی استاندارد این است که اپ فقط روی 127.0.0.1 (یا شبکهی داخلی) گوش بدهد و Nginx جلویش بنشیند. ولی وقتی واسطهای وسط میآید، اپ اطلاعاتی را که از اتصال مستقیم میگرفت (IP واقعی کاربر، اینکه HTTPS بوده یا نه، دامنهی درخواست) از دست میدهد؛ مگر اینکه Nginx آنها را در هدرها به اپ بدهد.
تشبیه: منشی و مدیر
Section titled “تشبیه: منشی و مدیر”reverse proxy مثل منشی یک مدیر است. مراجعان (کاربران) با منشی (Nginx) حرف میزنند و منشی پیام را به مدیر (اپ) میرساند. اگر منشی فقط بگوید «یک نفر این را خواسته»، مدیر نمیداند چه کسی (IP)، برای کدام شرکت (Host) و از کدام در (HTTP یا HTTPS) آمده. منشی خوب یک یادداشت همراه پیام میگذارد: «آقای فلانی، از طرف شرکت X، از در اصلی» (هدرهای X-Forwarded-*). و اگر مدیر خیلی طول بدهد، منشی بعد از مدتی به مراجع میگوید «فعلاً جواب نمیدهد» (timeout و 504).
یک درخواست، دو اتصال
Section titled “یک درخواست، دو اتصال”مثالهای عملی
Section titled “مثالهای عملی”این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24، Node.js 18 از مخزن Ubuntu) با کاربر root اجرا شده. اپ آزمایشی یک سرور کوچک Node است که هر چه از درخواست میبیند (IP اتصال، مسیر، هدرها) را بهصورت JSON برمیگرداند؛ پس میتوانیم ببینیم Nginx دقیقاً چه چیزی به اپ تحویل میدهد.
مثال ۱: اپ Node بهعنوان سرویس systemd
Section titled “مثال ۱: اپ Node بهعنوان سرویس systemd”اول خود اپ. فایل server.js فقط از ماژول داخلی http Node استفاده میکند (بدون npm install):
// tiny test app: reports what it sees about each requestconst http = require("http");
const server = http.createServer((req, res) => { const url = new URL(req.url, "http://placeholder"); if (url.pathname === "/slow") { // wait ?s= seconds before answering (for timeout tests) const s = Number(url.searchParams.get("s") || 3); return setTimeout(() => { res.end(`slow answer after ${s}s\n`); }, s * 1000); } const body = { app_saw_client: req.socket.remoteAddress, http_version: req.httpVersion, method: req.method, path: req.url, host: req.headers["host"], x_real_ip: req.headers["x-real-ip"], x_forwarded_for: req.headers["x-forwarded-for"], x_forwarded_proto: req.headers["x-forwarded-proto"], connection: req.headers["connection"], }; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(body, null, 1) + "\n");});
server.listen(3000, "127.0.0.1", () => console.log("app listening on 127.0.0.1:3000"));برای اینکه اپ مثل یک برنامهی واقعی بعد از ریبوت یا crash خودش بالا بیاید، آن را یک سرویس systemd میکنیم (درس سرویسها در دورهی لینوکس)؛ با کاربر کمدسترسی www-data:
[Unit]Description=LoopX test Node appAfter=network.target
[Service]ExecStart=/usr/bin/node /srv/node-app/server.jsUser=www-dataRestart=on-failureEnvironment=NODE_ENV=production
[Install]WantedBy=multi-user.targetsystemctl daemon-reloadsystemctl enable --now lx-node 2>&1 | tail -n 1sleep 1systemctl is-active lx-nodess -tlnp | grep ':3000'echo "--- مستقیم به اپ (از داخل سرور):"curl -s http://127.0.0.1:3000/helloCreated symlink /etc/systemd/system/multi-user.target.wants/lx-node.service → /etc/systemd/system/lx-node.service.activeLISTEN 0 511 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=21329,fd=24))--- مستقیم به اپ (از داخل سرور):{ "app_saw_client": "127.0.0.1", "http_version": "1.1", "method": "GET", "path": "/hello", "host": "127.0.0.1:3000"}اپ فقط روی 127.0.0.1:3000 گوش میدهد (ss)، پس از بیرون سرور اصلاً قابل دسترس نیست. وقتی مستقیم وصل شدیم، اپ کلاینت را 127.0.0.1 دید (درست، چون curl واقعاً از همین ماشین بود)، با HTTP/1.1.
مثال ۲: سادهترین proxy_pass، و چیزی که اپ میبیند
Section titled “مثال ۲: سادهترین proxy_pass، و چیزی که اپ میبیند”server { listen 80; server_name shop.test;
location / { proxy_pass http://127.0.0.1:3000; }}حالا از یک «کاربر» با IP دیگر (127.0.0.50؛ مثل درس لاگها، برای شبیهسازی کاربر بیرونی) درخواست میفرستیم:
ln -s /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s --interface 127.0.0.50 http://shop.test/orders?id=5nginx: configuration file /etc/nginx/nginx.conf test is successful{ "app_saw_client": "127.0.0.1", "http_version": "1.0", "method": "GET", "path": "/orders?id=5", "host": "127.0.0.1:3000", "connection": "close"}درخواست به اپ رسید و مسیر (/orders?id=5) هم درست بود، ولی ببین اپ چه چیزهایی را اشتباه دید:
app_saw_client: 127.0.0.1بهجای127.0.0.50: اپ فقط اتصال Nginx را میبیند.host: 127.0.0.1:3000بهجایshop.test: Nginx بهصورت پیشفرض هدرHostرا با آدرس داخلproxy_passعوض میکند. هر لینکی که اپ بسازد (ایمیل، ریدایرکت) به127.0.0.1:3000اشاره میکند؛ سناریوی اول درس.http_version: 1.0وconnection: close: Nginx بهصورت پیشفرض با HTTP/1.0 به اپ وصل میشود و بعد از هر درخواست اتصال را میبندد (مثال ۷).- هیچ هدر
X-*ای نیست.
مثال ۳: هدرهای درست
Section titled “مثال ۳: هدرهای درست”با proxy_set_header هدرهایی که به اپ فرستاده میشود را تعیین میکنیم:
cat > /etc/nginx/sites-available/shop.test <<'EOF'server { listen 80; server_name shop.test;
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; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s --interface 127.0.0.50 http://shop.test/orders?id=5nginx: configuration file /etc/nginx/nginx.conf test is successful{ "app_saw_client": "127.0.0.1", "http_version": "1.0", "method": "GET", "path": "/orders?id=5", "host": "shop.test", "x_real_ip": "127.0.0.50", "x_forwarded_for": "127.0.0.50", "x_forwarded_proto": "http", "connection": "close"}حالا اپ همهچیز را دارد:
| هدر | مقدار | معنی |
|---|---|---|
Host |
shop.test |
دامنهای که کاربر خواسته ($host) |
X-Real-IP |
127.0.0.50 |
IP واقعی کاربر ($remote_addr) |
X-Forwarded-For |
127.0.0.50 |
زنجیرهی IP ها (مثال ۴) |
X-Forwarded-Proto |
http |
پروتکل کاربر؛ بعد از HTTPS میشود https ($scheme) |
اپ (یا فریمورکش) باید تنظیم شود که به این هدرها اعتماد کند؛ مثلاً در Express با app.set("trust proxy", "loopback")، در Django با SECURE_PROXY_SSL_HEADER و USE_X_FORWARDED_HOST. چون اپ فقط روی 127.0.0.1 است، فقط Nginx میتواند این هدرها را بفرستد.
این چهار خط آنقدر رایجاند که Ubuntu فایلش را آماده دارد: /etc/nginx/proxy_params. بهجای تکرار، include کن:
cat /etc/nginx/proxy_paramssed -i '/proxy_set_header/d; s| proxy_pass http://127.0.0.1:3000;| proxy_pass http://127.0.0.1:3000;\n include proxy_params;|' /etc/nginx/sites-available/shop.testcat /etc/nginx/sites-available/shop.testnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s --interface 127.0.0.50 http://shop.test/ | grep -E '"(host|x_real_ip)"'proxy_set_header Host $http_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;server { listen 80; server_name shop.test;
location / { proxy_pass http://127.0.0.1:3000; include proxy_params; }}nginx: configuration file /etc/nginx/nginx.conf test is successful "host": "shop.test", "x_real_ip": "127.0.0.50",(تنها فرقش با نسخهی ما: Host $http_host بهجای $host؛ $http_host هدر Host را دقیقاً همانطور که کاربر فرستاده، با پورت، منتقل میکند.)
مثال ۴: X-Forwarded-For و جعل IP
Section titled “مثال ۴: X-Forwarded-For و جعل IP”X-Forwarded-For یک زنجیره است: هر proxy در مسیر، IP کسی را که به او وصل شده به انتهای آن اضافه میکند ($proxy_add_x_forwarded_for یعنی «XFF ای که آمده، بهعلاوهی $remote_addr»). ولی هر کسی میتواند خودش یک XFF جعلی بفرستد:
curl -s --interface 127.0.0.50 -H 'X-Forwarded-For: 1.2.3.4' http://shop.test/ | grep -E '"(x_real_ip|x_forwarded_for)"' "x_real_ip": "127.0.0.50", "x_forwarded_for": "1.2.3.4, 127.0.0.50",کاربر (127.0.0.50) ادعا کرد IP اش 1.2.3.4 است. Nginx ادعا را نگه داشت و IP واقعی را به انتها اضافه کرد: 1.2.3.4, 127.0.0.50. اگر اپ تو «اولین IP» XFF را IP کاربر بداند، با یک هدر ساده فریب میخورد (و مثلاً محدودیت ورود به ازای IP را دور میزنند). قاعده: فقط آخرین IP که proxy مورد اعتمادت اضافه کرده قابل اعتماد است؛ یا سادهتر، از X-Real-IP استفاده کن که Nginx خودش از $remote_addr (اتصال واقعی) میسازد و کاربر نمیتواند عوضش کند.
مثال ۵: اسلش آخر proxy_pass
Section titled “مثال ۵: اسلش آخر proxy_pass”این ریزترین و پراشتباهترین قاعدهی reverse proxy است. فرض کن اپ API را در مسیر / خودش دارد ولی روی سایت زیر /api/ میخواهیاش:
cat > /etc/nginx/sites-available/shop.test <<'EOF'server { listen 80; server_name shop.test; include proxy_params;
location /keep/ { proxy_pass http://127.0.0.1:3000; } location /strip/ { proxy_pass http://127.0.0.1:3000/; } location /swap/ { proxy_pass http://127.0.0.1:3000/v2/; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for p in /keep/users/7 /strip/users/7 /swap/users/7; do printf '%-15s -> app got path: ' "$p" curl -s "http://shop.test$p" | grep '"path"' | cut -d'"' -f4donenginx: configuration file /etc/nginx/nginx.conf test is successful/keep/users/7 -> app got path: /keep/users/7/strip/users/7 -> app got path: /users/7/swap/users/7 -> app got path: /v2/users/7- بدون مسیر بعد از آدرس (
http://127.0.0.1:3000): مسیر درخواست همانطور به اپ میرود (/keep/users/7). - با مسیر (حتی فقط
/): بخشی از مسیر که باlocationجور شده (/strip/) با آن مسیر جایگزین میشود:/strip/users/7←/users/7. - و همینطور
/swap/←/v2/.
پس سؤال «اسلش بگذارم یا نه؟» در واقع این است: «اپ مسیر را با پیشوند میخواهد یا بدون آن؟». (دقت کن include proxy_params را این بار در سطح server نوشتیم؛ به همهی location ها ارث رسید، چون هیچکدام proxy_set_header خودشان را ندارند؛ درس ساختار تنظیمات.)
مثال ۶: timeout ها، ۵۰۲ و ۵۰۴
Section titled “مثال ۶: timeout ها، ۵۰۲ و ۵۰۴”دو حالت خرابی رایج:
- اپ خاموش است یا اتصال را رد میکند ← Nginx نمیتواند وصل شود ←
502 Bad Gateway. - اپ وصل است ولی جواب نمیدهد (کند یا گیر کرده) ← بعد از
proxy_read_timeout(پیشفرض ۶۰ ثانیه) ←504 Gateway Timeout.
برای آزمایش، timeout را روی ۲ ثانیه میگذاریم و از مسیر /slow اپ میخواهیم ۱ یا ۴ ثانیه صبر کند:
cat > /etc/nginx/sites-available/shop.test <<'EOF'server { listen 80; server_name shop.test;
location / { proxy_pass http://127.0.0.1:3000; include proxy_params; proxy_connect_timeout 2s; proxy_read_timeout 2s; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1for s in 1 4; do curl -s -o /tmp/body -w "slow?s=$s -> HTTP %{http_code} after %{time_total}s " "http://shop.test/slow?s=$s" head -n 1 /tmp/body | tr -d '\r' | sed 's/<[^>]*>//g'; echodoneecho "=== اپ را متوقف میکنیم:"systemctl stop lx-nodecurl -s -o /dev/null -w "HTTP %{http_code} after %{time_total}s\n" http://shop.test/echo "=== error log:"grep -E 'timed out|Connection refused' /var/log/nginx/error.log | tail -n 2 | sed -E 's/^[0-9/]+ [0-9:]+ //; s/, client.*//'systemctl start lx-nodenginx: configuration file /etc/nginx/nginx.conf test is successfulslow?s=1 -> HTTP 200 after 1.004842s slow answer after 1s
slow?s=4 -> HTTP 504 after 2.003328s
=== اپ را متوقف میکنیم:HTTP 502 after 0.000961s=== error log:[error] 21421#21421: *17 upstream timed out (110: Connection timed out) while reading response header from upstream[error] 21422#21422: *19 connect() failed (111: Connection refused) while connecting to upstreams=1زیر timeout بود: ۲۰۰.s=4بعد از دقیقاً ۲ ثانیه با ۵۰۴ قطع شد؛ در error log:upstream timed out ... while reading response header from upstream.- با اپ خاموش، فوراً ۵۰۲ (اتصال رد شد،
Connection refused).
timeout را برای هر location جدا تنظیم کن: یک endpoint گزارشگیری سنگین شاید ۱۲۰ ثانیه لازم داشته باشد، ولی API معمولی نباید بیش از چند ثانیه کاربر را منتظر بگذارد.
مثال ۷: صفحهی خطای دوستانه و اتصال keepalive
Section titled “مثال ۷: صفحهی خطای دوستانه و اتصال keepalive”وقتی اپ در حال ریاستارت (مثلاً هنگام دیپلوی) است، کاربر بهجای صفحهی خشک «502 Bad Gateway» بهتر است یک صفحهی «چند لحظهی دیگر برمیگردیم» ببیند. همچنین با upstream و keepalive، اتصالهای Nginx به اپ باز میمانند و برای هر درخواست اتصال تازه ساخته نمیشود:
mkdir -p /var/www/errors && echo '<h1>We are updating the shop. Back in a minute!</h1>' > /var/www/errors/50x.htmlcat > /etc/nginx/sites-available/shop.test <<'EOF'upstream node_app { server 127.0.0.1:3000; keepalive 16;}
server { listen 80; server_name shop.test;
location / { proxy_pass http://node_app; include proxy_params; proxy_http_version 1.1; proxy_set_header Connection ""; }
error_page 502 503 504 /50x.html; location = /50x.html { root /var/www/errors; internal; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s http://shop.test/ | grep -E '"(http_version|connection|host)"'echo "=== هنگام توقف اپ:"systemctl stop lx-nodecurl -s -w " (HTTP %{http_code})\n" http://shop.test/systemctl start lx-nodesleep 1curl -s -o /dev/null -w "بعد از شروع دوباره: HTTP %{http_code}\n" http://shop.test/nginx: configuration file /etc/nginx/nginx.conf test is successful "http_version": "1.1", "host": "shop.test",=== هنگام توقف اپ:<h1>We are updating the shop. Back in a minute!</h1> (HTTP 502)بعد از شروع دوباره: HTTP 200- حالا اپ HTTP/1.1 میبیند و هدر
Connection: closeدیگر فرستاده نمیشود (پس اتصال بعد از هر درخواست بسته نمیشود). سه شرط keepalive به اپ:keepalive Nدرupstream،proxy_http_version 1.1وproxy_set_header Connection "". (چون اینجا یکproxy_set_headerدر location داریم، بایدinclude proxy_paramsهم در همان location باشد؛ قاعدهی ارثبری.) - هنگام توقف اپ، کاربر صفحهی خودمان را با کد ۵۰۲ دید (کد حفظ شد، فقط بدنه عوض شد).
پشت پرده: دو اتصال را ببینیم
Section titled “پشت پرده: دو اتصال را ببینیم”در مدتی که یک درخواست کند در جریان است، ss هر دو اتصال TCP را نشان میدهد:
curl -s -o /dev/null --interface 127.0.0.50 "http://shop.test/slow?s=2" &sleep 1ss -tnp | grep -E ':(80|3000) ' | grep -v LISTEN | awk '{print $4, "->", $5, $6}' | sed -E 's/,fd=[0-9]+//g; s/pid=[0-9]+,//g' | sortwait127.0.0.1:3000 -> 127.0.0.1:45038 users:(("node",pid=21474))127.0.0.1:3000 -> 127.0.0.1:45046 users:(("node",pid=21474))127.0.0.1:45038 -> 127.0.0.1:3000 users:(("nginx",pid=21462))127.0.0.1:45046 -> 127.0.0.1:3000 users:(("nginx",pid=21461))127.0.0.1:80 -> 127.0.0.50:56606 users:(("nginx",pid=21461))127.0.0.50:56606 -> 127.0.0.1:80 users:(("curl",pid=21487))دو اتصال جدا: 127.0.0.50 ← → 127.0.0.1:80 (کاربر با Nginx) و 127.0.0.1 ← → 127.0.0.1:3000 (Nginx با node). اپ هرگز 127.0.0.50 را نمیبیند؛ فقط از روی هدرها میداند. (اتصالهای keepalive قبلی Nginx به اپ هم ممکن است در فهرست باشند.)
یک جزئیات مهم دیگر: Nginx پاسخ اپ را بهصورت پیشفرض بافر (buffer) میکند؛ یعنی پاسخ را سریع از اپ میگیرد، اتصال اپ را آزاد میکند و بعد با سرعت کاربر (که شاید با اینترنت موبایل کند است) برایش میفرستد. اپ زودتر آزاد میشود تا به درخواست بعدی برسد. برای پاسخهایی که باید زنده برسند (مثل Server-Sent Events یا خروجی تدریجی)، proxy_buffering off;.
جدولهای مرجع
Section titled “جدولهای مرجع”هدرهای استاندارد reverse proxy:
| تنظیم | چه چیزی به اپ میرسد |
|---|---|
proxy_set_header Host $host; |
دامنهی درخواست (بدون پورت) |
proxy_set_header Host $http_host; |
هدر Host همانطور که آمده (با پورت؛ در proxy_params) |
proxy_set_header X-Real-IP $remote_addr; |
IP اتصال واقعی |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
زنجیرهی IP ها |
proxy_set_header X-Forwarded-Proto $scheme; |
http یا https |
include proxy_params; |
هر چهار (فایل آمادهی Ubuntu) |
proxy_pass و مسیر (در location /api/):
proxy_pass |
درخواست /api/users به اپ میرسد با |
|---|---|
http://127.0.0.1:3000 |
/api/users |
http://127.0.0.1:3000/ |
/users |
http://127.0.0.1:3000/v2/ |
/v2/users |
timeout ها و تنظیمهای مهم:
| directive | پیشفرض | کار |
|---|---|---|
proxy_connect_timeout |
60s |
حداکثر زمان برای برقراری اتصال با اپ |
proxy_read_timeout |
60s |
حداکثر سکوت بین دو تکهی پاسخ اپ (پیامد: ۵۰۴) |
proxy_send_timeout |
60s |
حداکثر سکوت هنگام فرستادن درخواست به اپ |
proxy_http_version |
1.0 |
برای keepalive و WebSocket: 1.1 |
proxy_buffering |
on |
off برای پاسخهای زنده (SSE) |
client_max_body_size |
1m |
حداکثر اندازهی بدنهی درخواست (آپلود؛ بیشتر ← 413) |
کدهای خطای reverse proxy:
| کد | یعنی | خط error log |
|---|---|---|
502 Bad Gateway |
اتصال به اپ ناموفق یا پاسخ نامعتبر | connect() failed (111: Connection refused) |
504 Gateway Timeout |
اپ در زمان مقرر جواب نداد | upstream timed out (110: Connection timed out) |
413 Request Entity Too Large |
بدنهی درخواست بزرگتر از client_max_body_size |
client intended to send too large body |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) فراموش کردن Host و X-Forwarded-*
Section titled “۱) فراموش کردن Host و X-Forwarded-*”مثال ۲: اپ IP همه را 127.0.0.1 و دامنه را 127.0.0.1:3000 میبیند. راهحل: include proxy_params; (و تنظیم «اعتماد به proxy» در فریمورک).
۲) اعتماد به اولین IP X-Forwarded-For
Section titled “۲) اعتماد به اولین IP X-Forwarded-For”مثال ۴: هر کسی میتواند XFF جعلی بفرستد. راهحل: X-Real-IP یا آخرین IP زنجیره که proxy خودت اضافه کرده.
۳) اسلش آخر proxy_pass
Section titled “۳) اسلش آخر proxy_pass”مثال ۵: یک / اضافه یا جاافتاده، همهی مسیرها را برای اپ عوض میکند (۴۰۴ از اپ). راهحل: با اپی که مسیر را برمیگرداند (یا لاگ اپ) ببین چه مسیری میرسد.
۴) آپلود فایل بزرگ و ۴۱۳
Section titled “۴) آپلود فایل بزرگ و ۴۱۳”head -c 2000000 /dev/zero > /tmp/big.bincurl -s -o /dev/null -w "upload 2 MB: HTTP %{http_code}\n" -X POST --data-binary @/tmp/big.bin http://shop.test/uploadgrep 'too large body' /var/log/nginx/error.log | tail -n 1 | sed -E 's/^[0-9/]+ [0-9:]+ //; s/, client:.*//'rm /tmp/big.binupload 2 MB: HTTP 413[error] 21459#21459: *29 client intended to send too large body: 2000000 bytesپیشفرض client_max_body_size فقط ۱ مگابایت است؛ آپلود بزرگتر قبل از رسیدن به اپ با 413 رد میشود. راهحل: client_max_body_size 20m; در server یا location آپلود.
۵) اپ روی 0.0.0.0
Section titled “۵) اپ روی 0.0.0.0”از بیرون، بدون Nginx، قابل دسترس است. راهحل: 127.0.0.1 و فایروال.
۶) timeout پیشفرض ۶۰ ثانیه برای همهچیز
Section titled “۶) timeout پیشفرض ۶۰ ثانیه برای همهچیز”کاربر یک دقیقه جلوی صفحهی سفید منتظر میماند، یا برعکس، گزارش سنگین بعد از ۶۰ ثانیه با ۵۰۴ قطع میشود. راهحل: timeout را برای هر location متناسب با کارش بگذار.
تمرین اصلی درس (بخش اول): یک اپ Node را پشت Nginx بگذار. از همان اپ این درس استفاده کن (یا اپ خودت)، آن را روی 127.0.0.1:3000 نگه دار و یک server block برای shop.test با proxy_pass و include proxy_params بساز. با curl از یک IP «کاربر» ثابت کن اپ دامنه و IP واقعی را میبیند، و با ss ثابت کن اپ از بیرون قابل دسترس نیست.
دیدن جواب
cat > /etc/nginx/sites-available/shop.test <<'EOF'server { listen 80; server_name shop.test;
location / { proxy_pass http://127.0.0.1:3000; include proxy_params; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1curl -s --interface 127.0.0.77 http://shop.test/products | grep -E '"(app_saw_client|host|x_real_ip|path)"'echo "--- اپ روی چه آدرسی گوش میدهد:"ss -tln | grep ':3000'ip=$(hostname -I | awk '{print $1}')curl -s -m 2 -o /dev/null "http://$ip:3000/"; echo "از IP شبکهی سرور ($ip:3000): curl exit code $?"nginx: configuration file /etc/nginx/nginx.conf test is successful "app_saw_client": "127.0.0.1", "path": "/products", "host": "shop.test", "x_real_ip": "127.0.0.77",--- اپ روی چه آدرسی گوش میدهد:LISTEN 0 511 127.0.0.1:3000 0.0.0.0:*از IP شبکهی سرور (172.17.0.3:3000): curl exit code 7اپ اتصال را از 127.0.0.1 (Nginx) میبیند ولی از هدرها دامنه (shop.test) و IP واقعی (127.0.0.77) را دارد. ss فقط 127.0.0.1:3000 را نشان میدهد و اتصال از IP شبکهی سرور رد شد (curl کد ۷)؛ پس تنها راه رسیدن به اپ از بیرون، Nginx است.
روی همان سایت، دو قسمت جدا بساز: /api/ به اپ برود بدون پیشوند /api (اپ مسیر /users را ببیند)، با proxy_read_timeout 5s؛ و /reports/ به اپ برود با پیشوند، با proxy_read_timeout 30s. با /slow?s=8 در هر کدام ثابت کن timeout ها جدا هستند. (راهنمایی: query string را هم نگاه کن.)
دیدن جواب
cat > /etc/nginx/sites-available/shop.test <<'EOF'server { listen 80; server_name shop.test; include proxy_params;
location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_read_timeout 5s; } location /reports/ { proxy_pass http://127.0.0.1:3000; proxy_read_timeout 30s; }}EOFnginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1echo "path seen by app for /api/users?page=2: $(curl -s 'http://shop.test/api/users?page=2' | grep '"path"' | cut -d'"' -f4)"echo "path seen by app for /reports/2026: $(curl -s 'http://shop.test/reports/2026' | grep '"path"' | cut -d'"' -f4)"curl -s -o /dev/null -w "/api/slow?s=8 -> HTTP %{http_code} after %{time_total}s\n" 'http://shop.test/api/slow?s=8'curl -s -o /dev/null -w "/reports/slow?s=8 -> HTTP %{http_code} after %{time_total}s\n" 'http://shop.test/reports/slow?s=8'nginx: configuration file /etc/nginx/nginx.conf test is successfulpath seen by app for /api/users?page=2: /users?page=2path seen by app for /reports/2026: /reports/2026/api/slow?s=8 -> HTTP 504 after 5.006547s/reports/slow?s=8 -> HTTP 200 after 0.003381s/api/users?page=2 به اپ رسید به شکل /users?page=2 (پیشوند برداشته شد، query ماند). /api/slow?s=8 بعد از ۵ ثانیه با ۵۰۴ قطع شد. /reports/slow به اپ با پیشوند رسید (/reports/slow)؛ اپ ما فقط مسیر دقیق /slow را کند میکند، پس این یکی فوراً جواب داد. برای دیدن timeout ۳۰ ثانیهای، باید اپ مسیر /reports/slow را هم کند کند؛ نکتهی واقعی: پیشوند یعنی اپ باید مسیر کامل را بشناسد.
تمرین اصلی درس (بخش دوم، دیپلوی بدون قطعی): دو نسخه از اپ روی پورتهای ۳۰۰۰ و ۳۰۰۱ اجرا کن و با یک upstream پشت Nginx بگذار (هر دو فعال). سپس «دیپلوی» را شبیهسازی کن: نسخهی ۳۰۰۰ را متوقف کن، در همین حال ۲۰ درخواست پشت هم بفرست، و ثابت کن هیچ درخواستی شکست نخورد. (راهنمایی: proxy_next_upstream بهصورت پیشفرض برای error و timeout درخواست را به سرور بعدی میدهد.)
دیدن جواب
sed 's/listen(3000/listen(3001/; s/127.0.0.1:3000/127.0.0.1:3001/' /srv/node-app/server.js > /srv/node-app/server-3001.js(exec node /srv/node-app/server-3001.js) > /dev/null 2>&1 &cat > /etc/nginx/sites-available/shop.test <<'EOF'upstream node_app { server 127.0.0.1:3000; server 127.0.0.1:3001;}server { listen 80; server_name shop.test; location / { proxy_pass http://node_app; include proxy_params; proxy_connect_timeout 1s; }}EOFsleep 1nginx -t 2>&1 | tail -n 1 && systemctl reload nginxsleep 1systemctl stop lx-nodeecho "نسخهی 3000 متوقف شد: $(systemctl is-active lx-node)"for i in $(seq 1 20); do curl -s -o /dev/null -w '%{http_code}\n' http://shop.test/; done | sort | uniq -cgrep -c 'Connection refused' /var/log/nginx/error.logsystemctl start lx-nodepkill -f server-3001.jsnginx: configuration file /etc/nginx/nginx.conf test is successfulنسخهی 3000 متوقف شد: inactive 20 2006هر ۲۰ درخواست ۲۰۰ گرفتند. وقتی Nginx به ۳۰۰۰ وصل نشد، همان درخواست را به ۳۰۰۱ داد (proxy_next_upstream error timeout پیشفرض است) و بعد از چند شکست، ۳۰۰۰ را موقتاً کنار گذاشت (max_fails و fail_timeout؛ درس load balancing). error log شکستها را ثبت کرد ولی کاربر چیزی نفهمید. این پایهی دیپلوی بدون قطعی (zero-downtime) است: نسخهها را یکییکی بهروز کن.
آزمونک
Section titled “آزمونک”اپ پشت Nginx همهی کاربران را 127.0.0.1 میبیند. چه چیزی کم است؟
اپ فقط اتصال Nginx را میبیند؛ IP کاربر باید در هدر برسد.
با proxy_pass http://127.0.0.1:3000; (بدون proxy_set_header Host) اپ چه Host ای میبیند؟
پیشفرض proxy_set_header Host $proxy_host است؛ $host یا $http_host بفرست.
در location /api/، proxy_pass http://127.0.0.1:3000/; درخواست /api/users را با چه مسیری به اپ میفرستد؟
وقتی proxy_pass مسیر دارد (حتی /)، بخش جورشدهی location جایگزین میشود.
کاربری X-Forwarded-For: 1.2.3.4 فرستاده و Nginx $proxy_add_x_forwarded_for دارد. اپ چه XFF ای میبیند؟
به اولین IP اعتماد نکن؛ X-Real-IP یا آخرین IP proxy خودت.
اپ خاموش است. کاربر چه کدی میگیرد؟
502: اتصال به اپ رد شد. 504: اپ وصل است ولی در زمان مقرر جواب نداد.
گزارشگیری سنگینی ۹۰ ثانیه طول میکشد و بعد از ۶۰ ثانیه ۵۰۴ میگیرد. چه تنظیمی؟
read_timeout سکوت در هنگام خواندن پاسخ است؛ پیشفرض ۶۰ ثانیه.
آپلود فایل ۵ مگابایتی با 413 رد میشود. علت و راهحل؟
Nginx قبل از رسیدن به اپ رد میکند.
جمعبندی
Section titled “جمعبندی”- reverse proxy: اپ روی
127.0.0.1:PORT، Nginx روی ۸۰/۴۴۳ باproxy_pass. دو اتصال جدا؛ اپ فقط Nginx را میبیند. - هدرها:
Host،X-Real-IP،X-Forwarded-For،X-Forwarded-Proto(یاinclude proxy_params;)؛ و در اپ «اعتماد به proxy» را روشن کن. به اولین IP XFF اعتماد نکن. - اسلش
proxy_pass: بدون مسیر ← مسیر دستنخورده؛ با مسیر (حتی/) ← بخش جورشدهی location جایگزین میشود. - خطاها: ۵۰۲ = اتصال به اپ ناموفق؛ ۵۰۴ = اپ دیر جواب داد (
proxy_read_timeout)؛ ۴۱۳ = بدنه بزرگ (client_max_body_size). timeout را برای هر location تنظیم کن و صفحهی خطای دوستانه بگذار. - keepalive به اپ:
upstream { keepalive N; }+proxy_http_version 1.1;+proxy_set_header Connection "";. - اپ را با systemd اجرا کن تا بعد از crash و ریبوت بالا بیاید؛ با دو نسخه و
upstream، دیپلوی بدون قطعی.
| دستور | کاری که میکند |
|---|---|
proxy_pass http://127.0.0.1:3000; | فرستادن به اپ (مسیر دستنخورده) |
proxy_pass http://127.0.0.1:3000/; | حذف پیشوند location از مسیر |
include proxy_params; | Host، X-Real-IP، X-Forwarded-For، X-Forwarded-Proto |
proxy_set_header X-Real-IP $remote_addr; | IP واقعی کاربر |
proxy_read_timeout 120s; | اپ کند (جلوگیری از ۵۰۴ زودهنگام) |
proxy_connect_timeout 2s; | تشخیص سریع اپ خاموش |
client_max_body_size 20m; | آپلود بزرگتر از ۱ مگابایت |
upstream app { server 127.0.0.1:3000; keepalive 16; } | گروه اپ با اتصالهای باز |
proxy_http_version 1.1; proxy_set_header Connection ""; | لازم برای keepalive |
error_page 502 503 504 /50x.html; | صفحهی دوستانه هنگام قطعی اپ |
ss -tnp | grep :3000 | دیدن اتصال Nginx به اپ |