مقاله

پیکربندی Nginx برای Laravel — php-fpm، root و بهینه‌سازی

پیکربندی Nginx برای Laravel — php-fpm، root و بهینه‌سازی

server block، fastcgi_pass، try_files به index.php و headerهای امنیتی — config آماده production.

جواب کوتاه: در production، Nginx درخواست‌ها را می‌گیرد و فایل‌های استاتیک را خودش سرو می‌کند. اجرای PHP کار PHP-FPM است: Nginx هر درخواست PHP را از طریق پروتکل FastCGI — با Unix socket یا پورت TCP — به آن می‌دهد. برای Laravel سه چیز باید درست باشد: root روی پوشه public باشد، try_files همه مسیرها را به index.php بفرستد، و fastcgi_pass دقیقاً به همان آدرسی اشاره کند که pool مربوط در PHP-FPM روی آن گوش می‌دهد.

Apache با mod_php در XAMPP آسان است؛ در production اوبونتو معمولاً Nginx + php-fpm است — سبک‌تر و concurrent بهتر. Laravel فقط از public/ سرو شود؛ بقیه پوشه‌ها نباید web-accessible باشند.

ساختار کلی

Client → Nginx :80/443 → fastcgi → php-fpm → Laravel bootstrap

PHP-FPM چیست و چه نقشی کنار Nginx دارد؟

PHP-FPM (FastCGI Process Manager) سرویسی است که چند پردازش PHP را آماده نگه می‌دارد و درخواست‌های FastCGI را به آن‌ها می‌دهد. Nginx خودش PHP اجرا نمی‌کند. وقتی درخواستی به index.php می‌رسد، Nginx آن را به PHP-FPM می‌فرستد. یکی از پردازش‌های آزاد pool، Laravel را اجرا می‌کند و پاسخ را برمی‌گرداند. هر پردازش تا پایان یک درخواست مشغول همان درخواست است. پس تعداد پردازش‌ها، تعداد درخواست‌های PHP هم‌زمان را محدود می‌کند.

در اوبونتو و دبیان، سرویس با نسخه PHP نام‌گذاری می‌شود (مثلاً php8.3-fpm) و فایل‌های اصلی این‌جاست:

  • /etc/php/8.3/fpm/php-fpm.conf — تنظیمات کلی سرویس
  • /etc/php/8.3/fpm/pool.d/www.conf — تنظیمات pool: کاربر، آدرس listen، تعداد پردازش‌ها
  • /etc/php/8.3/fpm/php.ini — php.ini مخصوص FPM؛ جدا از /etc/php/8.3/cli/php.ini که دستورهای artisan و worker از آن استفاده می‌کنند

اشتباه رایج: upload_max_filesize را در php.ini مربوط به CLI عوض می‌کنید و تعجب می‌کنید چرا سایت تغییری نکرده. بعد از تغییر تنظیمات FPM، سرویس را reload کنید:

sudo php-fpm8.3 -t
sudo systemctl reload php8.3-fpm

اتصال Nginx به PHP-FPM — Unix socket یا TCP؟

آدرسی که در listen داخل pool تعریف می‌کنید باید با fastcgi_pass در Nginx یکی باشد:

  • Unix socket (listen = /run/php/php8.3-fpm.sock): وقتی Nginx و PHP-FPM روی یک سرورند. کاربر Nginx باید اجازه دسترسی به فایل socket را داشته باشد (listen.owner و listen.group، معمولاً www-data). در اوبونتو /var/run به /run لینک شده و هر دو مسیر یکی است.
  • TCP (listen = 127.0.0.1:9000): وقتی PHP-FPM در کانتینر یا سرور دیگری است — مثل حالت Docker Compose پایین‌تر. پورت را فقط در شبکه داخلی باز کنید.

server block پایه

این پیکربندی بر اساس نمونه مستندات استقرار Laravel 12 است. نام دامنه، مسیر پروژه و نسخه PHP را با سرور خودتان عوض کنید:

server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    root /var/www/portfolio/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

