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

server block و چند سایت

توی این درس یاد می‌گیری چند سایت با دامنه‌های مختلف را روی یک سرور و یک IP میزبانی کنی. هر سایت یک server block است با چهار directive اصلی: listen (روی کدام پورت و IP)، server_name (برای کدام دامنه‌ها)، root (فایل‌ها کجا) و index (کدام فایل برای پوشه). می‌فهمی Nginx چطور با هدر Host تصمیم می‌گیرد کدام سایت جواب بدهد، سرور پیش‌فرض (default_server) کیست و چرا باید خودت تعیینش کنی، و با /etc/hosts دو دامنه‌ی محلی می‌سازی تا همه را بدون DNS واقعی آزمایش کنی.

مسئله: یک سرور، چند دامنه

Section titled “مسئله: یک سرور، چند دامنه”

یک سرور ابری با یک IP خریده‌ای. می‌خواهی shop.example.com، blog.example.com و example.com را روی همین سرور داشته باشی. DNS هر سه را به همان IP می‌فرستد و هر سه روی همان پورت ۸۰ می‌آیند. پس Nginx از کجا بفهمد هر درخواست مال کدام سایت است؟ جواب در خودِ درخواست است: مرورگر در هر درخواست HTTP یک هدر Host می‌فرستد که اسم دامنه در آن است. به این روش میزبانی مجازی بر اساس نام (name-based virtual hosting) می‌گویند و تقریباً همه‌ی وب امروز همین‌طور کار می‌کند.

تشبیه: یک ساختمان، چند شرکت

Section titled “تشبیه: یک ساختمان، چند شرکت”

سرور مثل یک ساختمان اداری با یک آدرس پستی (IP) است که چند شرکت در آن هستند. همه‌ی نامه‌ها به همان آدرس می‌آیند و روی پاکت اسم شرکت نوشته شده (هدر Host). نگهبان (Nginx) پاکت را نگاه می‌کند و به طبقه‌ی همان شرکت می‌فرستد (server_name). اگر اسم روی پاکت مال هیچ‌کدام نباشد، نامه به میز پیش‌فرض می‌رود (default_server)؛ و عاقلانه است آن میز یک سطل باشد، نه دفتر مدیرعامل یکی از شرکت‌ها.

شکل کلی
server {
listen 80; # port (and optionally IP) to accept on
server_name shop.test www.shop.test; # which Host names this block serves
root /var/www/shop.test; # where files live
index index.html; # file to show for a directory (/)
}
directive کار پیش‌فرض
listen پورت و IP؛ اختیاری default_server، ssl، http2 *:80
server_name یک یا چند اسم: دقیق، *. wildcard، ~ regex "" (خالی)
root پوشه‌ی فایل‌ها؛ آدرس به آن اضافه می‌شود html (نسبت به prefix نصب)
index فایل(های) پیش‌فرض برای آدرس‌هایی که به / ختم می‌شوند index.html
انتخاب server block در دو مرحله: اول از بین blockهایی که روی آن IP و پورت گوش می‌دهند (listen)، بعد با مقایسه‌ی هدر Host با server_name ها به ترتیب اولویت: اسم دقیق، wildcard ابتدا، wildcard انتها، regex؛ و اگر هیچ‌کدام جور نشد، default_server همان پورت.

این درس روی ماشین آزمایشی server (Ubuntu 24.04، Nginx 1.24) با کاربر root اجرا شده؛ روی سرور خودت جلوی دستورها sudo بگذار. سایت پیش‌فرض Ubuntu را در این درس خاموش می‌کنیم تا رفتار پیش‌فرض را خودمان ببینیم و بسازیم.

مثال ۱: دو دامنه‌ی محلی در /etc/hosts

Section titled “مثال ۱: دو دامنه‌ی محلی در /etc/hosts”

