13 KiB
Runbook — تشخیص «ریاستارت» سرور: recycle عادی یا خرابی واقعی؟
این سند برای وقتی است که در لاگهای production (Coolify) خطوطی مثل exited: worker-... دیده میشود و بهنظر میرسد «سرور مدام ریاستارت میشود».
خلاصه یکخطی
خروج ساعتیِ workerها با exit status 0; expected طراحیشده و بیضرر است. خرابی واقعی یعنی exit غیرصفر، FATAL، یا قطع شدن پاسخ /health.
۱. چرا workerها هر ساعت restart میشوند؟
هر دو consumer پیام (async و scheduler) با این فلگها اجرا میشوند:
messenger:consume async --time-limit=3600 --memory-limit=128M
messenger:consume scheduler_default --time-limit=3600
--time-limit=3600→ worker بعد از ۱ ساعت خودش با exit code 0 خارج میشود.- supervisord (یا
restart: unless-stoppedدر compose) بلافاصله دوباره آن را بالا میآورد (زیر ۱ ثانیه). - هدف: جلوگیری از نشت حافظه PHP در پروسههای طولانی و تازهکردن اتصالهای کهنه به MariaDB/Redis. الگوی استاندارد و توصیهشده Symfony Messenger است.
به همین دلیل این لاگ عادی است و هیچ اقدامی لازم ندارد:
INFO exited: worker-scheduler (exit status 0; expected)
INFO spawned: 'worker-scheduler' with pid 1464
INFO success: worker-scheduler entered RUNNING state, process has stayed up for > than 1 seconds
کلیدواژه expected یعنی supervisor از قبل میدانست این exit برنامهریزیشده است.
۲. نشانههای خرابی واقعی
اگر هر یک از اینها را دیدید، مشکل واقعی است:
| نشانه در لاگ | معنی |
|---|---|
exited: ... (exit status 1) یا هر عدد غیرصفر |
worker با خطا مرده |
entered FATAL state, too many start retries too quickly |
worker بالا نمیآید (مثلاً DB/Redis در دسترس نیست) |
گپ چند دقیقهای در لاگهای GET /health ... 200 |
وبسرور واقعاً پاسخ نمیداده |
GET /health با status غیر از 200 |
اپ boot نمیشود |
۳. دستورهای تشخیص روی سرور
سؤال اصلی: آیا خود کانتینر ریاستارت شده یا فقط پروسه worker داخل آن recycle شده؟
# وضعیت و uptime کانتینرها — Status باید «Up X hours (healthy)» باشد
docker ps --format 'table {{.Names}}\t{{.Status}}'
# تعداد ریاستارت واقعی و زمان آخرین start کانتینر
docker inspect --format '{{.RestartCount}} {{.State.StartedAt}}' <container-name>
# رویدادهای restart/die در ۲۴ ساعت گذشته
docker events --since 24h --filter event=restart --filter event=die
تفسیر:
RestartCount= 0 وStartedAtقدیمی (مثلاً چند روز پیش) → هیچ ریاستارت واقعیای رخ نداده؛ فقط recycle داخلی worker است.RestartCountبالا یاStartedAtتازه بدون deploy → کانتینر واقعاً crash/restart میشود؛ لاگهای قبل از مرگ را ببینید:docker logs --tail 200 <container>.
۴. اطلاعرسانی خودکار در Coolify
برای اینکه down شدن واقعی بلافاصله خبر داده شود (بهجای پایش دستی لاگ):
- در Coolify → Notifications، یک کانال (Telegram / Email / Discord) فعال کنید.
- رویدادهای «container stopped / unhealthy» و «deployment failed» را روشن کنید.
مستندات:
- https://coolify.io/docs/knowledge-base/notifications
- https://coolify.io/docs/knowledge-base/health-checks
۵. تمدید گواهی TLS (ACME) — مهمترین ریسک down شدن واقعی
علامت مشکل
در access log کانتینر app:
GET /.well-known/acme-challenge/xxxx HTTP/1.1" 404 ... "Let's Encrypt validation server"
چالش HTTP-01 لتسانکریپت باید توسط Traefik (پراکسی Coolify) پاسخ داده شود و اصلاً نباید به کانتینر app برسد. رسیدن آن به app و گرفتن 404 یعنی صدور/تمدید گواهی برای آن دامنه دارد شکست میخورد. اگر رها شود، بعد از انقضای گواهی فعلی، سایت با خطای TLS از دسترس خارج میشود — و این همان «سرور down شد» است.
چکلیست رفع
-
در Coolify → resource اپ → تب Domains: دامنه
clinic-pro.ir(وwww.clinic-pro.irاگر DNS دارد) باید دقیقاً باhttps://روی سرویسappثبت باشد. دامنهای که DNS آن به سرور اشاره میکند ولی در Coolify ثبت نیست، دقیقاً همین الگوی 404 را میسازد. -
تاریخ انقضای گواهی فعلی را چک کنید:
echo | openssl s_client -connect clinic-pro.ir:443 -servername clinic-pro.ir 2>/dev/null \ | openssl x509 -noout -dates -
لاگ پراکسی را برای خطاهای acme ببینید: Coolify → Servers → Proxy → Logs (جستجوی
acme).
مستندات: https://coolify.io/docs/knowledge-base/proxy/traefik/overview
۶. Healthcheckها — رفتار انتظاری
- کانتینر app:
docker/healthcheck.shمسیر واقعی/healthرا از داخل میزند. پارامترها درDockerfileوdocker-compose.ymlیکساناند:interval=15s, timeout=5s, retries=5, start_period=60s. یعنی برای unhealthy شدن باید ۵ بار پیاپی (~۷۵ ثانیه) fail شود. - کانتینرهای worker (حالت compose): healthcheck فقط وجود پروسه
messenger:consumeرا باps | grepچک میکند (interval=30s, retries=3). در لحظه exit ساعتیِ worker ممکن است یک چک fail شود — بیاهمیت است؛ برای unhealthy شدن ۳ شکست پیاپی (~۹۰ ثانیه) لازم است و worker در همان چند ثانیه اول برمیگردد. unhealthy شدن worker فقط وقتی رخ میدهد که consumer واقعاً بالا نیاید (مثلاً Redis در دسترس نباشد).
۷. down شدن بعد از چند ساعت — کمبود حافظه (OOM)
الگو: workerها چند بار recycle عادی میشوند و بعد از چند ساعت کل سرویس down میشود. مظنون اول: پر شدن حافظه سرور و کشته شدن پروسهها توسط OOM-killer.
تأیید روی سرور — اسکریپت آماده
فایل docs/collect-diagnostics.sh را روی هاست کپی و اجرا کنید (همه شواهد را یکجا جمع میکند):
scp docs/collect-diagnostics.sh root@SERVER:/tmp/ && ssh root@SERVER sh /tmp/collect-diagnostics.sh
یا دستورهای کلیدی بهصورت دستی:
# آیا کانتینر با OOM کشته شده؟
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}} {{.State.FinishedAt}}' <container>
# ردپای OOM-killer در کرنل (مهمترین دستور)
journalctl -k --since "24 hours ago" | grep -i -E "oom|killed process"
# مصرف لحظهای هر کانتینر
docker stats --no-stream
# حافظه و swap کل سرور
free -h
جدول تفسیر خروجی اسکریپت
| شاهد در خروجی | تشخیص | اقدام |
|---|---|---|
بخش ۱: Out of memory: Killed process ... php-fpm |
نشت حافظه fpm | فیکسهای pm.max_requests/memory_limit این repo + redeploy کافی است |
بخش ۱: Killed process ... mariadbd یا redis |
فشار کل RAM سرور | Redis maxmemory + Memory Limit در Coolify + swap |
بخش ۴: OOMKilled=true |
سقف حافظه خود کانتینر | سقف را بالاتر ببرید یا مصرف را کم کنید |
بخش ۳: دیسک ≥ ۹۰٪ یا *-json.log چند GB |
دیسک پر | log rotation در daemon.json + پاکسازی |
بخش ۷: maxmemory: 0 و used_memory بزرگ/رشدکننده |
Redis بیسقف | maxmemory 256mb + maxmemory-policy volatile-lru |
بخش ۶: memory_limit هنوز 1024M |
image قدیمی در حال اجراست | Redeploy نشده — دوباره deploy کنید |
لاگ worker: [critical] ... "socket error on read socket" (Connection.php) |
اتصال Redis وسط کار قطع شده — تقریباً همیشه یعنی خود Redis مرده/ریاستارت شده (OOM) | بخش ۷ اسکریپت: uptime_in_seconds کوچک Redis = ریاستارت شده؛ maxmemory 256mb + volatile-lru بگذارید و docker logs <redis> را ببینید |
| هیچکدام | فرضیه حافظه/دیسک رد شد | خروجی کامل اسکریپت + ۵۰ خط آخر لاگ قبل از down را برای تحلیل بفرستید |
چرا این اتفاق میافتاد (و فیکس اعمالشده)
memory_limitهر request برابر 1024M بود → به 256M کاهش یافت (docker/php/php.ini).- pool فقط
clear_env=noداشت؛pm.max_requestsتنظیم نشده بود → پروسههای php-fpm هرگز recycle نمیشدند و نشتهای کوچک حافظه در طول ساعتها جمع میشد. حالاpm.max_requests=500+ سقفpm.max_children=8(docker/php/zz-pool.conf). - workerهای messenger از قبل با
--time-limit=3600و--memory-limit=128Mمحافظت میشدند — مشکل از آنها نبود.
اقدامات تکمیلی روی سرور (خارج از repo)
- اول Redeploy — همه فیکسهای این repo (php.ini، zz-pool.conf، supervisord.conf) فقط با build/deploy جدید اعمال میشوند؛ و در env واقعی Coolify به
MESSENGER_TRANSPORT_DSNپارامتر?stream_max_entries=20000را اضافه کنید (نمونه در.env.coolify.example). - در Coolify برای resource اپ Memory Limit بگذارید (مثلاً 1G) تا در بدترین حالت فقط همان کانتینر ریاستارت شود، نه کل سرور.
- اگر سرور swap ندارد، 1-2G swap اضافه کنید:
fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile. - اسکنهای بات (درخواستهای
GET /xxx.php → 404پشتسرهم) توسط nginx مستقیم 404 میشوند و به Symfony نمیرسند — عامل مرگ نیستند، فقط نویز لاگ. - Redis بدون سقف حافظه: کش اپ (
cache.adapter.redis) و صف messenger هر دو روی یک Redis resource هستند و Redis پیشفرضmaxmemory=0(رشد بینهایت) دارد — یکی از عوامل خورده شدن تدریجی RAM. روی Redis resource در Coolify ست کنید:⚠️maxmemory 256mb maxmemory-policy volatile-lruallkeys-lruممنوع — پیامهای صف messenger (بدون TTL) را حذف میکند؛volatile-lruفقط کلیدهای کش (TTLدار) را evict میکند. در DSN صف هم?stream_max_entries=20000بگذارید (نمونه در.env.coolify.example). - چرخش لاگ Docker: اگر daemon محدودیت ندارد، فایلهای
*-json.logتا پر شدن دیسک رشد میکنند. چک:df -hوdu -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail. فیکس در/etc/docker/daemon.json:(بعدش{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "3" } }systemctl restart docker— کانتینرها باید recreate شوند تا اعمال شود.) - جدول
messenger_messages(failed): پیامهای شکستخورده (مثلاً SMS بعد از ۳ retry) برای همیشه در DB میمانند. هر چند وقت:php bin/console messenger:failed:showو پاکسازی باmessenger:failed:remove.
۸. اعمال تغییرات config
فایلهای docker/supervisord.conf، docker-compose.yml و supervisor.conf باید آینه هم بمانند (workerهای یکسان، فلگهای یکسان). هر تغییری در آنها فقط بعد از Redeploy در Coolify روی سرور اثر میکند.