چند نکته درباره این بلاک:

  • location ~ ^/index\.php(/|$) فقط front controller را به PHP-FPM می‌دهد، نه هر فایل .php را. اگر فایل PHP دیگری به هر دلیلی در public قرار بگیرد، اجرا نمی‌شود. با این حال در public فایل PHP دیگری نگذارید.
  • $realpath_root مسیر واقعی را بعد از دنبال کردن symlink می‌دهد. در روش‌های deploy که پوشه current یک symlink به نسخه جدید است، این باعث می‌شود PHP-FPM و OPcache مسیر نسخه جدید را ببینند.
  • بلاک آخر دسترسی به فایل‌های مخفی مثل .env و .git را می‌بندد، به‌جز .well-known که برای صدور گواهی SSL لازم است.

چرا public root؟

.env و storage بیرون document root — امنیت.

try_files ضروری

بدون آن routeهای Laravel 404 می‌دهند — همه به index.php front controller.

ترتیب کار try_files $uri $uri/ /index.php?$query_string این است: اول فایل واقعی (مثل CSS یا عکس داخل public)، بعد پوشه، و اگر هیچ‌کدام نبود، درخواست با query string کامل به index.php می‌رود تا router لاراول تصمیم بگیرد.

client_max_body_size

client_max_body_size 20M;

آپلود فایل — هماهنگ با PHP upload_max_filesize.

پیش‌فرض Nginx یک مگابایت است و آپلود بزرگ‌تر با خطای 413 رد می‌شود. در php.ini مربوط به FPM هم upload_max_filesize و post_max_size باید حداقل به همین اندازه باشند. هر کدام کمتر باشد، همان محدودیت اعمال می‌شود.

gzip

gzip on;
gzip_types text/plain text/css application/json application/javascript;

php-fpm pool

pm = dynamic
pm.max_children = 20
pm.start_servers = 4

تنظیم بر اساس RAM — monitor با htop.

این عددها نمونه‌اند، نه توصیه. منطق انتخابشان این است:

  • pm: حالت dynamic تعداد پردازش‌ها را بین حداقل و حداکثر تنظیم می‌کند و برای بیشتر سایت‌ها مناسب است. حالت static همیشه همه پردازش‌ها را روشن نگه می‌دارد و برای سرور اختصاصی با بار ثابت است. حالت ondemand پردازش‌ها را فقط هنگام درخواست می‌سازد و برای سایت کم‌ترافیک یا سروری با چند pool است.
  • pm.max_children: حافظه‌ای که برای PHP در نظر گرفته‌اید را بر میانگین حافظه هر پردازش FPM تقسیم کنید. این میانگین را روی سرور خودتان در ساعت پرترافیک اندازه بگیرید (مثلاً با ps -o rss= -C php-fpm8.3). عدد بیش از حد باعث استفاده از swap می‌شود و عدد کم باعث صف شدن درخواست‌ها.
  • pm.max_requests: هر پردازش بعد از این تعداد درخواست بازسازی می‌شود. این جلوی رشد تدریجی حافظه را می‌گیرد.
  • timeoutها: fastcgi_read_timeout در Nginx (پیش‌فرض ۶۰ ثانیه)، request_terminate_timeout در pool، و max_execution_time در php.ini را با هم هماهنگ کنید. کار طولانی — مثل ساخت گزارش بزرگ یا ارسال ایمیل گروهی — جایش در queue است، نه در درخواست وب.

اگر در لاگ PHP-FPM پیام رسیدن به max_children را می‌بینید، یعنی درخواست‌ها پشت pool صف شده‌اند. قبل از بالا بردن عدد، ببینید چرا درخواست‌ها طول می‌کشند. slowlog در pool درخواست‌های کند را ثبت می‌کند.

با Docker Compose

fastcgi_pass app:9000;

نام service به‌جای unix socket — Compose.

static asset

location ~* \.(js|css|png|jpg|webp)$ {
    expires 30d;
    access_log off;
}

یا CDN — بهینه‌سازی.

دسترسی فایل‌ها — storage و bootstrap/cache

Laravel در دو پوشه می‌نویسد: storage (لاگ، کش، session، فایل‌های آپلودی) و bootstrap/cache (کش پیکربندی و route). کاربری که PHP-FPM با آن اجرا می‌شود (مقدار user در pool، معمولاً www-data) باید در این دو پوشه اجازه نوشتن داشته باشد. بقیه پروژه فقط باید خواندنی باشد:

sudo chown -R deploy:www-data /var/www/portfolio
sudo chmod -R 775 /var/www/portfolio/storage /var/www/portfolio/bootstrap/cache
php artisan storage:link