برای آزمایش، به دامنه‌ی واقعی و DNS نیاز نداریم. فایل /etc/hosts نگاشت «اسم ← IP» را روی همین ماشین تعریف می‌کند و قبل از DNS خوانده می‌شود. دو دامنه‌ی shop.test و blog.test را به 127.0.0.1 می‌فرستیم:

Terminal window
printf '127.0.0.1 shop.test www.shop.test\n127.0.0.1 blog.test\n' >> /etc/hosts
tail -n 2 /etc/hosts
getent hosts shop.test blog.test
خروجی
127.0.0.1 shop.test www.shop.test
127.0.0.1 blog.test
127.0.0.1 shop.test www.shop.test
127.0.0.1 blog.test

getent hosts همان چیزی را نشان می‌دهد که برنامه‌ها (مثل curl و مرورگر) موقع پیدا کردن IP می‌بینند. (روی لپ‌تاپ خودت، برای دیدن سایت‌های سرور در مرورگر، همین خط‌ها را با IP سرور به /etc/hosts لپ‌تاپ اضافه کن؛ در ویندوز فایل C:\Windows\System32\drivers\etc\hosts.)

مثال ۲: دو سایت، یک پورت

Section titled “مثال ۲: دو سایت، یک پورت”

برای هر سایت پوشه و فایل تنظیمات جدا:

Terminal window
for site in shop.test blog.test; do
mkdir -p /var/www/$site
echo "<h1>Welcome to $site</h1>" > /var/www/$site/index.html
cat > /etc/nginx/sites-available/$site <<EOF
server {
listen 80;
server_name $site;
root /var/www/$site;
index index.html;
}
EOF
ln -s /etc/nginx/sites-available/$site /etc/nginx/sites-enabled/
done
cat /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s http://shop.test/
curl -s http://blog.test/
خروجی
server {
listen 80;
server_name shop.test;
root /var/www/shop.test;
index index.html;
}
nginx: configuration file /etc/nginx/nginx.conf test is successful
<h1>Welcome to shop.test</h1>
<h1>Welcome to blog.test</h1>

هر دو دامنه به 127.0.0.1:80 رفتند، ولی هر کدام صفحه‌ی خودش را گرفت. (حلقه‌ی بالا با here-doc بدون کوتیشن، $site را در هر فایل باز کرد؛ درس here-doc در دوره‌ی Bash.)

مثال ۳: هدر Host تصمیم می‌گیرد

Section titled “مثال ۳: هدر Host تصمیم می‌گیرد”

ثابت کنیم تصمیم فقط از هدر Host است، نه از اسمی که curl به آن وصل شد. با -H "Host: ..." هدر را دستی عوض می‌کنیم و هر دو بار به IP وصل می‌شویم:

Terminal window
curl -s -H "Host: shop.test" http://127.0.0.1/
curl -s -H "Host: blog.test" http://127.0.0.1/
echo "--- با -v می‌بینیم curl چه هدری فرستاده:"
curl -s -v http://blog.test/ 2>&1 | grep -E '^> (GET|Host)'
خروجی
<h1>Welcome to shop.test</h1>
<h1>Welcome to blog.test</h1>
--- با -v می‌بینیم curl چه هدری فرستاده:
> GET / HTTP/1.1
> Host: blog.test

اتصال هر دو بار به یک IP و پورت بود؛ فقط هدر Host فرق داشت. خط‌های > در خروجی -v همان درخواستی‌اند که curl فرستاده: GET / HTTP/1.1 و Host: blog.test.

مثال ۴: سرور پیش‌فرض (default_server)

Section titled “مثال ۴: سرور پیش‌فرض (default_server)”

حالا درخواستی با دامنه‌ای که هیچ server_nameی ندارد. فعلاً سایت default Ubuntu با default_server روشن است:

Terminal window
grep -n 'default_server\|server_name' /etc/nginx/sites-enabled/default | grep -v '#'
curl -s -H "Host: unknown.example" http://127.0.0.1/ | grep -o '<title>.*</title>'
خروجی
22: listen 80 default_server;
46: server_name _;
<title>Welcome to nginx!</title>

