یک پنج‌شنبه صبح سایت با خطای ۵۰۰ بالا نیامد. هیچ دیپلویی نداشتیم، هیچ کدی عوض نشده بود، دیتابیس هم سرِ جایش بود. عجیب‌تر این‌که وقتی رفتیم لاگ را بخوانیم تا بفهمیم چه شده، آخرین سطر مربوط به چند دقیقه قبل بود و بعد از آن هیچ چیز؛ انگار خودِ لاگ هم قطع شده بود.

چند دقیقه بعد ایمیل هاست رسید: سهمیه‌ی دیسک پر شده است. یک du ساده مقصر را لو داد؛ یک فایل به نام storage/logs/laravel.log که ماه‌ها بی‌سروصدا بزرگ شده بود و حالا چند گیگابایت جا گرفته بود.

این دسته از خرابی‌ها هشدار تدریجی ندارند. تا وقتی یک بایت جا باقی است همه‌چیز عادی کار می‌کند؛ لحظه‌ای که سهمیه تمام می‌شود همه‌چیز هم‌زمان می‌شکند: سشن، کش، آپلود، و حتی نوشتن همان لاگی که قرار بود ماجرا را توضیح بدهد. این یادداشت همان مسیر است: تشخیص، علت، و راه‌اندازی چرخش لاگ روی هاست اشتراکی تا دیگر تکرار نشود.

علامت‌ها: از کند شدن تا خطای نوشتن#

پر شدن سهمیه‌ی دیسک یک علامت واحد ندارد. بسته به این‌که کدام عملیات زودتر به دیوار بخورد، چیزهای مختلفی می‌بینید:

  • صفحه‌ی سفید یا خطای ۵۰۰ روی همه‌ی مسیرها، حتی مسیرهایی که هیچ کاری با فایل ندارند.
  • در لاگ وب‌سرور یا خروجی خطای PHP، پیام‌هایی از این دست: No space left on device یا failed to open stream: Disk quota exceeded.
  • گاهی به‌جای آن‌ها Permission denied می‌بینید. این یکی گمراه‌کننده است، چون آدم فوراً دنبال chmod می‌رود در حالی که مشکل دسترسی نیست.
  • کاربر لاگین می‌کند ولی بلافاصله بیرون می‌افتد، چون درایور سشن روی فایل است و دیگر نمی‌تواند فایل سشن بنویسد.
  • آپلود فایل شکست می‌خورد یا فایل ناقص ذخیره می‌شود.
  • کند شدن محسوس: هر درخواست منتظر یک نوشتن ناموفق می‌ماند.
  • ایمیل هشدار هاست با موضوعی شبیه «disk quota warning». معمولاً وقتی می‌رسد که دیر شده است.

نکته‌ی مهم این است که هیچ‌کدام از این‌ها اسم فایل مقصر را نمی‌گویند. سیستم فقط می‌گوید جا نیست؛ پیدا کردن این‌که چه چیزی جا را خورده، کار خودتان است.

پیدا کردن مقصر با du و ls#

اول ببینید اصلاً چقدر جا مصرف شده. روی سی‌پنل بخش Disk Usage همین را نشان می‌دهد، ولی از SSH سریع‌تر است:

Bash
du -sh ~

بعد یک لایه پایین‌تر بروید و بزرگ‌ترین پوشه‌ها را مرتب کنید:

Bash
du -sh ~/* | sort -h

sort -h اعداد با پسوند (K، M، G) را درست مرتب می‌کند. حالا داخل پوشه‌ی اپ همین کار را تکرار کنید تا به storage برسید:

Bash
du -sh ~/app/storage/* | sort -h
ls -lh ~/app/storage/logs

اگر مقصر لاگ باشد، ls -lh بلافاصله آن را نشان می‌دهد: یک laravel.log با اندازه‌ای که با بقیه‌ی فایل‌ها اصلاً هم‌مقیاس نیست.

برای این‌که مطمئن شوید چیز بزرگ دیگری جا نمانده، همه‌ی فایل‌های بالای صد مگابایت را در خانه‌ی کاربری فهرست کنید:

Bash
find ~ -type f -size +100M -exec ls -lh {} \; 2>/dev/null

2>/dev/null برای این است که خطاهای «دسترسی نیست» روی پوشه‌های سیستمی خروجی را شلوغ نکنند. اگر خروجی خالی بود، مشکل از تعداد فایل‌های کوچک است نه یک فایل غول؛ در بخش مصرف‌کننده‌های دیگر به آن می‌رسیم.

چرا فایل laravel.log بی‌نهایت بزرگ می‌شود#

پاسخ کوتاه: چون کسی به آن نگفته که نباید. لاراول به‌طور پیش‌فرض روی درایوری تنظیم می‌شود که فقط یک فایل دارد و همیشه به انتهای همان یک فایل اضافه می‌کند. هیچ سقفی، هیچ چرخشی، هیچ حذف خودکاری در کار نیست.

چهار چیز این رشد را سریع می‌کنند:

  1. درایور single. این درایور یعنی «همه‌چیز در laravel.log، برای همیشه». تا وقتی خودتان دست به آن نزنید، فایل فقط بزرگ‌تر می‌شود.
  1. سطح لاگ debug در پروداکشن. مقدار پیش‌فرض LOG_LEVEL در بسیاری از فایل‌های نمونه debug است. یعنی پایین‌ترین سطح ممکن؛ یعنی هر چیزی که هر پکیجی تصمیم بگیرد گزارش کند، نوشته می‌شود.
  1. یک خطای تکرارشونده. بدترین حالت این است: یک استثنا که در هر درخواست رخ می‌دهد و هر بار یک stack trace کامل می‌نویسد. یک stack trace می‌تواند چند کیلوبایت باشد. ضرب‌در تعداد درخواست‌های یک ماه، عدد ترسناکی می‌شود.
  1. بات‌ها. خزنده‌ها و اسکنرها مسیرهای عجیب می‌زنند، 404 و MethodNotAllowed تولید می‌کنند، و اگر این‌ها هم لاگ شوند، حجم لاگ ربطی به تعداد کاربران واقعی شما ندارد.

روی سرور اختصاصی این وضع هم بد است ولی دیرتر خودش را نشان می‌دهد. روی هاست اشتراکی که سهمیه‌ی دیسک اغلب چند گیگابایت است، یک خطای تکرارشونده در عرض چند هفته کل سهمیه را می‌بلعد.

پاک‌سازی فوری: چرا rm کار اشتباهی است#

وسوسه‌ی اول این است که فایل را پاک کنید:

Bash
rm ~/app/storage/logs/laravel.log

کار می‌کند، ولی دو ریسک دارد.

ریسک اول، مالکیت و دسترسی. با rm فایل و تمام مشخصاتش از بین می‌رود. فایل بعدی را هر پروسه‌ای که زودتر لاگ بنویسد از نو می‌سازد، با مالکیت و مجوزِ همان پروسه و umask آن. اگر روی هاست شما کاربر PHP با کاربر SSH یکی نباشد یا کرون‌ها با تنظیمات دیگری اجرا شوند، فایل بعدی می‌تواند برای یکی از این دو غیرقابل نوشتن باشد. آن‌وقت به‌جای «دیسک پر است» می‌رسید به «Permission denied» — همان علامت گمراه‌کننده‌ای که بالاتر گفتیم.

ریسک دوم، فضا واقعاً آزاد نمی‌شود. اگر پروسه‌ای فایل را باز نگه داشته باشد — مثل یک queue:work که به‌صورت دائمی در حال اجراست — حذف کردن نام فایل، فضای دیسک را برنمی‌گرداند. سیستم‌عامل فضا را تا وقتی که آخرین توصیف‌گر فایل بسته شود نگه می‌دارد. شما rm می‌زنید، du می‌گوید همه‌چیز مرتب است، ولی سهمیه هنوز پر است.

راه درست، خالی کردن محتوای فایل است، نه حذف آن:

Bash
truncate -s 0 ~/app/storage/logs/laravel.log

اگر truncate روی هاست نبود، همین کار را با خود شل بکنید:

Bash
: > ~/app/storage/logs/laravel.log

هر دو، فایل را با همان inode، همان مالک و همان مجوزها نگه می‌دارند و فقط طولش را صفر می‌کنند. پروسه‌هایی که فایل را در حالت append باز کرده‌اند — که مونولوگ همین کار را می‌کند — بدون مشکل به نوشتن ادامه می‌دهند. اگر می‌خواهید قبل از خالی کردن، چند سطر آخر را برای بررسی نگه دارید، اول یک کپی کوچک بردارید:

Bash
tail -n 2000 ~/app/storage/logs/laravel.log > ~/last-errors.txt
truncate -s 0 ~/app/storage/logs/laravel.log

این پاک‌سازی فقط اورژانس است. سایت بالا می‌آید ولی هیچ چیزی حل نشده؛ فردا دوباره شروع به رشد می‌کند.

درمان اصلی: چرخش لاگ با درایور daily#

راه‌حل واقعی، عوض کردن درایور از single به daily است. daily به‌جای یک فایل، برای هر روز یک فایل جدا می‌سازد و فایل‌های قدیمی‌تر از تعداد مشخصی روز را خودش پاک می‌کند.

در .env:

INI
LOG_CHANNEL=daily
LOG_LEVEL=warning
LOG_DAILY_DAYS=14

یک نکته‌ی نسخه‌ای: در نسخه‌های جدیدتر لاراول مقدار پیش‌فرض LOG_CHANNEL روی stack است و کانال stack خودش تصمیم می‌گیرد به کدام کانال‌ها بنویسد. اگر روی stack هستید، متغیر تعیین‌کننده LOG_STACK است نه LOG_CHANNEL:

INI
LOG_CHANNEL=stack
LOG_STACK=daily
LOG_LEVEL=warning
LOG_DAILY_DAYS=14

قبل از هر چیز مطمئن شوید کدام حالت را دارید: فایل config/logging.php را باز کنید و ببینید کانال stack از چه چیزی می‌خواند. بخش مربوط به daily معمولاً چنین شکلی دارد:

PHP
'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'debug'),
    'days' => (int) env('LOG_DAILY_DAYS', 14),
    'replace_placeholders' => true,
],

و بعد از هر تغییر در .env، کش کانفیگ را پاک کنید، وگرنه هیچ اتفاقی نمی‌افتد:

Bash
php artisan config:clear

اگر روی سرور کانفیگ را کش می‌کنید، php artisan config:cache را دوباره اجرا کنید. و اگر مقدارها را عوض کردید و هیچ تغییری ندیدید، به پایان‌خط‌های خود فایل .env هم شک کنید؛ یک کاراکتر نامرئی در انتهای خط می‌تواند مقدار را خراب کند بدون این‌که چیزی به شما بگوید.

مقایسه‌ی یک فایل لاگ که بی‌پایان رشد می‌کند با هفت فایل روزانه که قدیمی‌ترینشان حذف می‌شود

چرخش لاگ دقیقاً کِی اتفاق می‌افتد (و کِی نمی‌افتد)#

خوب است بدانید زیر کاپوت چه می‌گذرد، چون چند تا تله دارد.

daily از هندلری استفاده می‌کند که نام فایل را با تاریخ می‌سازد: به‌جای laravel.log فایل‌هایی مثل laravel-2026-09-07.log می‌بینید. وقتی لاگی نوشته می‌شود و تاریخ امروز با نام فایل جاری فرق دارد، هندلر «می‌چرخد»: فایل امروز را می‌سازد و همان موقع فایل‌های قدیمی‌ترِ همان الگو را می‌شمارد و مازاد بر days را حذف می‌کند.

سه نتیجه‌ی عملی از این طرز کار:

  • حذف، زمان‌بندی‌شده نیست؛ رویدادمحور است. اگر سایت چند هفته هیچ لاگی ننویسد، هیچ فایلی هم حذف نمی‌شود. البته در آن حالت چیزی هم رشد نکرده، پس مشکلی نیست.
  • فایل قدیمی laravel.log را خودش پاک نمی‌کند. الگوی حذف فقط فایل‌های تاریخ‌دار را می‌گیرد. آن laravel.log چند گیگابایتیِ قبلی همان‌جا می‌ماند تا خودتان یک بار حذفش کنید. این را حتماً چک کنید، وگرنه سهمیه هنوز پر است و فکر می‌کنید مشکل حل شده.
  • مقدار صفر یعنی بی‌نهایت. اگر LOG_DAILY_DAYS را صفر بگذارید، هیچ فایلی حذف نمی‌شود. عدد را صریح بنویسید.

برای تعداد روز، بین ۷ تا ۱۴ برای یک فروشگاه معمولی کافی است. اگر روزی چند مگابایت لاگ دارید و سهمیه‌تان تنگ است، ۷ محافظه‌کارانه‌تر است. اگر لاگتان سبک است و گاهی لازم می‌شود دو هفته عقب بروید، ۱۴ منطقی‌تر است.

سطح لاگ: چرا debug در پروداکشن گران است#

سطح لاگ یعنی «کف اهمیتی که می‌نویسم». هر پیامی که سطحش پایین‌تر از این کف باشد، اصلاً روی دیسک نمی‌رود. ترتیب سطح‌ها استاندارد است:

سطحیعنی چهدر پروداکشن
debugجزئیات ریز برای توسعهنه
infoاتفاق‌های عادی و مورد انتظارمعمولاً نه
noticeاتفاق عادی ولی قابل توجهبسته به مورد
warningچیزی درست نیست ولی کار ادامه داردبله
errorعملیات شکست خوردبله
critical و بالاتربخشی از سیستم از کار افتادهبله

برای اکثر سایت‌های پروداکشن، warning نقطه‌ی درستی است: هم استثناها و شکست‌ها را می‌گیرید، هم پرحرفی پکیج‌ها را نه. اگر می‌خواهید حجم را باز هم کمتر کنید و فقط شکست‌های واقعی برایتان مهم است، error هم قابل دفاع است — به شرطی که بدانید هشدارهای «هنوز نشکسته ولی دارد می‌شکند» را از دست می‌دهید.

یک اشتباه رایج را هم روشن کنیم: APP_DEBUG و LOG_LEVEL دو چیز جدا هستند. APP_DEBUG=false فقط جلوی نمایش جزئیات خطا به کاربر را می‌گیرد؛ نوشتن در لاگ سرِ جایش است. اگر درباره‌ی این تفکیک شک دارید، یادداشت ما درباره‌ی APP_DEBUG روی هاست اشتراکی دقیقاً همین را باز کرده است.

کانال stack و جدا کردن خطاهای مهم#

daily با warning مشکل حجم را حل می‌کند، ولی یک عارضه دارد: خطاهای واقعاً مهم لای انبوه هشدارهای معمولی گم می‌شوند. راه تمیز، کانال stack است؛ یعنی یک پیام هم‌زمان به چند مقصد برود، هر کدام با کف اهمیت خودش.

در config/logging.php یک کانال جدا برای خطاهای مهم اضافه کنید:

PHP
'critical' => [
    'driver' => 'daily',
    'path' => storage_path('logs/critical.log'),
    'level' => 'error',
    'days' => 60,
    'replace_placeholders' => true,
],

و در .env:

INI
LOG_CHANNEL=stack
LOG_STACK=daily,critical
LOG_LEVEL=warning
LOG_DAILY_DAYS=14

حالا دو جریان دارید. laravel-YYYY-MM-DD.log همه‌چیز از warning به بالا را نگه می‌دارد و بعد از دو هفته پاک می‌شود. critical-YYYY-MM-DD.log فقط error و بالاتر را می‌گیرد، کوچک می‌ماند، و می‌تواند دو ماه بماند بدون این‌که دیسک را اذیت کند. وقتی مشتری می‌گوید «هفته‌ی پیش پرداختم گیر کرد»، دقیقاً می‌دانید کدام فایل را باز کنید.

کرون هفتگی و هشدار زودهنگام سهمیه#

daily کار خودش را می‌کند، ولی یک تور ایمنی ارزان است. یک کرون هفتگی بگذارید که فایل‌های تاریخ‌دار قدیمی را جارو کند — همان‌هایی که ممکن است از کانال‌های دیگر یا از قبلِ تنظیمات جامانده باشند:

Bash
0 4 * * 0 /usr/bin/find /home/USER/app/storage/logs -name '*.log' -mtime +30 -delete

-mtime +30 یعنی فایل‌هایی که بیش از ۳۰ روز است تغییر نکرده‌اند. چون از تاریخ تغییر استفاده می‌کند، فایل امروز هرگز حذف نمی‌شود.

و یک کرون گزارش‌دهنده که هفته‌ای یک بار مصرف را بگوید. سی‌پنل خروجی کرون را برایتان ایمیل می‌کند، پس همین یک خط کافی است تا قبل از رسیدن به سقف خبردار شوید:

Bash
0 7 * * 1 /usr/bin/du -sh /home/USER /home/USER/app/storage /home/USER/public_html

اگر خروجی هیچ‌وقت به ایمیلتان نرسید یا کرون بی‌صدا شکست خورد، اول از همه به تفاوت شل کرون با شل تعاملی شک کنید؛ همین موضوع را در یادداشت PHP 8.4 در کرون‌جاب سی‌پنل شکافته‌ایم. خلاصه‌اش این است که در کرون هیچ alias و هیچ PATH دلخواهی وجود ندارد؛ مسیر کامل دستورها را بنویسید، همان‌طور که در نمونه‌های بالا نوشته‌ایم.

بقیه‌ی چیزهایی که سهمیه‌ی دیسک را می‌خورند#

لاگ معمولاً بزرگ‌ترین مقصر است، ولی تنها مقصر نیست. وقتی سهمیه تنگ می‌شود این چهار جا را هم نگاه کنید.

سهم مصرف دیسک بین لاگ، بک‌آپ، کش تصویر، سشن و کش فریم‌ورک

بک‌آپ‌های فراموش‌شده. یک اسکریپت بک‌آپ که چند ماه پیش نوشتید و هر شب یک sql.gz می‌سازد ولی هیچ‌وقت قدیمی‌ها را پاک نمی‌کند. این‌ها تک‌فایل‌های بزرگ‌اند و با همان find -size +100M بالا پیدا می‌شوند. بک‌آپ روی همان دیسکی که ممکن است پر شود، بک‌آپ حساب نمی‌شود؛ یک نسخه را جای دیگری ببرید.

کش تصویرها و بندانگشتی‌ها. اگر برای هر تصویر چند اندازه می‌سازید، پوشه‌ی کش تصویر می‌تواند از خود تصویرهای اصلی بزرگ‌تر شود. مخصوصاً اگر نسخه‌های قدیمی بعد از تغییر ابعاد پاک نشوند. اگر با GD کار می‌کنید، یادداشت پردازش تصویر با GD درباره‌ی همین محدودیت‌ها نوشته شده است.

فایل‌های سشن. با درایور فایل، هر بازدیدکننده — از جمله هر بات — یک فایل در storage/framework/sessions می‌سازد. هر فایل کوچک است، ولی تعدادشان می‌تواند به ده‌ها هزار برسد و روی بعضی هاست‌ها محدودیت تعداد inode زودتر از محدودیت حجم پر می‌شود. آن موقع du عدد کوچکی نشان می‌دهد و شما گیج می‌مانید.

کش فریم‌ورک. storage/framework/cache/data و storage/framework/views معمولاً ناچیزند، ولی اگر درایور کش روی فایل باشد و چیزهای بزرگ کش کنید، رشد می‌کند. پاک کردنشان بی‌خطر است:

Bash
php artisan view:clear
php artisan cache:clear

برای شمردن فایل‌های سشن و دیدن این‌که آیا تعدادشان از کنترل خارج شده:

Bash
ls -1 ~/app/storage/framework/sessions | wc -l

اگر می‌خواهید یک بار مرور کامل روی محدودیت‌های این نوع هاست داشته باشید، چک‌لیست هاست اشتراکی برای لاراول همین موارد را کنار هم گذاشته است.

لاگ نباید از وب قابل خواندن باشد#

یک نکته که موقع پر شدن دیسک معمولاً از قلم می‌افتد: محتوای لاگ حساس است. در stack trace ها ممکن است مسیر فایل‌ها، پارامترهای کوئری، توکن‌ها و گاهی مقدار متغیرها بیایند. این چیزی نیست که بخواهید عمومی باشد.

در ساختار استاندارد لاراول، storage بیرون از ریشه‌ی وب است و مشکلی نیست. ولی روی بعضی هاست‌های اشتراکی کل پروژه داخل public_html گذاشته می‌شود و آن‌وقت storage/logs از طریق مرورگر قابل دسترسی است. یک بار امتحان کنید: آدرس فایل لاگ را در مرورگر باز کنید. اگر چیزی دیدید، همان لحظه ببندیدش. در فایل .htaccess ریشه:

Apache
<FilesMatch "\.log$">
    Require all denied
</FilesMatch>

و بهتر از آن، ساختار را درست کنید تا storage اصلاً زیر ریشه‌ی وب نباشد و فقط پوشه‌ی public بیرون بماند. این کار یک بار زحمت دارد و برای همیشه یک دسته مشکل امنیتی را از سر راه برمی‌دارد.

درس اصلی#

پر شدن دیسک به‌خاطر لاگ، باگ نیست؛ نبودِ یک تصمیم است. لاراول از شما نمی‌پرسد لاگ تا کِی باید بماند، و اگر جواب ندهید جوابِ پیش‌فرض «برای همیشه» است.

سه خط در .env این تصمیم را می‌گیرد: درایور daily، سطح warning، و تعداد روز مشخص. یک کرون هفتگی هم تور ایمنی است، نه راه‌حل. و اگر همین حالا دیسکتان پر است، اول truncate بزنید تا سایت نفس بکشد، بعد بروید سراغ تنظیمات — نه برعکس.

و یک عادت که بارها به دردمان خورده: هر بار که یک فایل بی‌دلیل بزرگ می‌شود، قبل از پاک کردنش چند صد سطر آخرش را بخوانید. معمولاً همان‌جا نوشته که چه خطایی هزار بار در روز تکرار شده — و آن خطا، نه اندازه‌ی فایل، مشکل واقعی شماست.