از chmod 777 استفاده نکنید. یک دام رایج: اگر دستور artisan را با کاربر دیگری (مثلاً root) اجرا کنید، ممکن است فایل لاگ یا کش با مالکیت همان کاربر ساخته شود. بعد PHP-FPM نمی‌تواند در آن بنویسد و سایت خطای 500 می‌دهد. artisan را با همان کاربر deploy یا کاربر وب اجرا کنید.

خطاهای رایج: 403، 404، 502 و 504

اولین جایی که باید نگاه کنید /var/log/nginx/error.log است، بعد لاگ PHP-FPM و storage/logs/laravel.log.

  • 403 Forbidden: معمولاً root به پوشه اشتباهی اشاره می‌کند (پوشه‌ای بدون index.php)، یا کاربر Nginx اجازه عبور از یکی از پوشه‌های والد مسیر پروژه را ندارد.
  • 404 برای همه مسیرها به‌جز صفحه اصلی: try_files وجود ندارد یا اشتباه است.
  • پاسخ «File not found.» و پیام Primary script unknown در لاگ: مسیری که با SCRIPT_FILENAME به PHP-FPM می‌رسد وجود ندارد یا PHP-FPM به آن دسترسی ندارد — root را بررسی کنید.
  • 502 Bad Gateway: Nginx به PHP-FPM وصل نمی‌شود. اگر در لاگ No such file or directory می‌بینید، سرویس خاموش است یا مسیر socket با نسخه PHP نصب‌شده نمی‌خواند (مثلاً بعد از ارتقای PHP). اگر Permission denied می‌بینید، تنظیم listen.owner و listen.group را بررسی کنید.
  • 504 Gateway Timeout: PHP-FPM جواب داده ولی دیرتر از fastcgi_read_timeout. کار سنگین را به queue ببرید، نه اینکه timeout را بی‌نهایت کنید.
  • 500 با صفحه خطای Laravel یا صفحه سفید: معمولاً دسترسی نوشتن در storage، کلید APP_KEY یا پیکربندی کش‌شده قدیمی. لاگ لاراول علت را نشان می‌دهد.

Queue worker و scheduler — بیرون از PHP-FPM

worker صف (php artisan queue:work) و scheduler از PHP-FPM و Nginx رد نمی‌شوند. این‌ها پردازش‌های CLI هستند و از php.ini مربوط به CLI استفاده می‌کنند. worker را معمولاً Supervisor یا systemd زنده نگه می‌دارد و scheduler با یک کرون هر دقیقه اجرا می‌شود. دو نکته مرتبط با deploy: بعد از هر deploy دستور php artisan queue:restart را بزنید تا workerها کد جدید را بارگذاری کنند، و worker را با همان کاربری اجرا کنید که با دسترسی پوشه storage سازگار است. مفهوم صف و راه‌اندازی آن را در مقدمه Queue در Laravel توضیح داده‌ایم.

چک‌لیست بررسی بعد از deploy

sudo nginx -t && sudo systemctl reload nginx
sudo systemctl status php8.3-fpm
curl -I https://example.com
curl -I https://example.com/some-route
curl -I https://example.com/.env
php artisan optimize
php artisan about
  • صفحه اصلی و یک route غیر از صفحه اصلی کد 200 بدهند، و یک آدرس ناموجود صفحه 404 خود Laravel را نشان دهد
  • آدرس /.env باید 403 یا 404 بدهد، هرگز محتوای فایل
  • php artisan optimize کش config، event، route و view را می‌سازد. بعد از هر تغییر در .env دوباره اجرایش کنید
  • php artisan about محیط، وضعیت debug و وضعیت کش‌ها را نشان می‌دهد. در production، APP_DEBUG باید false باشد
  • یک فایل آزمایشی آپلود کنید و مطمئن شوید از طریق /storage قابل دسترسی است

اشتباهات

  • root روی پوشه پروژه نه public
  • fastcgi_param اشتباه — open_basedir error
  • فراموش کردن deny hidden files
  • مسیر socket در fastcgi_pass بعد از ارتقای نسخه PHP عوض نشده
  • تغییر php.ini مربوط به CLI به‌جای FPM

جمع‌بندی

SSL: Let us Encrypt. Deploy کامل: Ubuntu. Cron: Scheduled tasks.

تنظیم Nginx سرور

اشتراک‌گذاری