# دیپلوی ClinicPro روی Coolify (Docker Compose)

این راهنما نحوهٔ دیپلوی بک‌اند ClinicPro (Symfony 7.4 + پنل React) را روی **Coolify** با Build Pack از نوع **Docker Compose** توضیح می‌دهد.

> همهٔ فایل‌های دیپلوی در ریشهٔ ریپو هستند: `Dockerfile`، `docker-compose.yml`، پوشهٔ `docker/` و `.env.coolify.example`.
> فایل‌های `compose.yaml`، `compose.override.yaml` و `.ddev/` مخصوص محیط لوکال (ddev) هستند و **در دیپلوی نقشی ندارند**.

---

## معماری دیپلوی

`docker-compose.yml` پنج سرویس بالا می‌آورد:

| سرویس | نقش | نکته |
|-------|-----|------|
| `app` | وب (PHP-FPM + Nginx) | دامنه به این سرویس اختصاص می‌یابد (پورت ۸۰). `RUN_INIT=1` → migration و تولید کلید JWT |
| `worker-async` | مصرف صف `async` (ارسال SMS) | `RUN_INIT=0` |
| `worker-scheduler` | مصرف `scheduler_default` (انقضای نوبت‌های پرداخت‌نشده، هر دقیقه) | `RUN_INIT=0` |
| `mariadb` | دیتابیس MariaDB 11.8 | healthcheck دارد؛ سرویس‌های اپ منتظر سالم‌شدن آن می‌مانند |
| `redis` | Messenger transport + کش/OTP | با appendonly persist می‌شود |

هر سه سرویس اپ از یک image یکسان (همان `Dockerfile`) ساخته می‌شوند و فقط `command`/`RUN_INIT` آن‌ها متفاوت است.

---

## مراحل دیپلوی

### ۱. ساخت Resource در Coolify

1. **New Resource → Public/Private Repository** و ریپوی `clinicpro` را انتخاب کن.
2. **Build Pack** را روی **Docker Compose** بگذار (نه Nixpacks).
3. در تنظیمات:
   - **Branch:** `main`
   - **Base Directory:** `/` (ریشهٔ ریپو)
   - **Docker Compose File:** `docker-compose.yml`

### ۲. اختصاص دامنه

