# آماده‌سازی پروژه ClinicPro برای دیپلوی روی Coolify با Docker Compose

## زمینه

پروژه روی **Coolify** دیپلوی می‌شود، اما به‌جای Build Pack پیش‌فرض (Nixpacks)، از **Docker Compose** به‌عنوان build pack استفاده می‌کنیم — یعنی یک `docker-compose.yaml` در ریشهٔ پروژه «single source of truth» است و Coolify آن را اجرا می‌کند (طبق https://coolify.io/docs/builds/packs/docker-compose).

این یعنی ما باید **همهٔ image‌ها و سرویس‌ها را خودمان تعریف کنیم**: یک Dockerfile مرحله‌ای (multi-stage) برای ساخت اپ، و یک compose که اپ + worker‌ها را بالا بیاورد. هیچ‌کدام از این فایل‌ها الان وجود ندارند.

**وضعیت فعلی پروژه:**
- Symfony 7.4 / PHP ≥8.2 / Doctrine.
- دیتابیس **MariaDB/MySQL** (نه PostgreSQL — توجه: `compose.yaml` موجود از postgres است ولی آن مخصوص ddev و نامرتبط است؛ پروژهٔ واقعی روی MariaDB 11.8 با `DATABASE_URL=mysql://...` کار می‌کند).
- فرانت‌اند **React 19 + Webpack Encore** که باید با `yarn build` کامپایل شود → خروجی در `public/build/`.
- کلیدهای **JWT** (lexik) در `config/jwt/*.pem`، در `.gitignore`.
- دو **worker** Messenger:
  - `messenger:consume async` → ارسال SMS
  - `messenger:consume scheduler_default` → انقضای نوبت‌های پرداخت‌نشده (هر ۱ دقیقه؛ transport `schedule://default`)
- `public/index.php` نقطهٔ ورود است. کامند ساخت ادمین: `app:create-admin`.

**نکتهٔ مهم دربارهٔ ddev:** فایل‌های `compose.yaml`، `compose.override.yaml` و پوشهٔ `.ddev/` مخصوص محیط لوکال هستند و **نباید دست بخورند**. فایل دیپلوی Coolify باید نام متفاوتی داشته باشد (`docker-compose.coolify.yaml`) تا با ddev تداخل نکند؛ در Coolify UI همین فایل را به‌عنوان Compose file مشخص می‌کنیم.

## هدف

ساخت یک image تولیدی کامل (PHP-FPM + Nginx + دارایی‌های build‌شدهٔ فرانت‌اند) و یک `docker-compose.coolify.yaml` که روی Coolify با Docker Compose build pack بالا بیاید — شامل اپ وب، دو worker، و اتصال به سرویس‌های MariaDB و Redis. **بدون تغییر منطق برنامه.**

## فایل‌های مرتبط

| فایل | نقش | وضعیت |
|------|-----|-------|
| `Dockerfile` | image مرحله‌ای: composer + yarn build → runtime PHP-FPM + Nginx | **باید ساخته شود** |
| `docker-compose.coolify.yaml` | تعریف سرویس‌های app/worker برای Coolify | **باید ساخته شود** |
| `docker/nginx/default.conf` | کانفیگ Nginx برای Symfony (`public/index.php`) | **باید ساخته شود** |
| `docker/php/php.ini` | تنظیمات production PHP (upload size, memory) | **باید ساخته شود** |
| `docker/entrypoint.sh` | migration + تولید JWT key هنگام استارت | **باید ساخته شود** |
| `docker/supervisord.conf` | اجرای php-fpm + nginx در یک کانتینر | **باید ساخته شود** |
| `.dockerignore` | حذف node_modules/vendor/var از build context | **باید ساخته شود** |
| `config/packages/framework.yaml` | افزودن `trusted_proxies` / `trusted_headers` | باید ویرایش شود |
| `.env.coolify.example` | فهرست env برای داشبورد Coolify | **باید ساخته شود** |
| `docs/deploy/coolify.md` | راهنمای گام‌به‌گام | **باید ساخته شود** |

## وظایف

### ۱. ساخت `Dockerfile` چندمرحله‌ای

سه stage: یکی برای وابستگی‌های PHP (composer)، یکی برای build فرانت‌اند (node/yarn)، و stage نهایی runtime.

```dockerfile
# ---------- Stage 1: PHP vendor (composer) ----------
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock symfony.lock ./
# نصب بدون اسکریپت‌ها (kernel هنوز کامل کپی نشده)
RUN composer install --no-dev --no-scripts --no-interaction --prefer-dist --optimize-autoloader

# ---------- Stage 2: Frontend assets (yarn/encore) ----------
FROM node:20-alpine AS assets
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY webpack.config.js postcss.config.js tsconfig.json ./
COPY assets ./assets
# vendor لازم است چون encore به برخی bundleها رجوع می‌کند؛ در صورت نیاز کپی کن
COPY --from=vendor /app/vendor ./vendor
COPY public ./public
RUN yarn build   # خروجی → public/build

# ---------- Stage 3: Runtime (PHP-FPM + Nginx) ----------
FROM php:8.2-fpm-alpine AS runtime
RUN apk add --no-cache nginx supervisor icu-dev oniguruma-dev $PHPIZE_DEPS \
 && docker-php-ext-install pdo_mysql intl opcache \
 && apk del $PHPIZE_DEPS
# (در صورت نیاز redis extension: pecl install redis && docker-php-ext-enable redis)

WORKDIR /app
COPY . .
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build

COPY docker/php/php.ini /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh \
 && mkdir -p var/cache var/log var/uploads public/uploads config/jwt \
 && chown -R www-data:www-data var public/uploads config/jwt

EXPOSE 80
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
```

**نکات:**
- نسخهٔ PHP باید `8.2` باشد (هم‌خوان با `"php": ">=8.2"`).
- اکستنشن‌های لازم: `pdo_mysql` (MariaDB)، `intl` (Symfony)، `opcache`. اگر کد از `predis` استفاده می‌کند نیازی به اکستنشن redis نیست؛ بررسی کن `composer.json` چه دارد (`symfony/cache` با redis adapter ممکن است اکستنشن بخواهد) — اگر `\Redis` استفاده می‌شود، اکستنشن redis را نصب کن.
- چون فاز vendor با `--no-scripts` نصب می‌کند، در entrypoint یا با `composer run-script` کش warm شود (یا `cache:warmup` در entrypoint).
- بررسی کن آیا stage assets واقعاً به `vendor` نیاز دارد (به‌خاطر `@symfony/webpack-encore` و `symfony/ux-react`). اگر بدون vendor هم build می‌شود، آن COPY را حذف کن تا سبک‌تر شود.

### ۲. ساخت `docker/entrypoint.sh`

کارهای استارت‌آپ که نباید در build انجام شوند (چون به env و DB زنده نیاز دارند):

```sh
#!/bin/sh
set -e

# فقط برای سرویس وب اصلی، نه worker‌ها (worker‌ها CMD خودشان را دارند)
if [ "${RUN_INIT:-1}" = "1" ]; then
  # تولید کلید JWT اگر persist نشده (idempotent)
  php bin/console lexik:jwt:generate-keypair --skip-if-exists --no-interaction || true
  # warmup کش prod
  php bin/console cache:clear --no-warmup || true
  php bin/console cache:warmup || true
  # migration (idempotent)
  php bin/console doctrine:migrations:migrate --all-or-nothing --no-interaction || true
fi

exec "$@"
```

**نکته:** migration را یک‌بار اجرا کن. اگر سه سرویس (web + 2 worker) همگی همین entrypoint را اجرا کنند، race می‌شود → فقط سرویس web متغیر `RUN_INIT=1` داشته باشد و worker‌ها `RUN_INIT=0`. این را در compose منعکس کن. `--all-or-nothing` همان چیزی است که داک Coolify توصیه کرده.

### ۳. ساخت `docker/supervisord.conf`

برای اجرای هم‌زمان php-fpm و nginx در کانتینر web:

```ini
[supervisord]
nodaemon=true

[program:php-fpm]
command=php-fpm -F
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
command=nginx -g 'daemon off;'
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
```

### ۴. ساخت `docker/nginx/default.conf`

کانفیگ استاندارد Symfony front-controller:

```nginx
server {
    listen 80;
    server_name _;
    root /app/public;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        internal;
    }

    location ~ \.php$ { return 404; }

    client_max_body_size 16m;   # هماهنگ با MAX_FILE_SIZE_BYTES
    error_log /dev/stderr;
    access_log /dev/stdout;
}
```

### ۵. ساخت `docker/php/php.ini`

```ini
memory_limit = 256M
upload_max_filesize = 16M
post_max_size = 16M
max_execution_time = 60
expose_php = Off
opcache.enable = 1
opcache.preload = /app/config/preload.php
opcache.preload_user = www-data
```

**نکته:** `config/preload.php` در پروژه موجود است (تأیید شد) — می‌توان preload را فعال کرد. اگر باعث خطا شد، خط preload را حذف کن.

### ۶. ساخت `.dockerignore`

```
/node_modules
/vendor
/var
/public/build
/public/uploads
/.git
/.ddev
/.claude
/tests
*.sql
*.sql.gz
.env.local
.env.*.local
```

### ۷. ساخت `docker-compose.coolify.yaml`

سرویس‌ها: `app` (web)، `worker-async`، `worker-scheduler`. دیتابیس و Redis را **به‌عنوان سرویس مدیریت‌شدهٔ جداگانه در Coolify** بساز و از طریق env متصل کن — یا اگر می‌خواهی همه در compose باشند، MariaDB و Redis را هم اضافه کن. روش پیشنهادی: دیتابیس/Redis در همین compose تا «single source of truth» باشد.

```yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      RUN_INIT: "1"
      # SERVICE_FQDN_APP → دامنه از UI کولیفای ست می‌شود (پورت 80)
      - APP_ENV=prod
      - APP_DEBUG=0
      - APP_SECRET=${APP_SECRET}
      - DATABASE_URL=mysql://clinic:${DB_PASSWORD}@mariadb:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4
      - MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
      - REDIS_URL=redis://redis:6379
      - JWT_PASSPHRASE=${JWT_PASSPHRASE}
      - CORS_ALLOW_ORIGIN=${CORS_ALLOW_ORIGIN}
      - DEFAULT_URI=${APP_BASE_URL}
      - APP_BASE_URL=${APP_BASE_URL}
      - ALLOWED_FRONTEND_HOSTS=${ALLOWED_FRONTEND_HOSTS}
      - TRUSTED_PROXIES=${TRUSTED_PROXIES}
      - API_IR_BASE_URL=${API_IR_BASE_URL}
      - API_IR_TOKEN=${API_IR_TOKEN}
      - REFRESH_TOKEN_TTL=2592000
      - OTP_TTL=1200
      - MAX_FILE_SIZE_BYTES=5242880
      - UPLOAD_DIR=var/uploads
    volumes:
      - jwt_keys:/app/config/jwt
      - uploads_public:/app/public/uploads
      - uploads_var:/app/var/uploads
    depends_on:
      mariadb:
        condition: service_healthy
      redis:
        condition: service_started
    # پورت 80 — دامنه را در Coolify UI به این سرویس بده

  worker-async:
    build:
      context: .
      dockerfile: Dockerfile
    command: php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M -vv
    environment:
      RUN_INIT: "0"
      # همان env بالا (از .env کولیفای interpolate می‌شود)
      ...
    volumes:
      - jwt_keys:/app/config/jwt
      - uploads_public:/app/public/uploads
      - uploads_var:/app/var/uploads
    depends_on:
      mariadb:
        condition: service_healthy
      redis:
        condition: service_started

  worker-scheduler:
    build:
      context: .
      dockerfile: Dockerfile
    command: php bin/console messenger:consume scheduler_default --time-limit=3600 -vv
    environment:
      RUN_INIT: "0"
      ...
    depends_on:
      mariadb:
        condition: service_healthy
      redis:
        condition: service_started

  mariadb:
    image: mariadb:11.8
    environment:
      - MARIADB_DATABASE=clinic_pro
      - MARIADB_USER=clinic
      - MARIADB_PASSWORD=${DB_PASSWORD}
      - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
    volumes:
      - mariadb_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  jwt_keys:
  uploads_public:
  uploads_var:
  mariadb_data:
  redis_data:
```

**نکات حیاتی برای compose:**
- **هیچ `networks:` سفارشی تعریف نکن** — داک Coolify صریحاً هشدار داده که شبکهٔ سفارشی باعث قطعی متناوب در روتینگ Traefik می‌شود. Coolify خودش شبکه می‌سازد.
- برای جلوگیری از تکرار env در سه سرویس، می‌توان از YAML anchor (`x-app-env: &app-env`) استفاده کرد و در هر سرویس `<<: *app-env`. این را پیاده کن تا فایل تمیز بماند.
- متغیرهای حساس (`APP_SECRET`, `DB_PASSWORD`, `JWT_PASSPHRASE`, `API_IR_TOKEN`) با `${...}` از env کولیفای خوانده می‌شوند، نه hardcode. می‌توان از magic variableهای کولیفای مثل `${SERVICE_PASSWORD_DB}` برای پسورد دیتابیس استفاده کرد — این را در docs توضیح بده.
- worker‌ها `RUN_INIT=0` دارند تا فقط سرویس `app` migration/JWT را اجرا کند (جلوگیری از race).
- volume `jwt_keys` تضمین می‌کند کلید JWT بین دیپلوی‌ها persist شود؛ هر سه سرویسی که توکن می‌سازند/می‌خوانند باید همین volume را mount کنند.

### ۸. افزودن Trusted Proxies به `config/packages/framework.yaml`

Coolify پشت Traefik است؛ بدون این تنظیم `https` و IP کلاینت اشتباه تشخیص داده می‌شود.

```yaml
framework:
    secret: '%env(APP_SECRET)%'
    session: true

    trusted_proxies: '%env(TRUSTED_PROXIES)%'
    trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port']
```

مقدار پیش‌فرض در `.env.coolify.example` (رنج شبکهٔ داخلی داکر):
```
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
```

### ۹. ساخت `.env.coolify.example`

فهرست تمام متغیرهایی که در داشبورد Coolify باید وارد شوند (آن‌هایی که در compose با `${...}` ارجاع شده‌اند):

```env
# ── دامنه ──
APP_BASE_URL=https://your-domain.com
ALLOWED_FRONTEND_HOSTS=your-domain.com
CORS_ALLOW_ORIGIN='^https://your-domain\.com$'

# ── امنیتی (الزامی) ──
APP_SECRET=            # php -r "echo bin2hex(random_bytes(32));"
JWT_PASSPHRASE=        # openssl rand -hex 32
DB_PASSWORD=
DB_ROOT_PASSWORD=

# ── reverse proxy ──
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1

# ── api.ir ──
API_IR_BASE_URL=https://s.api.ir
API_IR_TOKEN=

# توجه: DATABASE_URL / MESSENGER_TRANSPORT_DSN / REDIS_URL در خودِ compose
# با نام سرویس (mariadb/redis) ساخته می‌شوند و نیازی به تعریف در UI ندارند.
# کلیدهای SMS و درگاه پرداخت از DB («تنظیمات سایت») خوانده می‌شوند، نه env.
```

### ۱۰. ساخت راهنمای `docs/deploy/coolify.md`

سند گام‌به‌گام فارسی شامل:
1. **Coolify UI:** New Resource → Git repo → Build Pack = **Docker Compose** → مشخص کردن `docker-compose.coolify.yaml` به‌عنوان Compose file و branch.
2. **Domain:** اختصاص دامنه به سرویس `app` (پورت 80) از UI.
3. **Environment Variables:** ارجاع به `.env.coolify.example` + کدام‌ها الزامی‌اند.
4. **Persistent Storage:** توضیح volumeها (`jwt_keys`, `uploads_public`, `uploads_var`, `mariadb_data`, `redis_data`) و **اهمیت persist بودن `jwt_keys`** (وگرنه هر دیپلوی همهٔ لاگین‌ها باطل می‌شود).
5. **اولین راه‌اندازی:** بعد از اولین دیپلوی، اجرای `php bin/console app:create-admin` داخل کانتینر `app` (از Terminal کولیفای).
6. **Worker‌ها:** توضیح اینکه `worker-async` و `worker-scheduler` در همین compose بالا می‌آیند و اگر scheduler اجرا نشود نوبت‌های پرداخت‌نشده آزاد نمی‌شوند.
7. **Healthcheck:** سرویس app روی `/` یا یک endpoint عمومی (از `config/packages/security.yaml` لیست public_endpoints را بررسی کن).

## نکات مهم (محدودیت‌ها و edge caseها)

- **MariaDB نه PostgreSQL:** `compose.yaml` موجود (postgres) فقط ddev است و نامرتبط؛ دست نزن. compose جدید MariaDB 11.8 با DSN `mysql://...&serverVersion=mariadb-11.8.0`.
- **بدون شبکهٔ سفارشی در compose** — هشدار صریح داک Coolify.
- **build فرانت‌اند داخل Dockerfile** — بدون stage assets و `yarn build`، پنل admin لود نمی‌شود.
- **persist کلید JWT** روی volume `jwt_keys` مهم‌ترین نکتهٔ عملیاتی است.
- **`JWT_PASSPHRASE` و `APP_SECRET`** باید قبل از اولین استارت در env کولیفای باشند.
- **race در migration:** فقط سرویس `app` با `RUN_INIT=1`؛ worker‌ها `RUN_INIT=0`.
- **ddev دست‌نخورده:** `compose.yaml`، `compose.override.yaml`، `.ddev/` تغییر نکنند.
- **هیچ منطق برنامه‌ای تغییر نکند** — فقط فایل‌های infra + یک ویرایش `framework.yaml`.
- بعد از تغییر `framework.yaml`، `ddev exec php bin/console cache:clear` بزن تا اعتبار config تأیید شود.
- این تغییرات API را عوض نمی‌کنند → نیازی به به‌روزرسانی `docs/api/*` نیست؛ فقط `docs/deploy/coolify.md` اضافه می‌شود.
- **`.dockerignore`** حتماً `vendor/`, `node_modules/`, `var/` را حذف کند تا build context سبک بماند و artifactهای لوکال وارد image نشوند.
