Files
clinicpro/docs/deploy/coolify.md
T

137 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# دیپلوی 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` |