دامنه‌ی ناشناخته به سایتی رسید که listen 80 default_server دارد. حالا ببینیم اگر هیچ default_serverی نباشد چه می‌شود؛ سایت پیش‌فرض Ubuntu را خاموش می‌کنیم:

Terminal window
rm /etc/nginx/sites-enabled/default
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
echo "دامنه‌ی ناشناخته:"
curl -s -H "Host: unknown.example" http://127.0.0.1/
echo "IP خالی (بدون دامنه):"
curl -s http://127.0.0.1/
nginx -T 2>/dev/null | grep '^# configuration file /etc/nginx/sites'
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
دامنه‌ی ناشناخته:
<h1>Welcome to blog.test</h1>
IP خالی (بدون دامنه):
<h1>Welcome to blog.test</h1>
# configuration file /etc/nginx/sites-enabled/blog.test:
# configuration file /etc/nginx/sites-enabled/shop.test:

بدون default_server، Nginx اولین server block آن پورت را (به ترتیبی که خوانده شده؛ اینجا blog.test چون از نظر الفبا قبل از shop.test است) پیش‌فرض می‌داند. یعنی هر کس IP سرور را بزند یا دامنه‌ی عجیبی به IP تو اشاره دهد، وبلاگ را می‌بیند؛ همان مشکل سناریوی اول درس. این رفتار به ترتیب فایل‌ها بستگی دارد و با اضافه شدن یک سایت تازه بی‌صدا عوض می‌شود.

راه حرفه‌ای: یک server block «همه‌گیر (catch-all)» که صریح پیش‌فرض باشد و درخواست‌های ناشناخته را رد کند. کد غیراستاندارد 444 در Nginx یعنی «اتصال را بدون هیچ پاسخی ببند»:

/etc/nginx/sites-available/00-default
server {
listen 80 default_server;
server_name _;
return 444;
}
Terminal window
ln -s /etc/nginx/sites-available/00-default /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
echo "دامنه‌ی ناشناخته:"
curl -s -H "Host: unknown.example" http://127.0.0.1/; echo "curl exit code: $?"
echo "دامنه‌های خودمان:"
curl -s http://shop.test/
curl -s http://blog.test/
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
دامنه‌ی ناشناخته:
curl exit code: 52
دامنه‌های خودمان:
<h1>Welcome to shop.test</h1>
<h1>Welcome to blog.test</h1>

curl با کد ۵۲ (Empty reply from server) تمام شد؛ Nginx اتصال را بدون پاسخ بست. ولی دو سایت واقعی عادی کار می‌کنند. server_name _; یک اسم بی‌معنی است که با هیچ دامنه‌ی واقعی جور نمی‌شود (قرارداد رایج)؛ چیزی که این block را پیش‌فرض می‌کند فقط default_server است، نه _.

مثال ۵: شکل‌های server_name و ریدایرکت www

Section titled “مثال ۵: شکل‌های server_name و ریدایرکت www”

server_name چند شکل دارد. یک block با wildcard برای همه‌ی زیردامنه‌های shop.test، و ریدایرکت www.shop.test به نسخه‌ی بدون www:

/etc/nginx/sites-available/lx-names
server {
listen 80;
server_name www.shop.test;
return 301 http://shop.test$request_uri;
}
server {
listen 80;
server_name *.shop.test;
return 200 "wildcard block: you asked for $host\n";
}
server {
listen 80;
server_name ~^(?<user>[a-z]+)\.blog\.test$;
return 200 "regex block: blog of $user\n";
}
Terminal window
ln -s /etc/nginx/sites-available/lx-names /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for h in www.shop.test api.shop.test img.shop.test sara.blog.test shop.test; do
printf '%-16s -> ' "$h"
curl -s -H "Host: $h" http://127.0.0.1/some/page?x=1 -o /tmp/body -w '%{http_code} %{redirect_url} '
head -c 60 /tmp/body | tr -d '\r\n'
echo
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
www.shop.test -> 301 http://shop.test/some/page?x=1 <html><head><title>301 Moved Permanently</title></head><
api.shop.test -> 200 wildcard block: you asked for api.shop.test
img.shop.test -> 200 wildcard block: you asked for img.shop.test
sara.blog.test -> 200 regex block: blog of sara
shop.test -> 404 <html><head><title>404 Not Found</title></head><body><
  • www.shop.test با اسم دقیق جور شد (اولویت اول، حتی با اینکه *.shop.test هم جورش می‌کرد) و با 301 به http://shop.test/some/page?x=1 فرستاده شد. $request_uri مسیر و query string اصلی را نگه می‌دارد.
  • api.shop.test و img.shop.test با wildcard *.shop.test جور شدند؛ $host اسمی است که کاربر خواسته.
  • sara.blog.test با regex (با ~ شروع می‌شود) جور شد و بخش (?<user>...) در متغیر $user ذخیره شد.
  • shop.test خودش با *.shop.test جور نمی‌شود (wildcard فقط زیردامنه‌ها را می‌گیرد) و به block خودش رسید؛ 404 به این خاطر است که فایل /some/page در آن سایت وجود ندارد.

مثال ۶: listen با IP و پورت‌های مختلف

Section titled “مثال ۶: listen با IP و پورت‌های مختلف”

listen فقط پورت نیست؛ می‌تواند IP هم داشته باشد. سایتی که فقط از داخل خود سرور (مثلاً برای ابزار مانیتورینگ) در دسترس باشد:

/etc/nginx/sites-available/lx-internal
server {
listen 127.0.0.1:8087;
server_name _;
return 200 "internal status page\n";
}
Terminal window
ln -s /etc/nginx/sites-available/lx-internal /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
ss -tln | grep -E ':(80|8087) '
curl -s http://127.0.0.1:8087/
ip=$(hostname -I | awk '{print $1}')
echo "از IP شبکه‌ی سرور ($ip):"
curl -s -m 3 "http://$ip:8087/"; echo "curl exit code: $?"
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 511 127.0.0.1:8087 0.0.0.0:*
internal status page
از IP شبکه‌ی سرور (172.17.0.3):
curl exit code: 7

ss -tln نشان می‌دهد پورت ۸۰ روی همه‌ی آدرس‌ها (0.0.0.0) باز است ولی ۸۰۸۷ فقط روی 127.0.0.1. درخواست از IP شبکه‌ی سرور با کد ۷ (Failed to connect) رد شد؛ این سایت از بیرون اصلاً دیده نمی‌شود، حتی بدون فایروال.

Nginx برای پیدا کردن فایل، آدرس را به root اضافه می‌کند: درخواست /css/app.css با root /var/www/shop.test یعنی فایل /var/www/shop.test/css/app.css. اگر آدرس به / ختم شود (پوشه)، فایل‌های فهرست index را به ترتیب امتحان می‌کند:

Terminal window
mkdir -p /var/www/shop.test/css /var/www/shop.test/docs /var/www/shop.test/empty
echo 'body { color: teal; }' > /var/www/shop.test/css/app.css
echo '<h1>docs home</h1>' > /var/www/shop.test/docs/home.html
sed -i 's/index index.html;/index index.html home.html;/' /etc/nginx/sites-available/shop.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for path in /css/app.css /docs/ /docs /empty/ /nothing.html; do
printf '%-14s -> ' "$path"
curl -s -o /tmp/body -w '%{http_code} %{content_type} %{redirect_url}' "http://shop.test$path"
echo " $(head -c 40 /tmp/body | tr -d '\r\n')"
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
/css/app.css -> 200 text/css body { color: teal; }
/docs/ -> 200 text/html <h1>docs home</h1>
/docs -> 301 text/html http://shop.test/docs/ <html><head><title>301 Moved Permanent
/empty/ -> 403 text/html <html><head><title>403 Forbidden</titl
/nothing.html -> 404 text/html <html><head><title>404 Not Found</titl
  • /css/app.css ← فایل با Content-Type: text/css (از mime.types).
  • /docs/ ← پوشه است؛ index.html نبود، پس دومی (home.html) را داد.
  • /docs (بدون / آخر) ← Nginx فهمید پوشه است و با 301 به /docs/ فرستاد.
  • /empty/ ← پوشه بدون هیچ فایل index: 403 Forbidden؛ Nginx به‌صورت پیش‌فرض فهرست فایل‌های پوشه را نشان نمی‌دهد (درس سایت استاتیک: autoindex).
  • /nothing.html ← 404.

