feat: Implement Docker-based deployment for ClinicPro on Coolify

- Added Dockerfile for multi-stage build including PHP, Node.js, and Nginx.
- Created docker-compose.coolify.yaml for service orchestration with app, workers, MariaDB, and Redis.
- Introduced entrypoint.sh for initialization tasks like JWT key generation and database migrations.
- Configured Nginx with default.conf for handling requests and routing to PHP-FPM.
- Added php.ini with production settings and opcache configuration.
- Set up supervisord.conf to manage PHP-FPM and Nginx processes.
- Created frontend-domains.json for managing allowed frontend domains.
- Added gen-cors-env.php script to generate CORS environment variables from frontend domains.
- Updated framework.yaml to configure trusted proxies and headers.
- Created .dockerignore to exclude unnecessary files from the Docker context.
- Added .env.coolify.example for environment variable configuration.
- Documented deployment steps and troubleshooting in coolify.md.
This commit is contained in:
hamed
2026-06-25 21:27:28 +03:30
parent dfda265af4
commit cffc88db05
13 changed files with 940 additions and 147 deletions
+136
View File
@@ -0,0 +1,136 @@
# دیپلوی ClinicPro روی Coolify (Docker Compose)
این راهنما نحوهٔ دیپلوی بک‌اند ClinicPro (Symfony 7.4 + پنل React) را روی **Coolify** با Build Pack از نوع **Docker Compose** توضیح می‌دهد.
> همهٔ فایل‌های دیپلوی در ریشهٔ ریپو هستند: `Dockerfile`، `docker-compose.coolify.yaml`، پوشهٔ `docker/` و `.env.coolify.example`.
> فایل‌های `compose.yaml`، `compose.override.yaml` و `.ddev/` مخصوص محیط لوکال (ddev) هستند و **در دیپلوی نقشی ندارند**.
---
## معماری دیپلوی
`docker-compose.coolify.yaml` پنج سرویس بالا می‌آورد:
| سرویس | نقش | نکته |
|-------|-----|------|
| `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.coolify.yaml`
### ۲. اختصاص دامنه
- در سرویس `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` |