- در سرویس `app`، **همهٔ** دامنه‌هایی که باید سرویس بگیرند را وارد کن — هم دامنهٔ API بک‌اند و هم همهٔ دامنه‌های فرانت‌اند (`nobat724.com` و همهٔ `*-nobat.ir`). Coolify لیست دامنهٔ کامادار را روی یک سرویس می‌پذیرد.
- چون کانتینر روی پورت `80` گوش می‌دهد، نیازی به افزودن پورت به دامنه نیست.
- Coolify به‌صورت خودکار برای هر دامنه TLS را از طریق Traefik (Let's Encrypt) صادر می‌کند.

> دو مفهوم را اشتباه نگیر:
> - **اختصاص دامنه در UI** = Traefik برای آن دامنه روت و گواهی TLS می‌سازد.
> - **`CORS_ALLOW_ORIGIN` / `ALLOWED_FRONTEND_HOSTS`** = سیمفونی به آن origin اجازهٔ مرورگری/پرداخت می‌دهد.
>
> یک دامنهٔ جدید معمولاً به **هر دو** نیاز دارد: هم در UI کولیفای اضافه شود، هم در `docker/frontend-domains.json` (و سپس بازتولید env). به بخش «چند دامنه فرانت‌اند» پایین مراجعه کن.

### ۳. متغیرهای محیطی

محتوای [`.env.coolify.example`](../../.env.coolify.example) را در تب **Environment Variables** وارد کن. الزامی‌ها پیش از اولین دیپلوی:

| متغیر | توضیح |
|-------|-------|
| `APP_SECRET` | `php -r "echo bin2hex(random_bytes(32));"` |
| `JWT_PASSPHRASE` | `openssl rand -hex 32` — **باید قبل از اولین استارت موجود باشد** (کلید JWT با آن ساخته می‌شود) |
| `DB_PASSWORD` | پسورد یوزر دیتابیس |
| `DB_ROOT_PASSWORD` | پسورد root مریادی‌بی |
| `APP_BASE_URL` | دامنهٔ **خودِ بک‌اند** (مثلاً `https://api.nobat724.com`) — برای callback پرداخت و URL مطلق |
| `ALLOWED_FRONTEND_HOSTS` / `CORS_ALLOW_ORIGIN` | دامنه‌های **فرانت‌اند** (چندتایی) — به بخش «چند دامنه» پایین مراجعه کن |
| `TRUSTED_PROXIES` | پیش‌فرض رنج شبکهٔ داخلی داکر (در فایل نمونه هست) |
| `API_IR_TOKEN` | توکن استعلام هویت (خالی = استعلام رد می‌شود) |

> `DATABASE_URL`، `MESSENGER_TRANSPORT_DSN` و `REDIS_URL` در خودِ compose از نام سرویس‌ها (`mariadb`/`redis`) ساخته می‌شوند؛ در UI تعریف نکن.
> کلیدهای **SMS** و **درگاه پرداخت** از DB («تنظیمات سایت») خوانده می‌شوند، نه از env.
> می‌توانی به‌جای hardcode از magic variableهای Coolify استفاده کنی، مثلاً `DB_PASSWORD=${SERVICE_PASSWORD_DB}`.

#### چند دامنه فرانت‌اند (مهم)

این بک‌اند به ده‌ها دامنهٔ شهری سرویس می‌دهد (`nobat724.com` و `*-nobat.ir`). دو متغیر باید همهٔ این دامنه‌ها را پوشش دهند:

- **`ALLOWED_FRONTEND_HOSTS`** — لیست host با کاما؛ در validate کردن host بازگشتِ پرداخت استفاده می‌شود (تطبیق دقیق در `PaymentController::isAllowedFrontend`).
- **`CORS_ALLOW_ORIGIN`** — یک regex واحد (nelmio با `origin_regex: true`) که فقط `https` و دقیقاً همان host‌ها را می‌پذیرد.

هر دو مقدار به‌صورت **خودکار** از فایل [`docker/frontend-domains.json`](../../docker/frontend-domains.json) تولید می‌شوند. برای افزودن یا حذف یک دامنه:

```bash
# ۱) یک رکورد به آرایهٔ "domains" در docker/frontend-domains.json اضافه/حذف کن، مثلاً:
#    { "domain": "newcity-nobat.ir", "label": "شهر جدید" }
# ۲) مقادیر جدید را تولید کن:
ddev exec php docker/gen-cors-env.php       # لوکال
#   یا روی سرور داخل کانتینر app:
php docker/gen-cors-env.php
# ۳) خروجی (CORS_ALLOW_ORIGIN و ALLOWED_FRONTEND_HOSTS) را در Coolify جایگزین کن و دوباره deploy کن
```

> فیلد `label` فقط برای خوانایی است و در تولید env استفاده نمی‌شود؛ فقط `domain` مهم است.

> `payment_allowed_frontend_hosts` در «تنظیمات سایت» (DB) بر مقدار env اولویت دارد؛ اگر آن را در DB ست کرده‌ای، آن مرجع است.

### ۴. Persistent Storage (حیاتی)

این volumeها در compose تعریف شده‌اند و Coolify آن‌ها را persist می‌کند:

| Volume | مسیر | چرا مهم است |
|--------|------|-------------|
| `jwt_keys` | `/app/config/jwt` | **مهم‌ترین.** کلید JWT بین دیپلوی‌ها باید ثابت بماند؛ در غیر این صورت هر دیپلوی همهٔ توکن‌ها را باطل و همهٔ کاربران را logout می‌کند |
| `uploads_public` | `/app/public/uploads` | فایل‌های آپلودی عمومی |
| `uploads_var` | `/app/var/uploads` | فایل‌های آپلودی خصوصی |
| `mariadb_data` | `/var/lib/mysql` | دادهٔ دیتابیس |
| `redis_data` | `/data` | پایداری Redis |

> مطمئن شو در Coolify این volumeها به‌صورت **named volume** (نه ephemeral) باقی می‌مانند.

### ۵. اولین دیپلوی و ساخت ادمین

1. **Deploy** را بزن. سرویس `app` هنگام استارت به‌صورت خودکار:
   - کلید JWT می‌سازد (اگر روی volume نباشد)،
   - کش prod را warm می‌کند،
   - migrationها را با `--all-or-nothing` اجرا می‌کند.
2. بعد از سالم‌شدن سرویس‌ها، از **Terminal** سرویس `app` در Coolify، ادمین اولیه را بساز:
   ```bash
   php bin/console app:create-admin
   ```

---

## نکات عملیاتی

- **Worker‌ها:** اگر `worker-scheduler` بالا نباشد، نوبت‌های رزرو ولی پرداخت‌نشده **آزاد نمی‌شوند**. اگر `worker-async` بالا نباشد، **SMS ارسال نمی‌شود**. هر دو در همین compose مدیریت می‌شوند و با `restart: unless-stopped` خودکار بازمی‌گردند.
- **Migration در دیپلوی‌های بعدی:** فقط سرویس `app` (با `RUN_INIT=1`) migration اجرا می‌کند تا بین سرویس‌ها race رخ ندهد. هر دیپلوی، migrationهای جدید را اعمال می‌کند.
- **Health check:** سرویس `app` با یک fsockopen روی پورت 80 سالم‌بودن خود را گزارش می‌دهد.
- **Trusted Proxies:** مقدار `TRUSTED_PROXIES` به Symfony می‌گوید به هدرهای `X-Forwarded-*` از Traefik اعتماد کند تا `https` و IP واقعی کلاینت درست تشخیص داده شوند. در محیط لوکال (ddev) این متغیر تنظیم نمی‌شود و مقدار پیش‌فرض خالی است.
- **بدون شبکهٔ سفارشی:** طبق توصیهٔ Coolify، در compose هیچ `networks:` سفارشی تعریف نشده تا روتینگ Traefik پایدار بماند.

---

## رفع اشکال

| نشانه | علت محتمل | راه‌حل |
|-------|-----------|--------|
| همهٔ کاربران بعد از دیپلوی logout می‌شوند | volume `jwt_keys` persist نشده | بررسی named volume بودن آن |
| خطای اتصال به دیتابیس هنگام استارت | `app` قبل از سالم‌شدن `mariadb` بالا آمده | `depends_on: condition: service_healthy` این را پوشش می‌دهد؛ صبر کن یا لاگ `mariadb` را ببین |
| پنل admin سفید/بدون استایل | دارایی‌های `public/build` ساخته نشده | بررسی موفقیت stage `assets` در لاگ build (`yarn build`) |
| تولید کلید JWT شکست می‌خورد | `JWT_PASSPHRASE` تنظیم نشده | متغیر را در Coolify ست کن و دوباره deploy کن |
| تصاویر/فایل‌های آپلودی بعد از ری‌دیپلوی ناپدید می‌شوند | volumeهای uploads persist نشده | بررسی `uploads_public` / `uploads_var` |