پشت پرده: Nginx چطور اسم‌ها را جست‌وجو می‌کند؟

Section titled “پشت پرده: Nginx چطور اسم‌ها را جست‌وجو می‌کند؟”

Nginx موقع خواندن تنظیمات، برای هر IP:پورت سه جدول درهم‌سازی (hash table) می‌سازد (اسم‌های دقیق، wildcard ابتدا، wildcard انتها) و یک فهرست regex. هنگام درخواست، به ترتیب جست‌وجو می‌کند و اولین جوری را برمی‌دارد:

۱. اسم دقیق (shop.test) ۲. بلندترین wildcard ابتدایی (*.shop.test) ۳. بلندترین wildcard انتهایی (shop.*) ۴. اولین regex به ترتیب نوشتن در تنظیمات (~^...$) ۵. وگرنه default_server آن IP:پورت (یا اولین block)

برای اینکه ببینی کدام block جواب داده، متغیر $server_name (اولین اسم آن block) و $host را در یک هدر برگردان:

Terminal window
cat > /etc/nginx/snippets/lx-debug.conf <<'EOF'
add_header X-Debug-Server "$server_name" always;
add_header X-Debug-Host "$host" always;
EOF
for f in shop.test blog.test; do
sed -i 's| index | include snippets/lx-debug.conf;\n index |' /etc/nginx/sites-available/$f
done
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for h in shop.test SHOP.TEST blog.test.; do
echo "== Host: $h"
curl -s -I -H "Host: $h" http://127.0.0.1/ | grep -i '^x-debug'
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
== Host: shop.test
X-Debug-Server: shop.test
X-Debug-Host: shop.test
== Host: SHOP.TEST
X-Debug-Server: shop.test
X-Debug-Host: shop.test
== Host: blog.test.
X-Debug-Server: blog.test
X-Debug-Host: blog.test

دو جزئیات جالب: نام دامنه حساس به حروف بزرگ و کوچک نیست (SHOP.TEST هم به shop.test رسید و $host آن را کوچک کرد)، و نقطه‌ی آخر (blog.test.، که در DNS یعنی «نام کامل») هم حذف می‌شود.

شکل‌های listen:

نوشتن معنی
listen 80; پورت ۸۰ روی همه‌ی IPv4 ها
listen [::]:80; پورت ۸۰ روی IPv6
listen 127.0.0.1:8087; فقط از داخل خود سرور
listen 80 default_server; پیش‌فرض این پورت
listen 443 ssl; HTTPS (درس HTTPS)

شکل‌های server_name (به ترتیب اولویت):

شکل نمونه جور می‌شود با
دقیق shop.test فقط همان
wildcard ابتدا *.shop.test a.shop.test، a.b.shop.test (نه خود shop.test)
wildcard انتها shop.* shop.test، shop.ir
regex ~^(?<user>\w+)\.blog\.test$ هر چه regex بگوید (با گروه نام‌دار)
چند اسم shop.test www.shop.test هر کدام
_ یا "" هیچ دامنه‌ی واقعی (برای catch-all)

کدهای پاسخی که در این درس دیدیم:

کد معنی
200 موفق
301 انتقال دائمی (www ← بدون www، پوشه بدون /)
403 ممنوع (پوشه بدون index، یا مجوز فایل)
404 پیدا نشد
444 (مخصوص Nginx) بستن اتصال بدون پاسخ

