رفتن به محتوا
LoopX

reverse proxy

توی این درس یاد می‌گیری یک اپلیکیشن (اینجا یک اپ 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 آن‌ها را در هدرها به اپ بدهد.

reverse proxy مثل منشی یک مدیر است. مراجعان (کاربران) با منشی (Nginx) حرف می‌زنند و منشی پیام را به مدیر (اپ) می‌رساند. اگر منشی فقط بگوید «یک نفر این را خواسته»، مدیر نمی‌داند چه کسی (IP)، برای کدام شرکت (Host) و از کدام در (HTTP یا HTTPS) آمده. منشی خوب یک یادداشت همراه پیام می‌گذارد: «آقای فلانی، از طرف شرکت X، از در اصلی» (هدرهای X-Forwarded-*). و اگر مدیر خیلی طول بدهد، منشی بعد از مدتی به مراجع می‌گوید «فعلاً جواب نمی‌دهد» (timeout و 504).

reverse proxy دو اتصال جدا دارد: کاربر به Nginx وصل می‌شود و Nginx یک اتصال تازه به اپ باز می‌کند. از دید اپ، مشتری خود Nginx (127.0.0.1) است؛ برای همین اطلاعات کاربر واقعی باید با هدرهای X-Real-IP، X-Forwarded-For، X-Forwarded-Proto و Host منتقل شود.

این درس روی ماشین آزمایشی 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):

/srv/node-app/server.js
// tiny test app: reports what it sees about each request
const 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:

/etc/systemd/system/lx-node.service
[Unit]
Description=LoopX test Node app
After=network.target
[Service]
ExecStart=/usr/bin/node /srv/node-app/server.js
User=www-data
Restart=on-failure
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
Terminal window
systemctl daemon-reload
systemctl enable --now lx-node 2>&1 | tail -n 1
sleep 1
systemctl is-active lx-node
ss -tlnp | grep ':3000'
echo "--- مستقیم به اپ (از داخل سرور):"
curl -s http://127.0.0.1:3000/hello
خروجی
Created symlink /etc/systemd/system/multi-user.target.wants/lx-node.service → /etc/systemd/system/lx-node.service.
active
LISTEN 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، و چیزی که اپ می‌بیند”
/etc/nginx/sites-available/shop.test
server {
listen 80;
server_name shop.test;
location / {
proxy_pass http://127.0.0.1:3000;
}
}

حالا از یک «کاربر» با IP دیگر (127.0.0.50؛ مثل درس لاگ‌ها، برای شبیه‌سازی کاربر بیرونی) درخواست می‌فرستیم:

Terminal window
ln -s /etc/nginx/sites-available/shop.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s --interface 127.0.0.50 http://shop.test/orders?id=5
خروجی
nginx: 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-* ای نیست.

با proxy_set_header هدرهایی که به اپ فرستاده می‌شود را تعیین می‌کنیم:

Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s --interface 127.0.0.50 http://shop.test/orders?id=5
خروجی
nginx: 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 کن:

Terminal window
cat /etc/nginx/proxy_params
sed -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.test
cat /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -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 یک زنجیره است: هر proxy در مسیر، IP کسی را که به او وصل شده به انتهای آن اضافه می‌کند ($proxy_add_x_forwarded_for یعنی «XFF ای که آمده، به‌علاوه‌ی $remote_addr»). ولی هر کسی می‌تواند خودش یک XFF جعلی بفرستد:

Terminal window
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 (اتصال واقعی) می‌سازد و کاربر نمی‌تواند عوضش کند.

این ریزترین و پراشتباه‌ترین قاعده‌ی reverse proxy است. فرض کن اپ API را در مسیر / خودش دارد ولی روی سایت زیر /api/ می‌خواهی‌اش:

Terminal window
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/;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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'"' -f4
done
خروجی
nginx: 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 اپ می‌خواهیم ۱ یا ۴ ثانیه صبر کند:

Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for 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'; echo
done
echo "=== اپ را متوقف می‌کنیم:"
systemctl stop lx-node
curl -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-node
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
slow?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 upstream
  • s=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 به اپ باز می‌مانند و برای هر درخواست اتصال تازه ساخته نمی‌شود:

Terminal window
mkdir -p /var/www/errors && echo '<h1>We are updating the shop. Back in a minute!</h1>' > /var/www/errors/50x.html
cat > /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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s http://shop.test/ | grep -E '"(http_version|connection|host)"'
echo "=== هنگام توقف اپ:"
systemctl stop lx-node
curl -s -w " (HTTP %{http_code})\n" http://shop.test/
systemctl start lx-node
sleep 1
curl -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 را نشان می‌دهد:

Terminal window
curl -s -o /dev/null --interface 127.0.0.50 "http://shop.test/slow?s=2" &
sleep 1
ss -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' | sort
wait
خروجی
127.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;.

هدرهای استاندارد 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

۱) فراموش کردن 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 خودت اضافه کرده.

مثال ۵: یک / اضافه یا جاافتاده، همه‌ی مسیرها را برای اپ عوض می‌کند (۴۰۴ از اپ). راه‌حل: با اپی که مسیر را برمی‌گرداند (یا لاگ اپ) ببین چه مسیری می‌رسد.

۴) آپلود فایل بزرگ و ۴۱۳

Section titled “۴) آپلود فایل بزرگ و ۴۱۳”
Terminal window
head -c 2000000 /dev/zero > /tmp/big.bin
curl -s -o /dev/null -w "upload 2 MB: HTTP %{http_code}\n" -X POST --data-binary @/tmp/big.bin http://shop.test/upload
grep 'too large body' /var/log/nginx/error.log | tail -n 1 | sed -E 's/^[0-9/]+ [0-9:]+ //; s/, client:.*//'
rm /tmp/big.bin
خروجی
upload 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 آپلود.

از بیرون، بدون 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 ثابت کن اپ از بیرون قابل دسترس نیست.

دیدن جواب
Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -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 را هم نگاه کن.)

دیدن جواب
Terminal window
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;
}
}
EOF
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
echo "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 successful
path seen by app for /api/users?page=2: /users?page=2
path 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 درخواست را به سرور بعدی می‌دهد.)

دیدن جواب
Terminal window
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;
}
}
EOF
sleep 1
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
systemctl stop lx-node
echo "نسخه‌ی 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 -c
grep -c 'Connection refused' /var/log/nginx/error.log
systemctl start lx-node
pkill -f server-3001.js
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
نسخه‌ی 3000 متوقف شد: inactive
20 200
6

هر ۲۰ درخواست ۲۰۰ گرفتند. وقتی Nginx به ۳۰۰۰ وصل نشد، همان درخواست را به ۳۰۰۱ داد (proxy_next_upstream error timeout پیش‌فرض است) و بعد از چند شکست، ۳۰۰۰ را موقتاً کنار گذاشت (max_fails و fail_timeout؛ درس load balancing). error log شکست‌ها را ثبت کرد ولی کاربر چیزی نفهمید. این پایه‌ی دیپلوی بدون قطعی (zero-downtime) است: نسخه‌ها را یکی‌یکی به‌روز کن.

⚡ بررسی سریع

اپ پشت Nginx همه‌ی کاربران را 127.0.0.1 می‌بیند. چه چیزی کم است؟

؟ آزمونک
  1. با proxy_pass http://127.0.0.1:3000; (بدون proxy_set_header Host) اپ چه Host ای می‌بیند؟

  2. در location /api/، proxy_pass http://127.0.0.1:3000/; درخواست /api/users را با چه مسیری به اپ می‌فرستد؟

  3. کاربری X-Forwarded-For: 1.2.3.4 فرستاده و Nginx $proxy_add_x_forwarded_for دارد. اپ چه XFF ای می‌بیند؟

  4. اپ خاموش است. کاربر چه کدی می‌گیرد؟

  5. گزارش‌گیری سنگینی ۹۰ ثانیه طول می‌کشد و بعد از ۶۰ ثانیه ۵۰۴ می‌گیرد. چه تنظیمی؟

  6. آپلود فایل ۵ مگابایتی با 413 رد می‌شود. علت و راه‌حل؟

  • 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 به اپ