# دیپلوی 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` |