۱) اعتماد به «اولین سایت» به‌عنوان پیش‌فرض

Section titled “۱) اعتماد به «اولین سایت» به‌عنوان پیش‌فرض”

مثال ۴: بدون default_server، ترتیب الفبایی فایل‌ها تعیین می‌کند کدام سایت جواب درخواست‌های ناشناخته را بدهد. راه‌حل: یک block صریح با listen 80 default_server; return 444; (و برای HTTPS، listen 443 ssl default_server;).

۲) دو default_server روی یک پورت

Section titled “۲) دو default_server روی یک پورت”
Terminal window
cat > /etc/nginx/conf.d/lx-second-default.conf <<'EOF'
server {
listen 80 default_server;
return 200 "second default\n";
}
EOF
nginx -t 2>&1 | grep emerg | sed -E 's/^[0-9/]+ [0-9:]+ //'
rm /etc/nginx/conf.d/lx-second-default.conf
خروجی
[emerg] 18447#18447: a duplicate default server for 0.0.0.0:80 in /etc/nginx/sites-enabled/00-default:2

فقط یک default_server به ازای هر IP:پورت مجاز است. سایت پیش‌فرض Ubuntu (default) هم default_server دارد؛ اگر catch-all خودت را می‌سازی، آن را خاموش کن.

۳) فایل‌هایی که www-data نمی‌تواند بخواند

Section titled “۳) فایل‌هایی که www-data نمی‌تواند بخواند”
Terminal window
mkdir -p /srv/private/site && echo '<h1>secret</h1>' > /srv/private/site/index.html
chmod 700 /srv/private
ls -ld /srv/private
sed -i 's|root /var/www/blog.test;|root /srv/private/site;|' /etc/nginx/sites-available/blog.test
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://blog.test/
tail -n 1 /var/log/nginx/error.log | sed -E 's/^[0-9/]+ [0-9:]+ //'
sed -i 's|root /srv/private/site;|root /var/www/blog.test;|' /etc/nginx/sites-available/blog.test
systemctl reload nginx
rm -rf /srv/private
خروجی
drwx------ 3 root root 4096 Oct 4 12:46 /srv/private
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP 403
[error] 18461#18461: *26 "/srv/private/site/index.html" is forbidden (13: Permission denied), client: 127.0.0.1, server: blog.test, request: "GET / HTTP/1.1", host: "blog.test"

پوشه‌ی /srv/private مجوز 700 دارد (فقط root)، و worker ها با کاربر www-data اجرا می‌شوند: 403 و در لاگ خطا Permission denied. همین اتفاق برای سایتی در /root یا پوشه‌ی خانه‌ی یک کاربر (با مجوز پیش‌فرض بسته) می‌افتد؛ کافی است یکی از پوشه‌های مسیر برای www-data مجوز x نداشته باشد (درس دسترسی‌ها). راه‌حل: سایت را در /var/www بگذار و مالک یا مجوزش را درست کن (chmod 755 برای پوشه‌ها، 644 برای فایل‌ها).

۴) server_name که با /etc/hosts یا DNS نمی‌خواند

Section titled “۴) server_name که با /etc/hosts یا DNS نمی‌خواند”

درخواست به سرور می‌رسد ولی به سایت پیش‌فرض می‌رود. اولین سؤال: هدر Host چیست؟ با curl -v ببین و با server_name مقایسه کن (www جاافتاده، دامنه‌ی اشتباه، پورت).

۵) /etc/hosts روی ماشین اشتباه

Section titled “۵) /etc/hosts روی ماشین اشتباه”

برای دیدن سایت آزمایشی در مرورگر لپ‌تاپ، خط /etc/hosts باید روی لپ‌تاپ باشد (با IP سرور)، نه روی سرور. /etc/hosts فقط روی همان ماشینی اثر دارد که رویش نوشته شده.

✎ تمرینآسان

