Վեբ սերվերի stack-ը՝ nginx, PHP-FPM և Node
Ինչպես են nginx-ը, Apache-ն, PHP-FPM-ը և Node հավելվածները համատեղվում իրական սերվերի վրա։
30 վայրկյանում
Վեբ սերվերը քո սերվերի մուտքի դուռն է․ այն ընդունում է յուրաքանչյուր HTTP request, ֆայլերը սպասարկում է ուղիղ և դինամիկ request-ները փոխանցում է քո հավելվածին։ Այսօր այդ դուռը սովորաբար nginx-ն է, որը PHP-ի հետ խոսում է PHP-FPM-ի միջոցով, իսկ Node հավելվածների հետ՝ լոկալ port-ով։ Երբ հասկանում ես server block-երը, location-ները և reverse proxying-ը, կարող ես մեկ մեքենայի վրա կողք կողքի host անել Laravel API և Next.js frontend և deploy անել դրանք առանց downtime-ի։
Ինչու եմ օգտագործում
- nginx-ը դնում եմ գրեթե այն ամենի առջևում, ինչ գործարկում եմ, որովհետև այն արագ է սպասարկում static ֆայլերը, terminate է անում HTTPS-ը և շատ քիչ հիշողությամբ proxy է անում Laravel և Next.js հավելվածներին։
- Սերվերների վրա իմ հետապնդած production bug-երի մեծ մասն ավարտվել է վեբ սերվերի կամ PHP-FPM-ի log-ում, ուստի այս շերտը լավ իմանալն է, որ թույլ է տալիս խնդիրները արագ շտկել՝ գուշակելու փոխարեն։
- Աշխատել եմ և՛ hosting պանելներով, և՛ մաքուր սերվերներով, և հենց ներքևի stack-ը հասկանալն է, որ դրանցից ցանկացածը անվտանգ է դարձնում շահագործման համար։
- Մաքուր nginx config-ը նաև փաստաթուղթ է․ ցանկացածը կարող է կարդալ, թե որ domain-ը որ հավելվածին է գնում, ինչն է cache արվում և ինչն է արգելափակված։
Ինչպես է աշխատում
01Ինչ է անում վեբ սերվերը, և ինչպես է դա անում nginx-ը
Պատկերացրու զբաղված ռեստորան։ Դռան մոտ կանգնած host-ը դիմավորում է յուրաքանչյուր հյուրի, մենյու է տալիս (static ֆայլեր) և իրական պատվերներն ուղարկում է խոհանոց (քո հավելվածը)։ Վեբ սերվերը հենց այդ host-ն է։ Այն լսում է 80 և 443 port-երով, parse է անում HTTP request-ը, որոշում է, թե ով պետք է պատասխանի դրան, և պատասխանը հետ է ուղարկում։ nginx-ը event-driven է․ փոքր թվով worker պրոցեսներ (սովորաբար մեկը յուրաքանչյուր CPU core-ի համար) յուրաքանչյուրը մշակում է հազարավոր միացումներ non-blocking I/O-ով, ուստի պարապ կամ դանդաղ client-ը գրեթե ոչինչ չարժե։ Apache-ն ավանդաբար օգտագործում էր մեկ պրոցես կամ thread յուրաքանչյուր միացման համար (prefork/worker MPM-ներ)․ նրա ժամանակակից event MPM-ը կրճատեց տարբերությունը, և Apache-ն դեռ փայլում է per-directory .htaccess ֆայլերով և մոդուլների հսկայական էկոհամակարգով։ Գործնականում շատ stack-եր, այդ թվում hosting պանելների մեծ մասը, nginx-ը դնում են առջևում static ֆայլերի և TLS-ի համար, իսկ Apache-ն պահում են դրա հետևում՝ համատեղելիության համար։
- Master պրոցես՝ կարդում է config-ը, բացում է port-երը, կառավարում է worker-ները։
- Worker պրոցեսներ՝ մշակում են միացումները․ worker_processes auto-ն համապատասխանեցնում է CPU core-երին։
- Apache-ի .htaccess-ը հարմար է, բայց կարդացվում է յուրաքանչյուր request-ի ժամանակ․ nginx-ը համարժեք չունի, և դա միտումնավոր է։
02Server block-եր և location-ներ
nginx config-ը ծառ է։ http context-ը պահում է գլոբալ կարգավորումները։ Դրա ներսում յուրաքանչյուր server block մեկ virtual host է, որն ընտրվում է ըստ port-ի և Host header-ի (server_name)։ Server-ի ներսում location block-երը համապատասխանեցնում են URL-ի ճանապարհը և որոշում, թե ինչ տեղի ունենա՝ սպասարկել ֆայլ, փոխանցել PHP-ին, proxy անել Node-ին, թե մերժել։ Location-ների համընկնումն ունի կանոններ, որոնք արժե անգիր անել․ ճշգրիտ համընկնումները (=) հաղթում են առաջինը, հետո ամենաերկար prefix համընկնումը, եթե միայն regular-expression location-ը (~) նույնպես չի համընկնում, որի դեպքում հաղթում է առաջին համընկնող regex-ը (^~ prefix-ը կանգնեցնում է regex-ի ստուգումները)։ try_files-ը հիմնական աշխատող գործիքն է․ այն ստուգում է իրական ֆայլը, հետո դիրեկտորիան, հետո fallback է անում հավելվածի entry point-ին։ Ubuntu-ում կայքերը գտնվում են /etc/nginx/sites-available-ում և միացվում են sites-enabled-ում symlink-ով։
server {
listen 80;
server_name static.example.com;
root /var/www/static/public;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location ~* \.(css|js|svg|png|jpg|webp|woff2)$ {
expires 30d;
access_log off;
}
location ~ /\.(?!well-known) {
deny all;
}
}
03PHP-FPM՝ ինչպես է nginx-ը գործարկում PHP
nginx-ը ինքը չի կարող PHP կատարել։ Այն request-ը FastCGI պրոտոկոլով փոխանցում է PHP-FPM-ին (FastCGI Process Manager)՝ առանձին ծառայության, որը պատրաստ պահում է PHP worker պրոցեսների pool։ Միացումը սովորաբար Unix socket է, օրինակ՝ /run/php/php8.5-fpm.sock (PHP 8.5-ը լռելյայն է Ubuntu 26.04-ում, 24.04-ը գալիս է 8.3-ով)։ Pool-ը worker-ների խումբ է՝ իր օգտատիրոջով, socket-ով և սահմանափակումներով։ Յուրաքանչյուր հավելվածի տուր իր pool-ը, որն աշխատում է իր system user-ով․ այդ դեպքում մեկ կոտրված կայքը չի կարող կարդալ մեկ այլ կայքի ֆայլերը, և մեկ դանդաղ հավելվածը չի կարող խժռել բոլոր worker-ները։ pm = dynamic-ը worker-ները scale է անում սահմանների միջև․ pm.max_children-ը կոշտ սահմանն է, ուստի չափավորիր այն մոտավորապես որպես (RAM PHP-ի համար) բաժանած (worker-ի միջին հիշողությանը, Laravel-ի համար հաճախ 40-ից 80 MB)։ Երբ բոլոր child-երը զբաղված են, request-ները հերթ են կանգնում, և տեսնում ես 502/504 սխալներ և «server reached pm.max_children» FPM log-ում։
; /etc/php/8.5/fpm/pool.d/api.conf
[api]
user = api
group = api
listen = /run/php/php8.5-fpm-api.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
request_terminate_timeout = 60s
04Laravel API nginx-ի հետևում
Laravel-ն ունի մեկ entry point՝ public/index.php, և document root-ը պետք է ուղղված լինի public թղթապանակին, որպեսզի .env-ը, storage-ը և vendor-ը երբեք հասանելի չլինեն վեբից։ try_files-ը նախ սպասարկում է իրական ֆայլերը, իսկ մնացած ամեն ինչ ուղարկում է index.php՝ query string-ը անփոփոխ։ $document_root-ի փոխարեն $realpath_root-ի օգտագործումը կարևոր է, երբ deploy ես անում «current» symlink-ով․ այդ դեպքում PHP-ն տեսնում է resolve արված release ճանապարհը, ուստի OPcache-ը չի շարունակում սպասարկել հին release-ը symlink-ի փոխարկումից հետո։ Ստորև certificate-ների ճանապարհներն այն են, ինչ certbot-ը ստեղծում է Let's Encrypt-ի համար։ http2 on-ը HTTP/2 միացնելու ժամանակակից ձևն է․ հին «listen 443 ssl http2» ձևը deprecated է nginx 1.25.1-ից ի վեր։
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
root /var/www/api/current/public;
index index.php;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
client_max_body_size 20m;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.5-fpm-api.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
}
location ~ /\.(?!well-known) { deny all; }
}
05Next.js frontend reverse proxy-ի միջոցով
Node հավելվածը (Next.js, NestJS, Express) իր սեփական HTTP սերվերն է, որը լսում է լոկալ port-ով, օրինակ՝ 127.0.0.1:3000։ nginx-ը գործում է որպես reverse proxy․ այն ընդունում է public HTTPS միացումը և request-ը փոխանցում այդ port-ին՝ ավելացնելով header-ներ, որպեսզի հավելվածն իմանա client-ի իրական IP-ն և պրոտոկոլը։ Node հավելվածը bind արա 127.0.0.1-ին, որպեսզի այն երբեք ուղիղ հասանելի չլինի։ Պրոցեսը կենդանի պահիր pm2-ով (pm2 start npm --name web -- start, հետո pm2 startup և pm2 save) կամ systemd unit-ով Restart=always-ով։ map block-ը պատկանում է http context-ին․ այն թույլ է տալիս WebSocket upgrade-ներին անցնել՝ սովորական request-ները պահելով կրկնակի օգտագործվող keepalive միացումների վրա։ Ավելացրու երկրորդ server block 80 port-ով, որը վերադարձնում է 301 դեպի HTTPS, և միացրու gzip (gzip_types CSS-ի, JS-ի, JSON-ի, SVG-ի համար)․ Brotli-ին պետք է լրացուցիչ մոդուլ, կամ կարող ես թողնել, որ Cloudflare-ի նման CDN-ը սեղմի edge-ում։
map $http_upgrade $connection_upgrade { default upgrade; '' ''; }
upstream web_app { server 127.0.0.1:3000; keepalive 16; }
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://web_app;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
06Zero-downtime reload-ներ և deploy-ներ
Երբեք nginx-ը restart մի արա config-ի փոփոխությունը կիրառելու համար։ Ստուգիր այն, հետո reload արա․ master պրոցեսը կարդում է նոր config-ը, գործարկում է նոր worker-ներ և թույլ է տալիս հին worker-ներին ավարտել իրենց ընթացիկ request-ները մինչև դուրս գալը։ Եթե ստուգումը ձախողվում է, ոչինչ չի փոխվում, և հին config-ը շարունակում է սպասարկել։ Նույն գաղափարը վերաբերում է հավելվածներին։ PHP-FPM-ն աջակցում է graceful reload։ Laravel-ի համար յուրաքանչյուր release deploy արա իր թղթապանակում, այնտեղ գործարկիր composer install և migration-ները, հետո ատոմար փոխարկիր «current» symlink-ը և reload արա PHP-FPM-ը․ rollback-ը պարզապես symlink-ը հետ ուղղելն է։ Restart արա queue worker-ները (php artisan queue:restart), որպեսզի դրանք վերցնեն նոր կոդը։ Node-ի համար pm2 reload-ը cluster ռեժիմում instance-ները restart է անում մեկ առ մեկ․ մեկ fork-mode instance-ի դեպքում դա փաստացի restart է, ուստի գործարկիր առնվազն երկու instance, եթե քեզ պետք է իրական zero downtime։
# nginx: validate, then graceful reload
sudo nginx -t && sudo systemctl reload nginx
# Laravel release switch (atomic symlink swap)
ln -sfn /var/www/api/releases/2026-11-05_1200 /var/www/api/current
sudo systemctl reload php8.5-fpm
php /var/www/api/current/artisan queue:restart
# Node: rolling restart of a pm2 cluster
pm2 reload web
07Hosting պանելներ ընդդեմ մաքուր սերվերի
Plesk-ի և cPanel-ի նման պանելները քո փոխարեն տեղադրում և կառավարում են այս ամբողջ stack-ը՝ վեբ սերվեր, PHP-ի տարբերակներ, տվյալների բազաներ, mail, DNS, certificate-ներ և backup-ներ, ամեն ինչ վեբ UI-ով։ Դրանք հիանալի են, երբ շատ փոքր կայքեր պետք է կառավարվեն ոչ մասնագետների կողմից, կամ երբ hosting ես վաճառում։ Գինը վերահսկողությունն ու overhead-ն է։ Պանելներն ավելացնում են լիցենզիայի վճարներ, լրացուցիչ ծառայություններ, որոնք հիշողություն են օգտագործում և patch-ի կարիք ունեն, իրենց config template-ները, որոնք վերագրում են ձեռքով արված փոփոխությունները, և ավելի մեծ attack surface (պանելն ինքը login էջ է ինտերնետում)։ nginx-ով, PHP-FPM-ով, Node-ով և մի քանի script-ով մաքուր սերվերն ավելի թեթև է և ամբողջությամբ հասկանալի, բայց յուրաքանչյուր մաս քոնն է՝ թարմացումներ, certificate-ներ, backup-ներ և monitoring։ Ինժեների կողմից գործարկվող մի քանի հավելվածի համար մաքուրը սովորաբար ավելի լավն է, տասնյակ brochure կայքերի և mailbox-ների համար պանելը դեռ կարող է պրագմատիկ ընտրություն լինել։
Օգտագործեք, երբ
- nginx՝ որպես public entry point գրեթե ցանկացած վեբ հավելվածի համար՝ TLS-ի, static ֆայլերի, սեղմման և proxying-ի համար։
- PHP-FPM առանձին pool-երով՝ ամեն անգամ, երբ PHP հավելվածներ ես host անում, հատկապես նույն սերվերի վրա մեկից ավելի։
- pm2 կամ systemd՝ Node հավելվածները աշխատեցնելու, crash-ի դեպքում restart անելու և boot-ի ժամանակ գործարկելու համար։
- Apache nginx-ի հետևում՝ երբ legacy հավելվածները կախված են .htaccess կանոններից կամ միայն Apache-ի մոդուլներից։
Մի՛ օգտագործեք, երբ
- nginx config-ները ձեռքով մի գրիր, եթե managed պլատֆորմը (PaaS կամ serverless host) արդեն համապատասխանում է քո հավելվածին և բյուջեին։
- Node-ի կամ PHP-FPM-ի port-երը ուղիղ մի բացիր ինտերնետին․ առջևում միշտ reverse proxy պահիր։
- Hosting պանել մի ավելացրու սերվերին, որը գործարկում է մեկ կամ երկու հավելված, որոնք ինքդ ես deploy անում․ դա overhead է առանց օգուտի։
Այլընտրանքներ
- Caddy
Ավտոմատ HTTPS և շատ կարճ config․ հիանալի է փոքր setup-ների համար, էկոհամակարգը nginx-ից փոքր է։
- Apache httpd
Հասուն, ճկուն, .htaccess-ի աջակցություն․ դասական ռեժիմներում մեկ միացման համար ավելի ծանր է, քան nginx-ը։
- Traefik
Reverse proxy, որն ինքն իրեն կարգավորում է Docker-ի կամ Kubernetes-ի label-ներից․ հայտնի է կոնտեյներների հետ։
- FrankenPHP / Laravel Octane
PHP հավելվածը request-ների միջև պահում է հիշողության մեջ արագության համար․ պահանջում է զգուշություն state-ի հետ։
Լավագույն պրակտիկաներ
- Reload-ից առաջ միշտ գործարկիր nginx -t և config-ները պահիր version control-ում։
- Մեկ server block յուրաքանչյուր domain-ի համար, մեկ PHP-FPM pool և մեկ system user յուրաքանչյուր հավելվածի համար։
- Document root-ն ուղղիր միայն public թղթապանակին․ մերժիր dotfile-երը, բացի .well-known-ից։
- App սերվերները bind արա 127.0.0.1-ին կամ Unix socket-ին և թող nginx-ը լինի միակ public listener-ը։
- HTTP-ն redirect արա HTTPS-ի, միացրու HTTP/2-ը և gzip-ը և fingerprint արված asset-ների համար սահմանիր երկար cache header-ներ։
- Օգտագործիր release թղթապանակներ գումարած ատոմար symlink փոխարկում, որպեսզի deploy-ները և rollback-ները վայրկյաններ տևեն։
- Հետևիր nginx-ի error log-ին և PHP-FPM-ի log-ին․ դրանք բացատրում են 502 և 504 սխալների մեծ մասը։
Տարածված սխալներ
- root-ը սահմանել նախագծի թղթապանակին public/-ի փոխարեն՝ բացահայտելով .env-ը և source code-ը։
- Ցերեկը ծառայությունները restart անել reload-ի փոխարեն՝ կտրելով կենդանի request-ները։
- pm.max_children-ը թողնել լռելյայն արժեքով (5), ուստի կայքը համեստ traffic-ի տակ հերթ է կանգնում, կամ դնել այնքան բարձր, որ սերվերը swap անի։
- Մոռանալ X-Forwarded-Proto header-ը, ուստի հավելվածը կարծում է, որ request-ները HTTP են, և սխալ redirect URL-ներ է կառուցում։
- Պանելի կողմից կառավարվող config ֆայլերը ձեռքով խմբագրել և կորցնել փոփոխությունները պանելի հաջորդ թարմացման ժամանակ։
Խորը վերլուծություն
08Buffering, դանդաղ client-ներ և streaming պատասխաններ
nginx-ի ամենաարժեքավոր աշխատանքներից մեկն անտեսանելի է՝ buffering-ը։ Երբ թույլ կապով հեռախոսը form է վերբեռնում, nginx-ը նախ կարդում է հարցման ամբողջ body-ն (հիշողության մեջ՝ մինչև client_body_buffer_size, հետո ժամանակավոր ֆայլում) և միայն դրանից հետո է այն փոխանցում PHP-FPM-ին կամ Node-ին։ Վերադարձի ճանապարհին այն արագ կարդում է upstream-ի ամբողջ պատասխանը proxy կամ FastCGI buffer-ների մեջ և ինքն է այն կաթիլ առ կաթիլ փոխանցում դանդաղ client-ին։ Թանկ application worker-ը ազատվում է միլիվայրկյաններում, փոխանակ վայրկյաններով զբաղված մնալու գնացքում նստած ինչ-որ մեկի պատճառով։ Բացառությունը streaming-ն է. Server-Sent Events-ը, long polling-ը և stream արվող AI կամ հաշվետվության պատասխանները պետք է հասնեն client-ին, երբ արտադրվում են։ Այդ location-ների համար անջատեք proxy buffering-ը և բարձրացրեք proxy_read_timeout-ը, որը լռելյայն 60 վայրկյան է երկու read-երի միջև։ Հավելվածը կարող է նաև անջատել buffering-ը կոնկրետ պատասխանի համար X-Accel-Buffering: no header-ով։
location /events/ {
proxy_pass http://web_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_buffering off; # send bytes as soon as they arrive
proxy_cache off;
proxy_read_timeout 1h; # default 60s would cut idle streams
}
09Microcaching. ինչպես դիմանալ տրաֆիկի պիկին
Այն էջերի համար, որոնք նույնն են յուրաքանչյուր անանուն այցելուի համար (ապրանքների ցուցակ, հրապարակային API պատասխան), nginx-ը կարող է քեշավորել upstream-ի պատասխանը նույնիսկ ընդամենը մեկ վայրկյանով։ Թվում է անօգուտ, բայց վայրկյանում 500 հարցման դեպքում այն 500 PHP կատարումը վերածում է մեկի։ fastcgi_cache_lock-ը ստիպում է նույն չքեշավորված key-ի միաժամանակյա հարցումներին սպասել մեկ upstream fetch-ի, փոխանակ PHP-ի վրա «խուժելու», իսկ fastcgi_cache_use_stale-ը updating-ով մատուցում է մի փոքր հին պատճենը, մինչ մեկ հարցումը այն թարմացնում է background-ում։ Վտանգը անձնական տվյալների քեշավորումն է։ Քեշավորեք միայն GET և HEAD անանուն օգտատերերի համար. շրջանցեք cache-ը, երբ առկա է session cookie կամ Authorization header, և երբեք մի քեշավորեք cookie սահմանող պատասխանները։ X-Cache header-ը (HIT, MISS, BYPASS, STALE) թույլ է տալիս վարքագիծը ստուգել curl -I-ով։ proxy_cache-ը նույն կերպ է աշխատում Node հավելվածների առջև։
# http context
fastcgi_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:50m
max_size=1g inactive=10m use_temp_path=off;
# inside location ~ \.php$
fastcgi_cache api_cache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 1s;
fastcgi_cache_lock on;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_bypass $cookie_app_session $http_authorization;
fastcgi_no_cache $cookie_app_session $http_authorization;
add_header X-Cache $upstream_cache_status always;
10Rate limiting limit_req-ով
limit_req-ը իրականացնում է leaky bucket ըստ key-ի, սովորաբար՝ client-ի հասցեի։ limit_req_zone-ը գտնվում է http context-ում և սահմանում է key-ը, shared memory zone-ը և արագությունը (r/s կամ r/m)։ 64-բիթանոց համակարգերում յուրաքանչյուր վիճակ զբաղեցնում է 128 բայթ, ուստի 10m zone-ը հետևում է մոտ 80,000 հասցեի։ location-ի ներսում limit_req-ը կիրառում է այն։ burst-ը արագությունից բարձր հարցումների հերթի չափն է։ Առանց nodelay-ի այդ հերթագրված հարցումները ուշացվում են՝ արագությանը համապատասխանելու համար, ինչը upstream-ները հարթ է պահում, բայց էջերը դանդաղ են թվում. nodelay-ով դրանք մատուցվում են անմիջապես, և մերժվում են միայն burst-ից դուրս հարցումները։ delay=N-ը (1.15.7-ից) համատեղում է երկուսը։ Մերժումները լռելյայն 503 են, ինչը մոնիթորինգի համար outage է թվում, ուստի սահմանեք limit_req_status 429։ Հիշողություն խնայելու համար օգտագործեք $binary_remote_addr՝ $remote_addr-ի փոխարեն, և եթե առջևում CDN կա, նախ կարգավորեք իրական IP-ները, այլապես բոլոր այցելուները կկիսեն CDN-ի bucket-ը։
# http context
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_status 429;
# server context
location = /api/login {
limit_req zone=login burst=5 nodelay;
try_files $uri /index.php?$query_string;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
try_files $uri /index.php?$query_string;
}
11Edge case-եր. default server-ներ, իրական IP-ներ և կապերի սահմանաչափեր
Եթե հարցման Host header-ը չի համընկնում ոչ մի server_name-ի հետ, nginx-ը այն փոխանցում է այդ port-ի default server-ին, որը առաջին սահմանվածն է, եթե միայն մեկը նշված չէ default_server։ Scanner-ները, որոնք դիմում են ուղիղ ձեր IP-ին, այդ դեպքում կտեսնեն ձեր առաջին կայքը, իսկ HTTPS-ով՝ նրա սերտիֆիկատը, ինչը բացահայտում է, թե որ դոմեններն են այնտեղ։ Catch-all server-ը, որը մերժում է TLS handshake-ը (ssl_reject_handshake, nginx 1.19.4+) և պարզ HTTP-ն փակում է ոչ ստանդարտ 444-ով, խուսափում է դրանից։ Cloudflare-ի կամ load balancer-ի հետևում օգտագործեք realip մոդուլը. set_real_ip_from՝ յուրաքանչյուր վստահելի proxy range-ի համար, և real_ip_header CF-Connecting-IP կամ X-Forwarded-For, որպեսզի log-երը, rate limit-ները և fail2ban-ը տեսնեն այցելուին։ Երբեք մի վստահեք այդ header-ին չվստահված աղբյուրներից։ Վերջապես՝ թողունակությունը. worker_processes-ը բազմապատկած worker_connections-ով սահմանափակում է միաժամանակյա կապերը, իսկ proxy արված հարցումն օգտագործում է երկու կապ (client գումարած upstream), ուստի worker_connections-ի հետ միասին բարձրացրեք worker_rlimit_nofile-ը։
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on; # no certificate leaked to IP scanners
return 444; # close the connection without a response
}
# http context, behind Cloudflare (one line per range from cloudflare.com/ips)
set_real_ip_from 173.245.48.0/20;
real_ip_header CF-Connecting-IP;
Իրական իրավիճակներ
Պատահական 502 սխալներ ճաշի ժամին
Խնդիր
Laravel API-ն առավոտյան աշխատում է, հետո ամեն օր կեսօրին մոտ ընդհատվող 502 և 504 սխալներ է վերադարձնում։ PHP-FPM-ի restart-ը մեկ ժամով լուծում է խնդիրը։
Ինչպես լուծել
Նախ կարդացեք FPM log-ը. «server reached pm.max_children»-ը հաստատում է, որ pool-ը սպառված է։ Այդ pool-ի համար միացրեք pm.status_path-ը և պիկի ժամանակ հետևեք ակտիվ process-ներին և listen queue-ին։ Հետո պարզեք, թե ինչու են worker-ները դանդաղ. միացրեք slow log-ը (request_slowlog_timeout = 5s, slowlog path)՝ խրված հարցումների stack trace-երը ստանալու համար, որոնք սովորաբար ցույց են տալիս արտաքին API կանչ առանց timeout-ի կամ առանց index-ի query։ Նախ ուղղեք դա, ավելացրեք HTTP client timeout-ներ, և միայն դրանից հետո բարձրացրեք pm.max_children-ը հիշողության բյուջեի սահմաններում (ազատ RAM-ը բաժանած ps-ից ստացված worker-ի միջին RSS-ի վրա)։ Restart-ը պարզապես թաքցնում էր հերթը։
Վերբեռնումները ձախողվում են 413-ով կամ լուռ դատարկ ֆայլերով
Խնդիր
Օգտատերերը հաղորդում են, որ 30 MB video-ի վերբեռնումը ձախողվում է։ Երբեմն բրաուզերը ցույց է տալիս 413 Request Entity Too Large, երբեմն հարցումը հաջողվում է, բայց ֆայլը հավելվածում բացակայում է։
Ինչպես լուծել
Կան երեք հաջորդական սահմանաչափ, և ամենափոքրն է հաղթում։ nginx-ը client_max_body_size-ից մեծ body-ները մերժում է 413-ով, ուստի բարձրացրեք այն server-ում կամ կոնկրետ upload location-ում։ Հետո PHP-ն կիրառում է post_max_size-ը և upload_max_filesize-ը. երբ դրանք գերազանցվում են, PHP-ն դեն է նետում body-ն, և հավելվածը սխալի փոխարեն տեսնում է դատարկ հարցում, ինչը բացատրում է բացակայող ֆայլերը։ Սահմանեք դրանք pool-ում php_admin_value[post_max_size] = 64M և php_admin_value[upload_max_filesize] = 64M-ով, reload արեք FPM-ը և դանդաղ կապերի համար ստուգեք request_terminate_timeout-ը։ Եթե առջևում Cloudflare է, նրա պլանը նույնպես սահմանափակում է request body-ները (անվճար պլանում 100 MB)։ Մեծ մեդիա ֆայլերի համար վերբեռնեք ուղիղ object storage՝ presigned URL-ներով։
Log-երում բոլոր այցելուներն ունեն նույն IP-ն
Խնդիր
DNS-ը Cloudflare տեղափոխելուց և proxying-ը միացնելուց հետո access log-ը ցույց է տալիս Cloudflare-ի ընդամենը մի քանի հասցե։ Laravel-ի rate limiting-ը հիմա միանգամից արգելափակում է բոլորին, իսկ audit log-երն անօգուտ են։
Ինչպես լուծել
Սովորեցրեք nginx-ին, թե ով է իրական client-ը։ http context-ում ավելացրեք set_real_ip_from տողեր cloudflare.com/ips-ում հրապարակված յուրաքանչյուր range-ի համար (IPv4 և IPv6, իդեալում՝ փոքր script-ով գեներացված ժամանակացույցով) և real_ip_header CF-Connecting-IP, հետո nginx -t և reload։ $remote_addr-ը, log-երը, limit_req-ը և fail2ban-ը հիմա տեսնում են այցելուին։ Laravel-ում կարգավորեք trusted proxy-ները, որպեսզի request()->ip()-ը և HTTPS-ի հայտնաբերումը օգտագործեն forwarded header-ները, բայց միայն ձեր սեփական nginx-ից։ Վերջում port 80-ը և 443-ը սահմանափակեք Cloudflare-ի range-երով, որպեսզի ոչ ոք չկարողանա շրջանցել edge-ը և կեղծել header-ը։
Արագ ուղեցույց
- Թեստավորել config-ը և նրբորեն reload անել
sudo nginx -t && sudo systemctl reload nginx- Տպել ամբողջական միավորված config-ը
sudo nginx -T | less- Միացնել կայքը Ubuntu-ում
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/- Թեստավորել PHP-FPM config-ը
sudo php-fpm8.5 -t- Բացել FPM pool-ի status-ը (պաշտպանեք այն)
pm.status_path = /fpm-status- Log անել դանդաղ PHP հարցումները stack trace-երով
request_slowlog_timeout = 5s slowlog = /var/log/php8.5-fpm-api.slow.log- Ստուգել, թե որ socket-ներում է լսում PHP-FPM-ը
sudo ss -xlp | grep php- Թեստավորել vhost-ը, նախքան DNS-ը դրան կուղղվի
curl -sI --resolve example.com:443:127.0.0.1 https://example.com- Հետևել սխալներին իրական ժամանակում
sudo tail -f /var/log/nginx/error.log /var/log/php8.5-fpm.log- Rate limit-ների համար վերադարձնել 429՝ 503-ի փոխարեն
limit_req_status 429;
Իմ իրական նախագծերում
- Սպասարկում եմ Plesk սերվերներ, որտեղ nginx-ը կանգնած է Apache-ի և PHP-FPM-ի առջևում՝ սպասարկելով բազմաթիվ Laravel և Next.js կայքեր։
- Իմ սերվերներում Laravel-ի queue worker-ները աշխատում են supervisor-ի տակ, Node հավելվածները՝ pm2-ի, իսկ scheduler-ները՝ cron-ի միջոցով։
- Առանձին SaaS սերվերի վրա nginx-ը reverse proxy-ն է Docker Compose ծառայությունների առջևում։
- Միավորման իմ պլանն է աշխատել առանց պանելի՝ մաքուր nginx PHP-FPM-ով և դրա հետևում Node հավելվածներով։
Հարցեր և պատասխաններ
Q1Ինչո՞ւ է nginx-ը սովորաբար ավելի արագ, քան Apache-ն, static ֆայլերի և շատ միացումների համար։
nginx-ը օգտագործում է event-driven, non-blocking մոդել․ մի քանի worker պրոցես յուրաքանչյուրը multiplex է անում հազարավոր միացումներ, ուստի պարապ keepalive-ը կամ դանդաղ client-ները շատ քիչ հիշողություն արժեն։ Apache-ի դասական MPM-ները յուրաքանչյուր միացմանը հատկացնում են պրոցես կամ thread, ինչը թանկ է դառնում բարձր concurrency-ի դեպքում։ Apache-ի event MPM-ը կրճատեց տարբերությունը, և դինամիկ request-ների համար bottleneck-ը սովորաբար հավելվածն է, ոչ թե վեբ սերվերը։ Մյուս գործնական տարբերությունը .htaccess-ն է․ Apache-ն ստուգում է այն յուրաքանչյուր request-ի ժամանակ, ինչը ճկուն է, բայց ավելի դանդաղ, մինչդեռ nginx-ը ամբողջ config-ը բեռնում է մեկ անգամ՝ գործարկման ժամանակ։ Ահա թե ինչու շատ stack-եր nginx-ը դնում են առջևում TLS-ի, static ֆայլերի և buffering-ի համար, իսկ Apache-ն պահում են միայն այնտեղ, որտեղ պետք են նրա մոդուլները։
Q2Ինչպե՞ս է nginx-ը խոսում PHP-ի հետ, և ինչն է առաջացնում 502 Bad Gateway։
nginx-ը PHP request-ները FastCGI-ով փոխանցում է PHP-FPM-ին, սովորաբար Unix socket-ի միջոցով։ PHP-FPM-ը պահում է worker պրոցեսների pool, որոնք կատարում են script-ը և վերադարձնում պատասխանը։ 502-ը նշանակում է, որ nginx-ը չկարողացավ վավեր պատասխան ստանալ այդ upstream-ից․ PHP-FPM-ը անջատված է, socket-ի ճանապարհը կամ թույլտվությունները սխալ են, կամ worker-ը crash է եղել։ 504-ը նշանակում է, որ upstream-ը չափազանց երկար է տևել։ Իմ առաջին քայլերն են nginx-ի error log-ը, PHP-FPM-ի log-ը և FPM ծառայության systemctl status-ը։ Շատ տարածված պատճառ է «server reached pm.max_children»-ը, որը նշանակում է, որ բոլոր worker-ները զբաղված են, հաճախ դանդաղ տվյալների բազայի query-ների կամ արտաքին API call-երի պատճառով, որոնք շտկման կարիք ունեն, ոչ թե պարզապես ավելի շատ worker-ների։
Q3Ինչպե՞ս կհոստավորես Laravel API և Next.js frontend մեկ սերվերի վրա։
Երկու server block nginx-ում, մեկը յուրաքանչյուր domain-ի համար։ API block-ը որպես root օգտագործում է Laravel-ի public թղթապանակը, try_files՝ index.php-ին fallback անելու համար, և fastcgi_pass՝ դեպի առանձին PHP-FPM pool, որն աշխատում է իր օգտատիրոջով։ Frontend block-ը proxy է անում 127.0.0.1:3000-ի վրա գտնվող Next.js սերվերին, որը կենդանի է պահվում pm2-ով կամ systemd-ով, forwarding header-ներով և WebSocket upgrade-ի աջակցությամբ։ Երկուսն էլ ստանում են Let's Encrypt certificate-ներ, HTTP-ից HTTPS redirect-ներ, HTTP/2 և սեղմում։ Queue worker-ները աշխատում են supervisor-ի կամ systemd-ի տակ, scheduler-ը՝ cron-ով։ Deploy-ները Laravel-ի համար օգտագործում են release թղթապանակներ և symlink փոխարկում, իսկ Next.js-ի համար՝ pm2 reload, ուստի ոչ մի կողմ downtime չունի։
Q4Ո՞րն է տարբերությունը nginx reload-ի և restart-ի միջև։
Restart-ը կանգնեցնում է master պրոցեսը և բոլոր worker-ները, հետո նորից գործարկում․ բաց միացումները կտրվում են, և եթե նոր config-ը կոտրված է, կայքը մնում է անջատված։ Reload-ը signal է ուղարկում master-ին, որը parse է անում նոր config-ը․ եթե այն վավեր է, գործարկում է նոր worker-ներ դրանով և հին worker-ներին ասում է դադարեցնել միացումների ընդունումը և դուրս գալ ընթացիկ request-ներն ավարտելուց հետո։ Եթե config-ը անվավեր է, հինը շարունակում է աշխատել։ Այնպես որ անվտանգ սովորությունն է nginx -t, որին հաջորդում է systemctl reload nginx։ Իրական restart քեզ պետք է միայն որոշ low-level կարգավորումներ փոխելու կամ binary-ն upgrade անելու նման դեպքերում, և նույնիսկ դրա համար կա graceful in-place մեխանիզմ։
Q5Արդյո՞ք պետք է օգտագործենք Plesk-ի կամ cPanel-ի նման hosting պանել։
Կախված է նրանից, թե ով է շահագործում սերվերը և քանի կայք է այն կրում։ Պանելն արժե, երբ host ես անում շատ փոքր կայքեր, mailbox-ներ և DNS zone-ներ և ուզում ես, որ ոչ ինժեներները կառավարեն դրանք UI-ով՝ ներկառուցված backup-ներով և certificate-ներով։ Developer-ների կողմից deploy արվող մի քանի custom հավելվածի համար մաքուր սերվերն ավելի թեթև է․ լիցենզիայի ծախս չկա, հիշողության overhead-ը քիչ է, ինտերնետին բաց պանելի login չկա, և config-ները ամբողջությամբ քո վերահսկողության տակ են ու կարող են պահվել Git-ում։ Փոխզիջումն այն է, որ թարմացումները, certificate-ները, backup-ները և monitoring-ը պետք է ինքդ մշակես, իդեալում՝ script-երով։ Շատ թիմեր պանելից անցնում են մաքուր սերվերների, երբ իրենց հավելվածները դուրս են գալիս brochure կայքերի hosting-ի շրջանակից։
Q6Ինչպե՞ս ես Node.js հավելվածը աշխատեցնում production-ում։
Գործարկիր այն process manager-ի տակ, երբեք՝ տերմինալային session-ում։ pm2-ը հայտնի է․ այն restart է անում հավելվածը crash-ի դեպքում, կարող է մի քանի instance գործարկել cluster ռեժիմում, մոդուլով rotate է անում log-երը, իսկ pm2 startup-ը գումարած pm2 save-ը reboot-ից հետո վերականգնում են պրոցեսները։ systemd unit-ը Restart=always-ով, առանձին օգտատիրոջով և environment ֆայլով հավասարապես լավ, առանց dependency-ների տարբերակ է։ Ամեն դեպքում հավելվածը bind արա 127.0.0.1-ին, առջևում դիր nginx TLS-ի և buffering-ի համար, սահմանիր հիշողության սահմանափակումներ, որպեսզի leak-ը restart trigger անի swap անելու փոխարեն, և բացիր health endpoint, որը monitoring-ը կարող է ստուգել։ Zero downtime-ի համար գործարկիր առնվազն երկու instance և reload արա դրանք մեկ առ մեկ։
Սենիոր մակարդակի հարցեր
S1Մեկ nginx և PHP-FPM սերվեր հաջորդ շաբաթ պետք է դիմանա տրաֆիկի տասնապատիկ պիկին։ Ի՞նչ եք անում։
Նախ չափում եմ ընթացիկ առավելագույնը staging-ի դեմ load test-ով՝ հետևելով FPM-ի ակտիվ process-ներին, CPU-ին և database-ի բեռին, որպեսզի իմանամ, թե որ ռեսուրսն է առաջինը կոտրվում։ Հետո հեռացնում եմ աշխատանքը, ոչ թե ավելացնում worker-ներ։ Static ֆայլերը գնում են CDN-ով՝ երկար cache header-ներով։ Անանուն էջերը ստանում են մեկվայրկյանանոց microcache՝ cache lock-ով և stale-while-updating-ով, ինչը միավորում է նույնական հարցումները։ Թանկ endpoint-ները ստանում են rate limit-ներ, որոնք վերադարձնում են 429։ Դանդաղ կողմնակի գործողությունները, ինչպես email-ները և webhook-ները, տեղափոխվում են հերթեր։ Միայն դրանից հետո եմ pm.max_children-ը հաշվարկում worker-ի իրական հիշողության համեմատ, և եթե CPU-ն է սահմանը, իրադարձության համար vertical scale եմ անում։ Worker-ներ ավելացնելն առանց bottleneck-ը ուղղելու սովորաբար պարզապես հերթը տեղափոխում է database։
S2PHP-FPM, թե երկարակյաց runtime, ինչպես Laravel Octane-ը կամ FrankenPHP-ն. ինչպե՞ս եք որոշում։
PHP-FPM-ը framework-ը boot է անում յուրաքանչյուր հարցման ժամանակ, ինչն արժե մի քանիից մինչև տասնյակ միլիվայրկյան, բայց տալիս է կատարյալ մեկուսացում. հարցումների միջև state չի արտահոսում, իսկ memory leak-ը մեռնում է հարցման հետ։ Octane-ը FrankenPHP-ի, Swoole-ի կամ RoadRunner-ի վրա boot արված հավելվածը պահում է հիշողության մեջ, ուստի framework-ով ծանրաբեռնված endpoint-ների latency-ն և throughput-ը նկատելիորեն բարելավվում են։ Գինը կարգապահությունն է. singleton-ները, static property-ները և boot-ի ժամանակ resolve արված ամեն ինչ կարող են մեկ օգտատիրոջ տվյալները տանել հաջորդ հարցում, իսկ leak-երը կուտակվում են, մինչև worker-ը restart լինի։ Լռելյայն ընտրում եմ FPM, իսկ Octane-ը՝ երբ profiling-ը ցույց է տալիս, որ bootstrap-ի ժամանակն է գերիշխում, codebase-ը audit է արված request state-ի համար, և թիմն ունի թեստեր և max-request recycling՝ leak-երը բռնելու համար։
S3Ինչպե՞ս եք ապահով պահպանում client-ի IP-ն և պրոտոկոլը CDN-ի, nginx-ի և հավելվածի միջով։
Յուրաքանչյուր hop պետք է վստահի միայն իր առջևի hop-ին։ CDN edge-ում client-ի IP-ն հայտնի է. CDN-ը ավելացնում է header, օրինակ՝ CF-Connecting-IP։ nginx-ը օգտագործում է realip մոդուլը set_real_ip_from-ով՝ սահմանափակված CDN-ի հրապարակված range-երով, ուստի $remote_addr-ը դառնում է այցելուն միայն այն ժամանակ, երբ կապն իսկապես գալիս է CDN-ից։ Հետո nginx-ը հավելվածի ուղղությամբ սահմանում է X-Forwarded-For և X-Forwarded-Proto, իսկ framework-ը վստահում է proxy-ներին միայն 127.0.0.1-ից կամ private load balancer-ի հասցեից։ Firewall-ը web port-երը թույլատրում է միայն CDN-ից, այլապես ցանկացած ոք կարող էր ուղիղ միանալ և կեղծել header-ը։ Սա սխալ անելը կոտրում է rate limit-ները, fraud ստուգումները, audit log-երը և HTTPS redirect-ի հայտնաբերումը։
S4Ե՞րբ կընտրեք Caddy-ն կամ Traefik-ը nginx-ի փոխարեն։
Caddy-ն ուժեղ ընտրություն է փոքր կարգավորումների համար, որտեղ սերտիֆիկատների ավտոմատ կառավարումը և տասը տողանոց config-ը ավելի կարևոր են, քան նուրբ tuning-ը. այն ինքն է ստանում և թարմացնում սերտիֆիկատները և ունի ողջամիտ TLS defaults։ Traefik-ը հարմար է container պլատֆորմների համար. այն ծառայությունները հայտնաբերում է Docker label-երից կամ Kubernetes ռեսուրսներից և վերակարգավորում է route-երը առանց reload-ի, ինչն իդեալական է, երբ ծառայությունները գալիս ու գնում են։ nginx-ը հաղթում է, երբ պետք է հասուն caching, մանրամասն rate limiting, FastCGI դեպի PHP-FPM, փաստաթղթավորված կոնֆիգուրացիաների հսկայական բազա և կանխատեսելի performance բեռի տակ։ Գործառնական տեսանկյունից լավագույն proxy-ն այն է, որը ձեր թիմը կարող է debug անել գիշերը։ PHP-ի և խառը stack-երի համար ստանդարտացնում եմ nginx-ի վրա, իսկ label-ներով կառավարվող container միջավայրում կընդունեի Traefik-ը։
Բառարան
- Reverse proxy
- Սերվեր, որն ընդունում է client-ների հարցումները և փոխանցում backend հավելվածներին՝ վերադարձնելով դրանց պատասխանները, կարծես ինքն է դրանք ստեղծել։
- Upstream
- nginx-ում՝ backend սերվերը կամ սերվերների խումբը, որին proxy են արվում հարցումները, օրինակ՝ Node հավելված կամ PHP-FPM։
- FastCGI
- Երկուական պրոտոկոլ, որով web սերվերը հարցումներ է ուղարկում երկարակյաց application process manager-ին, ինչպես PHP-FPM-ը։
- Pool (PHP-FPM)
- PHP worker process-ների խումբ՝ իր user-ով, socket-ով և սահմանաչափերով, սովորաբար մեկը՝ յուրաքանչյուր հավելվածի համար։
- worker_connections
- Միաժամանակյա կապերի առավելագույն քանակը, որ կարող է բացել մեկ nginx worker, ներառյալ upstream-ների հետ կապերը։
- Keepalive
- Մեկ TCP կապի վերօգտագործում մի քանի HTTP հարցման համար, ինչը խնայում է handshake-ները client-ի, nginx-ի և upstream-ի միջև։
- Buffering
- Երբ nginx-ը ամբողջությամբ պահում է հարցումը կամ պատասխանը՝ նախքան փոխանցելը, որպեսզի դանդաղ client-ները չզբաղեցնեն application worker-ները։
- Microcache
- Դինամիկ պատասխանների քեշավորում շատ կարճ ժամանակով, հաճախ մեկ վայրկյան, նույնական հարցումների պոռթկումները միավորելու համար։
- Default server
- Server block-ը, որը nginx-ն օգտագործում է port-ի համար, երբ հարցման Host header-ը չի համընկնում ոչ մի server_name-ի։
- OPcache
- PHP-ի ներկառուցված քեշը կոմպիլացված script-ների համար shared memory-ում, որը խուսափում է PHP ֆայլերը ամեն հարցման ժամանակ նորից parse անելուց։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։