# تشخیص عمیق down شدن سرور بعد از ~۱۰ سیکل + وریفای و تکمیل فیکس‌های پایداری ## زمینه روی production (Coolify، تک‌کانتینر با supervisord: nginx + php-fpm + دو messenger worker) workerها هر ساعت با `--time-limit=3600` و exit code 0 عمداً recycle می‌شوند — این عادی است. مشکل واقعی: **بعد از حدود ۱۰ سیکل (~۱۰ ساعت) کل سرویس down می‌شود**. در لاگ‌های ارائه‌شده هیچ crash دیده نمی‌شود (`exit status 0; expected`، `/health` همیشه 200، اسکن‌های بات `GET /*.php` توسط nginx مستقیم 404 می‌شوند و به Symfony نمی‌رسند) — یعنی قاتل بیرون از لاگ اپ است. فرضیه اصلی (به ترتیب احتمال): **فشار تدریجی حافظه تا OOM** و سپس **پر شدن دیسک**: 1. php-fpm قبلاً بدون `pm.max_requests` بود → پروسه‌ها هرگز recycle نمی‌شدند و نشت حافظه ساعت‌به‌ساعت جمع می‌شد؛ `memory_limit=1024M` هم به یک request اجازه می‌داد 1GB بخورد. 2. Redis (کش اپ + صف messenger، یک instance مشترک) پیش‌فرض `maxmemory=0` → رشد بی‌نهایت. 3. لاگ‌های docker (json-file) بدون `max-size` → پر شدن تدریجی دیسک. بخشی از فیکس‌ها قبلاً در repo اعمال شده؛ این پرامپت آن‌ها را **وریفای** می‌کند، یک **اسکریپت جمع‌آوری شواهد** برای اجرای روی سرور می‌سازد، و چک‌لیست تنظیمات سمت Coolify را نهایی می‌کند. ## مشکل / هدف - تشخیص قطعی و مبتنی بر شواهد (نه حدس): OOM؟ دیسک؟ یا چیز دیگر؟ - اطمینان از اینکه همه فیکس‌های پایداری در repo حاضرند (اگر revert شده‌اند، دوباره اعمال شوند). - تحویل یک اسکریپت آماده که کاربر روی سرور اجرا کند و خروجی‌اش تشخیص را قطعی کند. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `docker/php/php.ini` | باید `memory_limit = 256M` باشد (فیکس قبلی؛ ممکن است revert شده باشد — وریفای کن) | | `docker/php/zz-pool.conf` | باید بلوک `pm = dynamic … pm.max_requests = 500` را داشته باشد | | `docker/supervisord.conf` | هر دو worker بدون `-v`، هر دو با `--memory-limit=128M` | | `docker-compose.yml` | آینه supervisord (حالت compose) | | `supervisor.conf` | آینه (نسخه Liara) | | `.env.coolify.example` | `MESSENGER_TRANSPORT_DSN` نمونه باید `?stream_max_entries=20000` داشته باشد | | `docs/ops-restarts.md` | runbook — بخش‌های ۷ (OOM) و ۸ باید موجود باشند | | `docs/collect-diagnostics.sh` | **جدید** — اسکریپت جمع‌آوری شواهد روی سرور | ## وضعیت فعلی وضعیت انتظاری بعد از فیکس‌های قبلی (`docker/php/zz-pool.conf`): ```ini [www] clear_env = no pm = dynamic pm.max_children = 8 pm.start_servers = 2 pm.min_spare_servers = 2 pm.max_spare_servers = 4 pm.max_requests = 500 ``` و `docker/php/php.ini`: ```ini memory_limit = 256M ``` ⚠️ کاربر ممکن است `memory_limit` را به 1024M برگردانده باشد — وضعیت فعلی فایل را بخوان و اگر برگشته، با ذکر دلیل (۸ پروسه × 1024M = 8GB سقف نظری روی VPS کوچک) دوباره 256M کن، مگر اینکه کاربر صراحتاً 1024M خواسته باشد — در آن صورت دست نزن و فقط در گزارش ذکر کن. ## وظایف ### ۱. وریفای فیکس‌های repo (idempotent) هر ۷ فایل جدول بالا را چک کن؛ هر موردی که غایب/revert شده را دوباره اعمال کن: ```bash grep -n "memory_limit" docker/php/php.ini # انتظار: 256M grep -n "pm.max_requests" docker/php/zz-pool.conf # انتظار: 500 grep -n "consume" docker/supervisord.conf docker-compose.yml supervisor.conf # انتظار: هیچ ` -v` ای نباشد؛ هر دو worker دارای --time-limit=3600؛ هر دو --memory-limit=128M grep -n "stream_max_entries" .env.coolify.example # انتظار: 20000 grep -n "OOM\|maxmemory" docs/ops-restarts.md # بخش ۷ موجود ``` سه فایل supervisor/compose باید آینه هم بمانند. ### ۲. ساخت `docs/collect-diagnostics.sh` — اسکریپت شواهد اسکریپت shell (POSIX sh، بدون وابستگی خاص) که کاربر روی **هاست سرور** (نه داخل کانتینر) اجرا می‌کند و همه شواهد لازم را یک‌جا چاپ می‌کند: ```sh #!/bin/sh # ClinicPro production diagnostics — run ON THE HOST as root (or with docker access). # Usage: sh collect-diagnostics.sh [container-name-or-id] set -u C="${1:-$(docker ps --format '{{.Names}}' | grep -i -m1 clinic || true)}" echo "=== 1. OOM evidence (kernel, last 48h) ===" journalctl -k --since "48 hours ago" 2>/dev/null | grep -i -E "oom|killed process" | tail -20 \ || dmesg | grep -i -E "oom|killed process" | tail -20 echo "=== 2. Memory / swap now ===" free -h echo "=== 3. Disk ===" df -h | head -10 du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -5 echo "=== 4. Container state ===" docker ps -a --format 'table {{.Names}}\t{{.Status}}' [ -n "$C" ] && docker inspect --format \ 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} RestartCount={{.RestartCount}} StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' "$C" echo "=== 5. Live memory per container ===" docker stats --no-stream echo "=== 6. php-fpm inside app container ===" [ -n "$C" ] && docker exec "$C" sh -c 'php -i 2>/dev/null | grep -E "^memory_limit"; ps -o rss,args 2>/dev/null | grep -E "php-fpm|messenger" | grep -v grep' echo "=== 7. Redis memory (set host/port/pass if needed) ===" R="$(docker ps --format '{{.Names}}' | grep -i -m1 redis || true)" [ -n "$R" ] && docker exec "$R" redis-cli info memory 2>/dev/null | grep -E "used_memory_human|maxmemory_human|maxmemory_policy" echo "=== 8. Docker log driver config ===" docker info --format '{{.LoggingDriver}}' cat /etc/docker/daemon.json 2>/dev/null || echo "(no daemon.json)" echo "=== done — paste this whole output back for diagnosis ===" ``` اجرایی‌اش کن (`chmod +x`). در انتهای `docs/ops-restarts.md` یک خط ارجاع به این اسکریپت اضافه کن. ### ۳. جدول تفسیر شواهد در runbook به `docs/ops-restarts.md` (بخش ۷) جدول تصمیم اضافه کن: | شاهد در خروجی اسکریپت | تشخیص | اقدام | |---|---|---| | بخش ۱: `Out of memory: Killed process ... php-fpm` | نشت fpm | فیکس‌های pm/memory_limit + redeploy کافی است | | بخش ۱: `Killed process ... mariadbd` یا `redis` | فشار کل RAM | Redis maxmemory + Coolify memory limit + swap | | بخش ۴: `OOMKilled=true` | سقف حافظه کانتینر | سقف را بالاتر ببر یا مصرف را کم کن | | بخش ۳: دیسک ≥ 90% یا json.log چند GB | دیسک پر | log rotation daemon.json + پاکسازی | | بخش ۷: `maxmemory:0` و `used_memory` رشد کرده | Redis بی‌سقف | `maxmemory 256mb` + `maxmemory-policy volatile-lru` | | هیچ‌کدام | فرضیه رد شد | خروجی کامل + لاگ ۵۰ خط آخر قبل از down برای تحلیل بعدی | ### ۴. چک‌لیست تنظیمات سمت Coolify (فقط مستندسازی — قابل اجرا از repo نیست) در همان بخش runbook، چک‌لیست کوتاه: 1. Redeploy بعد از merge این تغییرات (configها فقط با image جدید اعمال می‌شوند). 2. env واقعی Coolify: به `MESSENGER_TRANSPORT_DSN` پارامتر `?stream_max_entries=20000` اضافه شود. 3. Redis resource: `maxmemory 256mb` + `maxmemory-policy volatile-lru` (نه `allkeys-lru` — پیام‌های صف TTL ندارند و evict می‌شوند). 4. App resource → Memory Limit = 1G. 5. `/etc/docker/daemon.json`: `{"log-driver":"json-file","log-opts":{"max-size":"20m","max-file":"3"}}` + restart docker. 6. اگر `free -h` نشان داد swap صفر است: 2G swap بساز. 7. Coolify Notifications برای container stop/unhealthy فعال شود. ## نکات مهم - هیچ تغییری در رفتار workerها نده — recycle ساعتی عمدی است؛ فقط `--memory-limit=128M` باید روی هر دو باشد. - این پرامپت PHP/React لمس نمی‌کند → migration و `docs/api/` لازم ندارد؛ فقط configهای deploy + `docs/`. - اسکریپت باید بدون bash-ism باشد (`sh` خالص) چون روی هاست Ubuntu اجرا می‌شود نه داخل کانتینر alpine. - بعد از هر تغییر config، سه فایل supervisord/compose/liara آینه بمانند (کامنت بالای فایل‌ها همین را می‌گوید). - تست repo-side: `docker compose -f docker-compose.yml config -q` (فقط warning env، بدون خطا) و `sh -n docs/collect-diagnostics.sh` برای syntax اسکریپت.