تمرین اصلی درس: دو سایت با دو دامنه‌ی محلی در /etc/hosts راه بینداز (alpha.test و beta.test)، هر کدام با پوشه و صفحه‌ی خودش، و با curl نشان بده هر دامنه صفحه‌ی خودش را می‌گیرد. دامنه‌ی سوم (gamma.test) را هم در /etc/hosts بگذار ولی سایتی برایش نساز و ببین چه اتفاقی می‌افتد.

دیدن جواب
Terminal window
printf '127.0.0.1 alpha.test beta.test gamma.test\n' >> /etc/hosts
for s in alpha beta; do
mkdir -p /var/www/$s.test
echo "<h1>$s</h1>" > /var/www/$s.test/index.html
printf 'server {\n listen 80;\n server_name %s.test;\n root /var/www/%s.test;\n}\n' $s $s > /etc/nginx/sites-available/$s.test
ln -s /etc/nginx/sites-available/$s.test /etc/nginx/sites-enabled/
done
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s http://alpha.test/
curl -s http://beta.test/
curl -s http://gamma.test/; echo "gamma.test -> curl exit code $?"
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
<h1>alpha</h1>
<h1>beta</h1>
gamma.test -> curl exit code 52

gamma.test به catch-all 00-default رسید که اتصال را بست (curl کد ۵۲). اگر catch-all نداشتی، یکی از سایت‌ها را نشان می‌داد. index را ننوشتیم چون پیش‌فرضش index.html است.

✎ تمرینمتوسط

برای alpha.test نسخه‌ی www.alpha.test را هم بپذیر ولی همیشه با 301 به نسخه‌ی بدون www (با حفظ مسیر و query string) بفرست. با curl -I ثابت کن هدر Location درست است و با curl -L ثابت کن در نهایت به صفحه‌ی درست می‌رسی.

دیدن جواب
Terminal window
echo '127.0.0.1 www.alpha.test' >> /etc/hosts
cat > /etc/nginx/sites-available/www.alpha.test <<'EOF'
server {
listen 80;
server_name www.alpha.test;
return 301 http://alpha.test$request_uri;
}
EOF
ln -s /etc/nginx/sites-available/www.alpha.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
curl -s -I 'http://www.alpha.test/products?page=2' | grep -iE '^(HTTP|location)'
curl -s -L 'http://www.alpha.test/'
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP/1.1 301 Moved Permanently
Location: http://alpha.test/products?page=2
<h1>alpha</h1>

return 301 سریع‌ترین شکل ریدایرکت است (بدون خواندن هیچ فایلی). $request_uri همان مسیر و query string اصلی است (/products?page=2). curl -L ریدایرکت را دنبال کرد و به صفحه‌ی alpha رسید. (در درس HTTPS همین الگو برای http ← https به کار می‌رود.)

✎ تمرینسخت

یک block بساز که هر زیردامنه‌ی sites.test (مثل ali.sites.test، sara.sites.test) را از پوشه‌ی هم‌نام در /var/www/sites/ سرو کند (ali.sites.test ← /var/www/sites/ali/)، با یک regex و گروه نام‌دار در server_name و استفاده از آن در root. زیردامنه‌ای که پوشه ندارد باید 404 بگیرد. با سه زیردامنه آزمایش کن.

دیدن جواب
Terminal window
printf '127.0.0.1 ali.sites.test sara.sites.test nobody.sites.test\n' >> /etc/hosts
for u in ali sara; do
mkdir -p /var/www/sites/$u
echo "<h1>home page of $u</h1>" > /var/www/sites/$u/index.html
done
cat > /etc/nginx/sites-available/sites.test <<'EOF'
server {
listen 80;
server_name ~^(?<user>[a-z0-9-]+)\.sites\.test$;
root /var/www/sites/$user;
}
EOF
ln -s /etc/nginx/sites-available/sites.test /etc/nginx/sites-enabled/
nginx -t 2>&1 | tail -n 1 && systemctl reload nginx
sleep 1
for h in ali sara nobody; do
printf '%-18s -> ' "$h.sites.test"
curl -s -o /tmp/body -w '%{http_code} ' "http://$h.sites.test/"
head -c 40 /tmp/body | tr -d '\r\n'; echo
done
خروجی
nginx: configuration file /etc/nginx/nginx.conf test is successful
ali.sites.test -> 200 <h1>home page of ali</h1>
sara.sites.test -> 200 <h1>home page of sara</h1>
nobody.sites.test -> 404 <html><head><title>404 Not Found</titl

