Files
clinicpro/docs/ops-restarts.md
T

110 lines
6.9 KiB
Markdown

# 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}}' <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 شدن واقعی بلافاصله خبر داده شود (به‌جای پایش دستی لاگ):
1. در Coolify → **Notifications**، یک کانال (Telegram / Email / Discord) فعال کنید.
2. رویدادهای «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 شد» است.
### چک‌لیست رفع
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`).
مستندات: <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 در دسترس نباشد).
## ۷. اعمال تغییرات config
فایل‌های `docker/supervisord.conf`، `docker-compose.yml` و `supervisor.conf` باید آینه هم بمانند (workerهای یکسان، فلگ‌های یکسان). هر تغییری در آن‌ها فقط بعد از **Redeploy در Coolify** روی سرور اثر می‌کند.