سرور وب salam serve: از یک پوشه تا چند دامنه
salam serve دو کار میکند: یک پوشه را برای توسعه روی HTTP سرو میکند، یا از یک پروندهی پیکربندی چند سایت واقعی را با میزبان مجازی، مسیر، هدایت، حافظهی نهان و پشتیبان PHP اجرا میکند. این درس هر دو حالت را کامل میگوید، بعد سراغ چند دامنه و چند زیردامنه میرود، اتصال PHP و پایتون و گو را نشان میدهد و در پایان HTTPS و استقرار تولیدی را جمع میکند.
- salam serve دقیقاً چیست؟
- حالت ساده: سرو کردن یک پوشه
- ساختن سایت با سلام و سرو کردنش
- پیکربندی: از یک سایت تا چند سایت
- مرجع تنظیمها
- قاعدههای مسیر و ترتیب انتخاب
- چند دامنه و چند سایت مختلف روی یک سرور
- چند زیردامنه به یک پروژهی یکسان
- اتصال به PHP با php-fpm
- پایتون، گو، نود و بقیهی زبانها
- اتصال به خود سلام و سازگاری
- HTTPS و قرار گرفتن پشت Caddy یا nginx
- خطاها، عیبیابی و بررسی پیش از استقرار
- استقرار تولیدی و چکلیست
- کدام راه را انتخاب کنم؟
salam serve دقیقاً چیست؟
سرور وب در خود سلام نوشته شده است: بستهی net/httpd یک سرور HTTP با انتخاب سایت از سرآیند Host، قاعدههای مسیر، هدایت، صفحهی خطا، حافظهی نهان، گزارش دسترسی و پشتیبان فستسیجیآی است. فرمان salam serve فقط یک پوستهی نازک دور همین بسته است؛ برای همین هر چیزی که در این درس میبینید از داخل یک برنامهی سلام هم قابل استفاده است.
دو حالت دارد که نباید با هم اشتباه شوند:
| حالت | فرمان | برای چه |
|---|---|---|
| سرو یک پوشه | salam serve [dir] | پیشنمایش سریع یک سایت ثابت، آزمایش خروجی salam web و salam page build، توسعهی محلی |
| چند سایت از پیکربندی | salam serve --config=web.salam | چند دامنه، چند زیردامنه، مسیرها، هدایت، حافظهی نهان، گزارش دسترسی، PHP و FastCGI |
| فقط بررسی پیکربندی | salam serve --config=web.salam --test | پیش از استقرار یا در CI؛ بدون باز کردن هیچ درگاهی |
حالت اول عمداً ساده است: نه TLS دارد، نه هدر ETag، نه بازهی بایت و نه قاعدهی مسیر. حالت دوم همان چیزی است که برای سایت واقعی میخواهید. اگر فقط یک پوشهی ثابت دارید و به هیچ قاعدهای نیاز ندارید، حالت اول کافی است؛ ولی همان را هم میتوانید با یک پیکربندی کوچک به حالت دوم ببرید و حافظهی نهان و ETag و بلاکشدن پروندههای نقطهای را بگیرید.
مرزها را از اول بدانید. در این نسخه salam serve نه TLS دارد، نه proxy_pass، نه CGI و نه سوکت یونیکس برای FastCGI. HTTPS را یک پایاندهندهی TLS مثل Caddy یا nginx انجام میدهد و برنامههای HTTP پایتون و گو و نود هم پشت همان قرار میگیرند. جزئیاتش در بخشهای HTTPS و پایتون و گو است.
حالت ساده: سرو کردن یک پوشه
بدون هیچ پروندهای، همین فرمان پوشهی جاری را روی http://127.0.0.1:8000/ سرو میکند:
salam serve
پوشه را هم میتوانید بدهید و درگاه و نشانی را عوض کنید:
salam serve dist
salam serve dist --port=8080
salam serve dist --host=0.0.0.0 --port=8080
پیشفرض 127.0.0.1:8000 است؛ یعنی فقط از همان رایانه دیده میشود. برای دیدن از گوشی یا شبکهی محلی --host=0.0.0.0 بدهید، اما بدانید این حالت پروندههای نقطهای را هم سرو میکند.
- برای هر پوشه
index.htmlرا میفرستد. اگر نبود، فهرست پوشه را به شکل HTML نشان میدهد. - فقط
index.htmlرا میشناسد؛ فهرستindexدلخواه و صفحهی ۴۰۴ سفارشی ندارد. - فایلها را در حافظه نگه میدارد و وقتی فایل عوض شود دوباره میخواند؛ برای پیشنمایش سریع خوب است.
- هدر
ETag،Last-Modified، پاسخ ۳۰۴ و بازهی بایت (Range) ندارد. اینها را حالت پیکربندی دارد. - پروندههای نقطهای مثل
.envرا سرو میکند. مسیرهای..را بیرون از پوشه نمیبرد، اما پوشهای که راز دارد را با این حالت روی شبکه نگذارید. - گزارش دسترسی به شکل
GET /path -> 200در خروجی چاپ میشود. - فایلها را دوباره نمیسازد و تغییرها را نمیبیند؛ بعد از هر تغییر دوباره بسازید و صفحه را تازه کنید.
ساختن سایت با سلام و سرو کردنش
صفحههای سلام به HTML ساخته میشوند. یک پروژهی صفحه با salam web به یک صفحهی کامل، و یک پوشه با salam page build به یک HTML برای هر صفحه تبدیل میشود. خروجی را در پوشهای مثل dist بگذارید و همان را سرو کنید:
mkdir -p dist
salam web --output=dist/index.html
salam serve dist --port=8080
برای سایت چندصفحهای، salam page build هر صفحه را به فایل HTML خودش مینویسد و فایلهای CSS و JS کنارش میآیند. اگر صفحهها را با --split ساخته باشید، خروجی جداگانهی CSS و JS هم دارید که میتوانید برایشان حافظهی نهان بگذارید. صفحههای ثابت همانطور که هستند کاملاند؛ برای بخشهای پویا به اتصال به خود سلام بروید.
خود سایت سلام هم برای پیشنمایش خروجی درست همین کار را میکند: salam serve website/dist/fa.
پیکربندی: از یک سایت تا چند سایت
پیکربندی یک پروندهی متنی با همان قالب بلوکی سلام است، اما برنامهی سلام نیست: داده است، نه کد. یک تنظیم name = value است، یک بلوک با name: باز و با end بسته میشود، توضیح با // یا /* ... */ میآید، و هر تنظیم و مقدارش در یک خط میماند. رشتهها میان دو نقلقولاند؛ برای مقداری که بکاسلش دارد، مثل عبارت باقاعده، از پشتنقلقول استفاده کنید: `~ \.php$`.
server:
log: access = "/var/log/salam/access.log" end
site:
listen = 80
names = "example.ir www.example.ir"
root = "/srv/example/public"
index = "index.html index.htm"
error page = 404 "404.html"
location "/assets/": cache = 30d end
location "= /health": return = 200 "ok" end
location "/old/": redirect = "https://www.example.ir$uri" code = 301 end
end
end
ریشهی همهچیز بلوک server: است. داخلش بلوک log: برای گزارش دسترسی و بلوکهای site: برای هر سایت میآید. هر سایت یک root دارد و میتواند چند بلوک location داشته باشد. بلوکها را میتوان یکخطی هم نوشت: location "= /health": return = 200 "ok" end.
سایت با سرآیند Host انتخاب میشود. نام دقیق بر الگوی ستارهدار میچربد، الگوی بلندتر بر الگوی کوتاهتر، و درخواستی که هیچ سایتی آن را ادعا نکرده به سایتی میرسد که default = true دارد، وگرنه به اولین سایت آن درگاه. هر سایت اگر listen نداشته باشد روی درگاه ۸۰ مینشیند؛ این نکته را در پیکربندی چندسایتی فراموش نکنید.
پیش از هر استقرار، پرونده را بررسی کنید:
salam serve --config=web.salam --test
خروجی موفق یک خط است: configuration is ok: 3 site(s), 2 listener(s) و کد خروج ۰. اگر ایرادی باشد، هر مشکل با file:line:column چاپ میشود و کد خروج ۱ است. اجرای واقعی با خطای پیکربندی شروع نمیشود و کد خروج ۲ میدهد. --test بدون --config هم خطا میدهد.
مرجع تنظیمها
این جدول همهی تنظیمهای پیکربندی است. ستون «کجا» میگوید تنظیم در سطح سایت، در سطح مسیر، یا هر دو معنا دارد.
| تنظیم | کجا | معنا |
|---|---|---|
listen = 80 | سایت | درگاه، بهدلخواه با نشانی: listen = "127.0.0.1:8080". میتوان تکرارش کرد تا یک سایت روی چند درگاه باشد. بدون آن، درگاه ۸۰. |
names = "a.ir www.a.ir" | سایت | نام میزبانها با فاصله. *.a.ir هر میزبانی را که به .a.ir ختم شود میگیرد، حتی چند سطح زیردامنه. |
default = true | سایت | سایت پیشفرض آن درگاه برای نامهایی که هیچ سایتی ادعا نکرده است. |
root = "/srv/site" | سایت و مسیر | پوشهای که مسیر درخواست به آن چسبانده میشود. root مسیر، جای root سایت را میگیرد. |
alias = "/srv/files" | فقط مسیر پیشوندی | جای پیشوند مطابق را میگیرد: با location "/dl/": alias = "/srv/files" end مسیر /dl/a.zip میشود /srv/files/a.zip. با = و ~ کار نمیکند. |
index = "index.html index.htm" | سایت و مسیر | فایلهایی که برای یک پوشه به ترتیب امتحان میشوند. پیشفرض index.html. |
return = 200 "ok" | سایت و مسیر | بیدرنگ با یک وضعیت و متن اختیاری پاسخ میدهد. برای health check و صفحههای نگهداری خوب است. |
redirect = "https://$host$request_uri" | سایت و مسیر | هدایت میکند؛ code کد را عوض میکند (پیشفرض ۳۰۱). متغیرها: $host، $uri، $args، $request_uri و $scheme. $scheme همیشه http است. |
code = 302 | سایت و مسیر | کد هدایت را از ۱۰۰ تا ۵۹۹ تعیین میکند. |
cache = 30d | سایت و مسیر | همراه فایلهای ایستا Cache-Control: public, max-age=... میفرستد. یکاها: s، m، h، d، w، y. |
header = "Name" "value" | سایت و مسیر | یک هدر پاسخ اضافه میکند؛ دقیقاً دو رشته. میشود تکرارش کرد. هدرهای سایت روی همهی پاسخها و هدرهای مسیر روی پاسخهای همان مسیر اعمال میشوند؛ اگر همنام باشند، هدری که بعدتر در پرونده آمده برنده است. |
error page = 404 "404.html" | سایت و مسیر | فایلی نسبت به root که با همان وضعیت خطا فرستاده میشود. کد از ۴۰۰ تا ۵۹۹. |
fastcgi_pass = "127.0.0.1:9000" | سایت و مسیر | درخواستهای منطبق را به یک پشتیبان FastCGI مثل php-fpm میسپارد. فقط نشانی TCP؛ سوکت یونیکس پشتیبانی نمیشود. |
fastcgi_index = "index.php" | سایت و مسیر | اسکریپتی که برای مسیر تمامشده با / استفاده میشود. پیشفرض index.php. |
fastcgi_timeout = 30s | سایت و مسیر | سقف زمان یک درخواست تا پشتیبان؛ پیشفرض ۳۰ ثانیه. کل تبادل را میپوشاند. |
fastcgi_param = "NAME" "value" | سایت و مسیر | یک متغیر CGI اضافه یا عوض میکند؛ میشود تکرارش کرد. متغیرهای $uri و $header:Name را میشناسد. |
log: access = "..." | سطح سرور | گزارش دسترسی در یک فایل، یا "stdout" و "stderr". اگر باز نشود سرور بالا نمیآید. |
قاعدههای مسیر و ترتیب انتخاب
داخل هر سایت، مسیر درخواست مثل nginx با بلوکهای location مقایسه میشود. چهار شکل دارید:
location "= /health": فقط همین مسیر، بدون آزمایش چیز دیگر. سریعترین و قطعیترین.location "/assets/": هر مسیری که با این پیشوند شروع شود؛ بلندترین پیشوند برنده است.location "^~ /static/": پیشوندی که اگر برنده شود، جلوی آزمایش عبارتهای باقاعده را میگیرد.location "~ \.php$"و"~* \.png$": عبارت باقاعده؛ دومی به بزرگی و کوچکی حروف حساس نیست. به ترتیب پرونده آزمایش میشوند و اولین مطابقت برنده است.
ترتیب کلی این است: اول تطابق دقیق؛ بعد بلندترین پیشوند؛ اگر آن پیشوند ^~ بود همینجا تمام؛ وگرنه عبارتهای باقاعده به ترتیب پرونده؛ و اگر هیچکدام نخورد، همان بلندترین پیشوند. مسیر پیش از مقایسه URL-decode و پاکسازی میشود؛ .. نمیتواند از root بالا برود و مسیری با %00 پاسخ ۴۰۰ میگیرد.
چند رفتار که در عمل مهماند:
- فقط فایلهای ایستا به
GETوHEADپاسخ میدهند؛ روش دیگر ۴۰۵ با هدرAllowمیگیرد. مسیر FastCGI به همهی روشها پاسخ میدهد، چون تصمیمش با برنامه است. - فایلها با
ETagوLast-Modifiedمیروند؛ درخواست تازه ۳۰۴ بدون بدنه میگیرد وRangeبا ۲۰۶ همان بخش را. بازهی بیرون از فایل ۴۱۶ است. چند بازه در یک درخواست با کل فایل پاسخ داده میشود. - پوشهای که با
/نیامده باشد با ۳۰۱ به شکل اسلشدار هدایت میشود. - هر بخشی از مسیر که با نقطه شروع شود، مثل
.envیا.git، ۴۰۴ میگیرد؛ تنها استثنا.well-knownاست، برای ACME و تأیید دامنه. - برای فایل، مسیر همیشه نسبت به
rootمؤثر خوانده میشود؛aliasفقط پیشوند مسیر را با یک پوشه عوض میکند.
چند دامنه و چند سایت مختلف روی یک سرور
همهی سایتها در یک پرونده میآیند و یک فرایند salam serve همه را اجرا میکند. هر سایت نامها، ریشه و قاعدههای خودش را دارد:
server:
log: access = "/var/log/salam/access.log" end
site:
listen = 80
names = "example.ir www.example.ir"
root = "/srv/example/public"
location "/assets/": cache = 30d end
location "= /health": return = 200 "ok" end
end
site:
listen = 80
names = "shop.example.ir"
root = "/srv/shop/public"
location "/media/": cache = 7d end
end
site:
listen = 80
names = "blog.example.ir"
root = "/srv/blog"
default = true
end
end
نکتههای این پیکربندی:
- هر سه سایت روی درگاه ۸۰ هستند، ولی یک شنونده ساخته میشود؛ انتخاب با
Hostاست. درگاههای متفاوت شنوندهی جدا میسازند. - دو نام یک سایت را میتوانید در یک رشته بیاورید:
"example.ir www.example.ir". DNS هر دو باید به همین سرور برسد. - سایت سوم
default = trueاست، پس درخواست با نام ناشناخته به آن میرسد. این الگوی خوبی برای صفحهی «سایت پیدا نشد» یا سایت اصلی است. بیش از یک default روی یک درگاه نگذارید. - یک نام تکراری روی یک درگاه خطای پیکربندی است:
name 'x' is used by two sites on port 80. اگر همان نام را روی درگاه دیگر بخواهید، اشکالی ندارد.
اگر چند دامنهی کاملاً متفاوت باید یک پروژه و یک ریشهی یکسان را نشان دهند، همه را در names یک سایت بیاورید: names = "a.ir www.a.ir b.ir c.ir". DNS همه باید به همین سرور برسد و محتوا یکی است. اگر هر دامنه محتوای خودش را میخواهد، برای هر کدام سایت جدا بسازید.
هدایت دامنهی بدون www به www
سطح سایت، redirect روی همهی درخواستهای آن سایت اجرا میشود؛ پس دامنهی بدون www را یک سایت جدا بدهید و سایت اصلی را جدا:
site:
listen = 80
names = "example.ir"
redirect = "https://www.example.ir$request_uri"
end
site:
listen = 80
names = "www.example.ir"
root = "/srv/example/public"
end
سایت هدایتگر به root نیاز ندارد، چون سطح سایت پیش از هر مسیری هدایت میکند. چون TLS را یک پراکسی جلوتر انجام میدهد، مقصد را صریح https:// نوشتهایم؛ $scheme در سلام همیشه http است. برای آزمایش محلی، Host را دستی بدهید:
curl -H "Host: shop.example.ir" http://127.0.0.1:8080/
روی رایانهی خودتان میتوانید این نامها را در /etc/hosts (یا فایل hosts ویندوز) به 127.0.0.1 بدهید و مرورگر را باز کنید.
چند زیردامنه به یک پروژهی یکسان
اگر همهی زیردامنهها همان کد و همان ریشه را دارند، یک سایت با همهی نامها بسازید:
site:
listen = 80
names = "app.example.ir *.app.example.ir"
root = "/srv/app/public"
end
الگوی *.app.example.ir به هر میزبانی میخورد که به .app.example.ir ختم شود؛ حتی a.b.app.example.ir. اگر فقط یک سطح زیردامنه میخواهید، نامها را یکییکی بنویسید یا در برنامه بررسی کنید. DNS هم باید *.app.example.ir را به سرور بدهد.
حالا برنامه با سرآیند Host میفهمد کاربر کدام زیردامنه را باز کرده است. برای سایت ثابت، همان محتوا در همه میآید؛ اگر محتوای هر زیردامنه فرق دارد، دو راه دارید:
- یک ریشه، محتوای پویا: برنامهی سلام (یا PHP) از
Hostاستفاده میکند و محتوای همان زیردامنه را میسازد. برای این کار یک سایت با نامهای ستارهدار کافی است. - ریشهی جدا برای هر کدام: برای هر زیردامنه یک بلوک
site:جدا باrootخودش بسازید. نام دقیق بر ستاره میچربد، پس میتوانید یک سایت ستارهدار برای بقیه داشته باشید و یکی دو زیردامنهی خاص را جدا کنید:
site:
listen = 80
names = "admin.app.example.ir"
root = "/srv/admin/public"
end
site:
listen = 80
names = "*.app.example.ir"
root = "/srv/app/public"
default = true
end
در این مثال admin ریشهی خودش را دارد و بقیهی زیردامنهها از پروژهی اصلی میآیند. برای گواهی TLS این چند نام، یا گواهی wildcard بگیرید یا در Caddy هر نام را جدا تعریف کنید؛ salam serve خودش گواهی نمیگیرد.
اتصال به PHP با php-fpm
PHP از طریق FastCGI به سلام وصل میشود. اول php-fpm را روی یک درگاه TCP بشنوید، چون سوکت یونیکس پشتیبانی نمیشود. در فایل pool، مثلاً /etc/php/8.3/fpm/pool.d/www.conf:
listen = 127.0.0.1:9000
; اگر فقط از همین سرور وصل میشوید:
listen.allowed_clients = 127.0.0.1
بعد php-fpm را دوباره راه بیندازید. حالا یک سایت PHP با فایلهای واقعی:
server:
site:
listen = 80
names = "php.example.ir www.php.example.ir"
root = "/srv/php/public"
index = "index.html"
location "= /":
fastcgi_pass = "127.0.0.1:9000"
fastcgi_index = "index.php"
fastcgi_timeout = 30s
fastcgi_param = "APP_ENV" "production"
end
location "~ \.php(/|$)":
fastcgi_pass = "127.0.0.1:9000"
fastcgi_timeout = 30s
fastcgi_param = "APP_ENV" "production"
end
location "/assets/":
cache = 30d
end
end
end
مسیر / با تطابق دقیق به php-fpm میرود و فایل index.php را اجرا میکند. عبارت ~ \.php(/|$) هر مسیری را که به .php ختم شود یا بعد از آن بخش اضافه داشته باشد میگیرد، پس /index.php/a/b هم با PATH_INFO=/a/b اجرا میشود و هم فایل PHP بهصورت متن خام بیرون نمیرود. بقیهی فایلهای ایستا از root خوانده میشوند.
هرگز فایل PHP را در فهرست index نگذارید. اگر index = "index.php" بدهید و مسیری به FastCGI نرود، سلام همان فایل را بهعنوان فایل ایستا میفرستد و کد PHP لو میرود. فهرست index را روی index.html بگذارید، مسیر / را با تطابق دقیق به FastCGI بدهید و فقط فایلهای ورودی PHP را زیر root نگه دارید.
سلام این متغیرهای CGI را خودش پر میکند: GATEWAY_INTERFACE، SERVER_SOFTWARE، SERVER_PROTOCOL، REQUEST_METHOD، REQUEST_URI، SCRIPT_NAME، SCRIPT_FILENAME، PATH_INFO، DOCUMENT_ROOT، DOCUMENT_URI، QUERY_STRING، CONTENT_TYPE، CONTENT_LENGTH، SERVER_NAME، SERVER_PORT، REMOTE_ADDR، REDIRECT_STATUS و HTTP_* برای هدرهای رایج. fastcgi_param یکی دیگر را اضافه یا عوض میکند و $uri، $request_uri، $args، $document_root، $script_name، $server_port، $remote_addr و $header:Name را میفهمد.
محدودیت مهم برای فریمورکها. سلام برای FastCGI دنبال یک فایل واقعی زیر root میگردد؛ مسیری که به هیچ فایلی نرسد، پیش از رسیدن به PHP پاسخ ۴۰۴ میگیرد. یعنی /index.php، /index.php/a/b و فایلهای واقعی کار میکنند، اما آدرس زیبای /login در Laravel یا وردپرس با پیوند یکتا بدون یک پراکسی جلوتر کار نمیکند. اگر پیوند یکتای بدون index.php میخواهید، Caddy یا nginx را جلو بگذارید و بازنویسی را آنجا انجام دهید؛ یا فعلاً آدرسها را با index.php بدهید. گزینهی سوم، نگهداشتن پیوند سادهی ?p=123 در وردپرس است.
پاسخ همان چیزی است که PHP میفرستد: خط Status: وضعیت را تعیین میکند، بقیهی هدرها همانطور بیرون میروند (چند Set-Cookie هم) و بدنه بایتبهبایت رد میشود. اگر پشتیبان در دسترس نباشد یا نیمهکار بمیرد، پاسخ ۵۰۲ است. هر درخواست اتصال خودش را باز میکند و REMOTE_ADDR بهطور پیشفرض 127.0.0.1 است؛ پشت پراکسی آن را با fastcgi_param = "REMOTE_ADDR" "$header:X-Real-IP" عوض کنید و به برنامه بگویید TLS در جلو تمام شده است.
پایتون، گو، نود و بقیهی زبانها
اینجا باید صریح باشم: salam serve پراکسی معکوس HTTP ندارد و فقط فایل ایستا و FastCGI میفهمد. پس برای پایتون و گو و نود دو راه واقعی دارید.
راه اول و پیشنهادی: یک پراکسی معکوس جلوتر
برنامهی پایتون یا گو را روی یک پورت محلی اجرا کنید و Caddy را جلو بگذارید. اینگونه TLS و WebSocket و بازنویسی مسیر هم یکجا حل میشود. نمونهی Caddyfile وقتی سلام هم بخشی از سایت را سرو میکند:
example.ir {
encode gzip
handle /api/* {
reverse_proxy 127.0.0.1:8001
}
handle {
reverse_proxy 127.0.0.1:8080
}
}
اینجا 8001 برنامهی پایتون یا گو و 8080 همان salam serve است. اگر بخش سلام را ندارید، فقط همان reverse_proxy به برنامه را نگه دارید. Caddy گواهی Let's Encrypt را خودش میگیرد و Host را دستنخورده رد میکند.
راه دوم: FastCGI با یک فایل ورودی
اگر برنامهتان FastCGI حرف میزند، میتوانید مستقیم وصلش کنید. ترفند کار این است که یک فایل خالی به نام نقطهی اتصال زیر root بگذارید تا سلام آن را «اسکریپت» ببیند و بقیهی مسیر را به PATH_INFO بدهد. مثلاً فایل خالی /srv/app/api و این مسیر:
location "/api":
fastcgi_pass = "127.0.0.1:9100"
fastcgi_timeout = 60s
end
حالا /api/users به پشتیبان میرسد: SCRIPT_NAME=/api، PATH_INFO=/users. یک استثنای ریز: چون fastcgi_index برای PHP طراحی شده، درخواست دقیقاً /api/ به /api/index.php تبدیل میشود؛ از /api یا /api/... استفاده کنید. برای پایتون، uWSGI میتواند همین پروتکل را روی TCP حرف بزند:
pip install uwsgi
uwsgi --fastcgi-socket 127.0.0.1:9100 \
--wsgi-file /srv/app/app.py \
--callable app \
--processes 4
و کوچکترین برنامهی WSGI که مسیر را برمیگرداند:
def app(environ, start_response):
path = environ.get("PATH_INFO", "/")
body = ("hello from python: " + path + "\n").encode()
start_response("200 OK", [
("Content-Type", "text/plain; charset=utf-8"),
("Content-Length", str(len(body))),
])
return [body]
برای گو، بستهی net/http/fcgi یک سرور FastCGI میدهد. توجه کنید که این بسته در نسخههای تازهی گو «منسوخ» علامت خورده اما هنوز کار میکند:
package main
import (
"fmt"
"net"
"net/http"
"net/http/fcgi"
)
func main() {
ln, err := net.Listen("tcp", "127.0.0.1:9100")
if err != nil {
panic(err)
}
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello from go: %s\n", r.URL.Path)
})
if err := fcgi.Serve(ln, h); err != nil {
panic(err)
}
}
برای نود هم اگر بستهای مثل node-fastcgi دارید همین الگو جواب میدهد؛ وگرنه راه اول را بردارید. محدودیتها را جدی بگیرید: سلام مسیر را با فایل ورودی تطبیق میدهد، هر درخواست یک اتصال تازه باز میکند، سوکت یونیکس ندارد و همهی درخواستهای آن مسیر به یک پشتیبان میروند. برای نرمالترین استقرار، پراکسی جلوتر بهتر است.
اتصال به خود سلام و سازگاری
سازگاری سلام با salam serve از دو جهت است. اول اینکه پیکربندی همان قالب بلوکی سلام است و با تجزیهگر net/httpd/conf خوانده میشود: بلوکها با end (یا پایان) بسته میشوند، توضیحها همان // هستند و خطاها با شمارهی خط و ستون گزارش میشوند. دوم اینکه خود سرور یک بستهی کتابخانهی استاندارد است، پس میتوانید آن را داخل برنامهی سلام راه بیندازید:
واردسازی شبکه.سرور وب
تابع ریشه:
اگر سرور وب.بررسی("web.salam") != ۰:
برگشت ۱
پایان
اگر وارونه سرور وب.بارگذاری پرونده("web.salam"):
برگشت ۲
پایان
برگشت سرور وب.اجرا()
پایاناین برنامه دقیقاً کاری را میکند که salam serve --config=web.salam میکند: پرونده را بررسی و بار میکند و اجرا را شروع میکند. برای تست پیکربندی بدون اجرا، httpd.Check (یا سرور وب.بررسی) همان --test است؛ برای پرسیدن اینکه یک میزبان یا مسیر به کدام سایت میرسد، SiteFor، LocationFor، SiteCount و SiteNames را دارید و هیچ درگاهی باز نمیشود.
سه الگوی رایج:
- سایت ثابت ساختهشده با سلام:
salam webیاsalam page build، بعدsalam serve dist. این سادهترین و سریعترین حالت است. - برنامهی پویا با
net/http: برنامهی سلام خودش سرور را روی یک پورت راه میاندازد و میتواند فایلهای ایستا را هم باrouter.Staticبدهد. این برنامه را مستقیم اجرا کنید یا پشت Caddy بگذارید؛ salam serve آن را در خودش mount نمیکند چون پراکسی معکوس ندارد. - ترکیب ثابت و PHP: salam serve هم فایل ایستا میدهد هم PHP را به php-fpm میسپارد؛ اگر برنامهی پایتون یا گو هم دارید، Caddy بخش
/apiرا به آن و بقیه را به salam میدهد.
یک تفاوت ظریف: کلیدهای پیکربندی انگلیسی میمانند، مثل کلیدهای JSON، حتی اگر API فارسی باشد. نام تابعها و نوعهای net.httpd نام فارسی هم دارند (بررسی، بارگذاری پرونده، اجرا، توقف)، ولی پیکربندی داده است و با کلید ثابت خوانده میشود.
HTTPS و قرار گرفتن پشت Caddy یا nginx
در این نسخه listen = 443 tls خوانده میشود اما با پیام tls is not available yet رد میشود. پس TLS را به یک پایاندهنده بسپارید. کوتاهترین راه Caddy است:
example.ir, www.example.ir {
reverse_proxy 127.0.0.1:8080
}
این سه کار را با هم میکند: گواهی را میگیرد و تمدید میکند، HTTP را به HTTPS هدایت میکند و درخواست را با Host درست به سلام میدهد. اگر nginx را ترجیح میدهید، همان proxy_pass http://127.0.0.1:8080; و proxy_set_header Host $host; کافی است.
پشت پراکسی دو نکته را تنظیم کنید. برای PHP، REMOTE_ADDR را از هدر پراکسی بگیرید و به برنامه بگویید TLS تمام شده است:
fastcgi_param = "REMOTE_ADDR" "$header:X-Real-IP"
fastcgi_param = "HTTPS" "on"
fastcgi_param = "HTTP_X_FORWARDED_PROTO" "https"
برای فایلهای ایستا، salam گزارش دسترسی را با میزبان و مسیر مینویسد اما نشانی طرف مقابل را ندارد؛ اگر به آن نیاز دارید، لاگ Caddy یا nginx را نگه دارید. و یادتان باشد هدایت سطح سایت با $scheme همیشه http میسازد، پس مقصد https را صریح بنویسید.
خطاها، عیبیابی و بررسی پیش از استقرار
هر چیزی که ممکن است در پرونده غلط باشد هنگام بارکردن پیدا میشود، نه در اولین درخواست. نمونهی خروجی --test برای یک پروندهی خراب:
web.salam:3:18: port must be a number from 1 to 65535
web.salam:6:9: header needs two strings: the name and the value
web.salam:7:9: '[' is not a valid regular expression
web.salam:8:13: unknown setting 'returx' in a location (did you mean 'return'?)
web.salam:2:5: root '/srv/does-not-exist' is not a directory
configuration is not valid
| پیام یا نشانه | معنا و راهحل |
|---|---|
root '...' is not a directory | پوشه هنگام بارکردن باید وجود داشته باشد. مسیر را درست کنید یا اول بسازید. |
name '...' is used by two sites on port ... | دو سایت یک نام روی یک درگاه دارند. یکی را عوض کنید یا درگاه را جدا کنید. |
site number N has no names and is not the default site | هر سایت یا باید names داشته باشد یا default = true باشد. |
a location path must start with '/' | مسیر پیشوندی باید با اسلش شروع شود. برای عبارت باقاعده از ~ استفاده کنید. |
fastcgi_pass cannot read a unix socket yet | php-fpm را روی TCP بگذارید: listen = 127.0.0.1:9000. |
tls is not available yet | در سلام TLS نگذارید؛ Caddy یا nginx را جلو بگذارید. |
| پاسخ ۵۰۲ | پشتیبان FastCGI در دسترس نیست، نشانی اشتباه است یا پشتیبان نیمهکار مرده است. اتصال TCP و لاگ php-fpm را ببینید. |
| پاسخ ۴۰۴ برای مسیر PHP بدون پسوند | سلام فایل اسکریپت را پیدا نکرده است. آدرس را با index.php بدهید یا پراکسی جلوتر با بازنویسی بگذارید. |
| پاسخ ۴۰۵ | روش غیر از GET و HEAD روی فایل ایستا. Allow را ببینید؛ برای API از FastCGI یا برنامهی سلام استفاده کنید. |
پاسخ ۴۰۴ برای .env و پروندههای نقطهای | حالت پیکربندی آنها را میبندد؛ تنها استثنا .well-known است. رازها را بیرون از root بگذارید. |
چند نکتهی عیبیابی که وقت زیاد میگیرند:
--testفقط پیکربندی و وجودrootوaliasرا بررسی میکند؛ سالم بودن php-fpm، وجود اسکریپت برای هر درخواست و باز بودن درگاه را نمیسنجد.- اگر سرور بالا نمیآید و پیام
cannot listen on ...است، درگاه اشغال است. باlsof -i :80یاss -ltnpببینید چه چیزی آن را گرفته است. - اگر پیکربندی را عوض کردید، سرور در حال اجرا سایتهای قبلی را نگه میدارد؛ دوباره راه بیندازید.
- نام میزبان به حروف کوچک تبدیل میشود و درگاه و نقطهی پایانی از آن حذف میشوند؛
WWW.Example.IR.همانwww.example.irاست. - لاگ دسترسی فقط زمان، میزبان، روش، مسیر و وضعیت را دارد؛ IP و زمان پاسخ در آن نیست. اگر اینها را میخواهید، لاگ پراکسی جلوتر را نگه دارید.
استقرار تولیدی و چکلیست
برای یک سرور لینوکسی، سرور را با systemd بالا نگه دارید. یک واحد ساده:
[Unit]
Description=Salam web server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/srv
ExecStart=/usr/local/bin/salam serve --config=/srv/web.salam
Restart=on-failure
RestartSec=2
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now salam-web
sudo systemctl status salam-web
sudo journalctl -u salam-web -f
اگر درگاه ۸۰ را به کاربر غیرریشه میدهید، AmbientCapabilities بالا کافی است؛ یا سرور را روی 127.0.0.1:8080 بگذارید و Caddy را روی ۸۰ و ۴۴۳. کاربر سرویس باید به rootها و مسیر لاگ دسترسی خواندن (و اگر برنامه مینویسد، نوشتن) داشته باشد.
چکلیست کوتاه پیش از انتشار:
salam serve --config=web.salam --testرا در CI یا پیش از هر استقرار اجرا کنید؛ کد خروج باید ۰ باشد.- پروندهی راز،
.env،.gitو نسخههای پشتیبان را بیرون ازrootبگذارید. حالت پیکربندی پروندههای نقطهای را میبندد، اما بهترین کار این است که اصلاً آنجا نباشند. - برای داراییهای نسخهدار (CSS و JS با هش)
cache = 30dیا بیشتر بگذارید؛ برای HTML حافظهی نهان نگذارید. - یک
location "= /health": return = 200 "ok" endبرای probe بگذارید. - گزارش دسترسی را با
logrotateبچرخانید؛ سلام خودش چرخش ندارد. - HTTPS را Caddy یا nginx انجام دهد و
X-Forwarded-Protoرا به برنامه برساند. - هر سایت یک
defaultمعنادار داشته باشد؛ سایت پیشفرض بیصاحب باعث میشود دامنهی اشتباه هم پاسخ ۲۰۰ بگیرد. - اگر FastCGI دارید، سقف
fastcgi_timeoutرا با زمان واقعی پاسخ برنامه تنظیم کنید و به یاد داشته باشید هر درخواست یک اتصال تازه میسازد.
کدام راه را انتخاب کنم؟
| نیاز | راه پیشنهادی |
|---|---|
| پیشنمایش سریع یک پوشه در توسعه | salam serve dir |
| سایت ثابت واقعی با حافظهی نهان و صفحهی ۴۰۴ | حالت پیکربندی با یک site: و cache |
| چند دامنه روی یک سرور | یک پرونده با چند site: و names جدا |
| چند زیردامنه روی یک پروژه | یک site: با نامهای ستارهدار و یک root؛ زیردامنههای خاص را با سایت دقیق جدا کنید |
| PHP | fastcgi_pass به php-fpm روی TCP |
| پایتون، گو، نود | Caddy یا nginx بهعنوان پراکسی معکوس؛ FastCGI فقط اگر برنامه پشتیبانی کند |
| برنامهی پویا با خود سلام | برنامهی net/http را مستقیم اجرا کنید یا پشت Caddy بگذارید |
| HTTPS | Caddy یا nginx؛ salam serve خودش گواهی نمیگیرد |
| WebSocket | در برنامهی سلام با net/websocket؛ salam serve پراکسی WebSocket نیست |
در پایان، salam serve یک سرور کوچک و خوانا برای سایتهای ثابت و FastCGI است، نه جایگزین کامل nginx. برای یک سایت ثابت یا چند دامنهی ثابت با PHP، همهچیز در یک فرمان و یک پروندهی متنی جمع میشود. برای برنامههای HTTP دیگر و TLS، یک پراکسی ساده جلوتر بگذارید و بگذارید هر ابزار کار خودش را بکند.