regex با ~ شروع می‌شود و (?<user>...) یک متغیر نام‌دار می‌سازد که در root به کار رفته. regex را محدود نوشتیم ([a-z0-9-]+ و ^...$) تا کسی نتواند با یک Host عجیب (مثلاً شامل ../) مسیر را به جای دیگری ببرد. برای nobody پوشه‌ای نبود: 404. (این الگو برای میزبانی صفحه‌ی شخصی هر کاربر یا پیش‌نمایش هر شاخه‌ی Git کاربرد دارد.)

⚡ بررسی سریع

سه دامنه به یک IP اشاره می‌کنند و هر سه روی پورت ۸۰ می‌آیند. Nginx از کجا می‌فهمد هر درخواست مال کدام سایت است؟

؟ آزمونک
  1. درخواستی با Host ناشناخته به سروری می‌رسد که هیچ default_server ای ندارد. کدام server block جواب می‌دهد؟

  2. server_name *.shop.test با کدام Host جور نمی‌شود؟

  3. هر دو block دارای www.shop.test (دقیق) و *.shop.test هستند. درخواست www.shop.test به کدام می‌رسد؟

  4. return 444 در Nginx چه می‌کند؟

  5. سایتی با root /home/ali/site همیشه 403 می‌دهد و در error.log «Permission denied» هست. علت محتمل؟

  6. listen 127.0.0.1:8087; یعنی چه؟

  • هر سایت یک server block است: listen (پورت/IP)، server_name (دامنه‌ها)، root (پوشه) و index (فایل پیش‌فرض پوشه).
  • چند دامنه روی یک IP و پورت با هدر Host از هم جدا می‌شوند (name-based virtual hosting). برای آزمایش: /etc/hosts یا curl -H "Host: ..." یا curl --resolve.
  • ترتیب جور شدن server_name: دقیق ← *.x ← x.* ← regex (~، با گروه نام‌دار) ← default_server.
  • بدون default_server، اولین block (به ترتیب فایل‌ها) پیش‌فرض است؛ پس یک catch-all صریح بساز: listen 80 default_server; server_name _; return 444;. فقط یک default_server به ازای هر IP:پورت.
  • ریدایرکت www: return 301 http://example.com$request_uri;.
  • listen 127.0.0.1:PORT برای صفحه‌های فقط-داخلی.
  • root + آدرس = مسیر فایل؛ پوشه بدون / ← 301؛ پوشه بدون index ← 403؛ فایل‌ها باید برای www-data خواندنی باشند.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
server { listen 80; server_name shop.test; root /var/www/shop.test; index index.html; }یک سایت
listen 80 default_server; server_name _; return 444;catch-all برای دامنه‌های ناشناخته
server_name shop.test www.shop.test;چند اسم برای یک سایت
server_name *.shop.test;همه‌ی زیردامنه‌ها
server_name ~^(?<user>[a-z0-9-]+)\.sites\.test$;regex با گروه نام‌دار
return 301 http://shop.test$request_uri;ریدایرکت با حفظ مسیر
listen 127.0.0.1:8087;فقط داخلی
echo "127.0.0.1 shop.test" | sudo tee -a /etc/hostsدامنه‌ی محلی برای آزمایش
curl -H "Host: shop.test" http://127.0.0.1/آزمایش با هدر Host دلخواه
curl -sv http://shop.test/ 2>&1 | grep '^> Host'دیدن هدر Host فرستاده‌شده
add_header X-Debug-Server $server_name always;کدام block جواب داد؟