# 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 شده؟ ```bash # وضعیت و uptime کانتینرها — Status باید «Up X hours (healthy)» باشد docker ps --format 'table {{.Names}}\t{{.Status}}' # تعداد ری‌استارت واقعی و زمان آخرین start کانتینر docker inspect --format '{{.RestartCount}} {{.State.StartedAt}}' # رویدادهای 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 `. ## ۴. اطلاع‌رسانی خودکار در Coolify برای اینکه down شدن واقعی بلافاصله خبر داده شود (به‌جای پایش دستی لاگ): 1. در Coolify → **Notifications**، یک کانال (Telegram / Email / Discord) فعال کنید. 2. رویدادهای «container stopped / unhealthy» و «deployment failed» را روشن کنید. مستندات: - - ## ۵. تمدید گواهی 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 شد» است. ### چک‌لیست رفع 1. در Coolify → resource اپ → تب **Domains**: دامنه `clinic-pro.ir` (و `www.clinic-pro.ir` اگر DNS دارد) باید دقیقاً با `https://` روی سرویس `app` ثبت باشد. دامنه‌ای که DNS آن به سرور اشاره می‌کند ولی در Coolify ثبت نیست، دقیقاً همین الگوی 404 را می‌سازد. 2. تاریخ انقضای گواهی فعلی را چک کنید: ```bash echo | openssl s_client -connect clinic-pro.ir:443 -servername clinic-pro.ir 2>/dev/null \ | openssl x509 -noout -dates ``` 3. لاگ پراکسی را برای خطاهای acme ببینید: Coolify → Servers → Proxy → Logs (جستجوی `acme`). مستندات: ## ۶. 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`](collect-diagnostics.sh) را روی **هاست** کپی و اجرا کنید (همه شواهد را یک‌جا جمع می‌کند): ```bash scp docs/collect-diagnostics.sh root@SERVER:/tmp/ && ssh root@SERVER sh /tmp/collect-diagnostics.sh ``` یا دستورهای کلیدی به‌صورت دستی: ```bash # آیا کانتینر با OOM کشته شده؟ docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}} {{.State.FinishedAt}}' # ردپای 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 ` را ببینید | | هیچ‌کدام | فرضیه حافظه/دیسک رد شد | خروجی کامل اسکریپت + ۵۰ خط آخر لاگ قبل از 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) 0. **اول Redeploy** — همه فیکس‌های این repo (php.ini، zz-pool.conf، supervisord.conf) فقط با build/deploy جدید اعمال می‌شوند؛ و در env واقعی Coolify به `MESSENGER_TRANSPORT_DSN` پارامتر `?stream_max_entries=20000` را اضافه کنید (نمونه در `.env.coolify.example`). 1. در Coolify برای resource اپ **Memory Limit** بگذارید (مثلاً 1G) تا در بدترین حالت فقط همان کانتینر ری‌استارت شود، نه کل سرور. 2. اگر سرور swap ندارد، 1-2G swap اضافه کنید: `fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile`. 3. اسکن‌های بات (درخواست‌های `GET /xxx.php → 404` پشت‌سرهم) توسط nginx مستقیم 404 می‌شوند و به Symfony نمی‌رسند — عامل مرگ نیستند، فقط نویز لاگ. 4. **Redis بدون سقف حافظه**: کش اپ (`cache.adapter.redis`) و صف messenger هر دو روی یک Redis resource هستند و Redis پیش‌فرض `maxmemory=0` (رشد بی‌نهایت) دارد — یکی از عوامل خورده شدن تدریجی RAM. روی Redis resource در Coolify ست کنید: ``` maxmemory 256mb maxmemory-policy volatile-lru ``` ⚠️ `allkeys-lru` ممنوع — پیام‌های صف messenger (بدون TTL) را حذف می‌کند؛ `volatile-lru` فقط کلیدهای کش (TTL‌دار) را evict می‌کند. در DSN صف هم `?stream_max_entries=20000` بگذارید (نمونه در `.env.coolify.example`). 5. **چرخش لاگ Docker**: اگر daemon محدودیت ندارد، فایل‌های `*-json.log` تا پر شدن دیسک رشد می‌کنند. چک: `df -h` و `du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail`. فیکس در `/etc/docker/daemon.json`: ```json { "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "3" } } ``` (بعدش `systemctl restart docker` — کانتینرها باید recreate شوند تا اعمال شود.) 6. **جدول `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** روی سرور اثر می‌کند.