# جدا کردن MariaDB و Redis از docker-compose به Resourceهای مستقل Coolify

## پروژه

`clinicpro` (backend / deploy)

## زمینه

الان `clinicpro/docker-compose.yml` یک استک کامل است: `app` + `worker-async` + `worker-scheduler` + `mariadb` + `redis`. هر سه سرویس اپ از یک image مشترک (`clinicpro-app:latest`) استفاده می‌کنند و `mariadb`/`redis` داخل همین compose به‌صورت سرویس تعریف شده‌اند با volumeهای محلی (`mariadb_data`, `redis_data`).

کاربر می‌خواهد در Coolify، **MariaDB و Redis را به‌صورت Database Resource مستقل** بسازد و مدیریت کند (بکاپ، monitoring، آپدیت جدا، ری‌استارت مستقل از اپ). یعنی این دو باید از `docker-compose.yml` اپ حذف شوند و اپ به آن‌ها از طریق شبکه‌ی Coolify وصل شود.

## هدف

`docker-compose.yml` فقط شامل سرویس‌های اپلیکیشن باشد (`app` + دو worker). MariaDB و Redis از compose حذف شوند و اپ به Resourceهای مستقل Coolify وصل شود.

## قوانین Coolify (از داکیومنت‌ها — رعایت اجباری)

- **شبکه:** Coolify برای هر Resource یک شبکه‌ی bridge ایزوله (بر اساس UUID) می‌سازد. سرویس‌های داخل یک compose با **نام سرویس** همدیگر را می‌بینند. اما برای دسترسی به یک Resource **دیگر** (دیتابیس مستقل)، باید گزینه‌ی **"Connect to Predefined Network"** روی استک اپ فعال شود و دیتابیس با شناسه‌ی کامل `<service>-<uuid>` به‌عنوان hostname رفرنس داده شود (مثل `postgresql-abc123...`؛ برای ما `mariadb-<uuid>` / `redis-<uuid>`).
- **هرگز `networks:` سفارشی** در compose تعریف نکن — Coolify خودش مدیریت می‌کند؛ شبکه‌ی سفارشی routing Traefik را می‌شکند و باعث قطعی متناوب می‌شود.
- **مقادیر اتصال (DATABASE_URL / REDIS_URL / MESSENGER_TRANSPORT_DSN)** دیگر به نام سرویس داخلی اشاره نمی‌کنند؛ از تب Environment Variables در Coolify ست می‌شوند و به hostname دیتابیس مستقل (`mariadb-<uuid>` / `redis-<uuid>`) اشاره می‌کنند.

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

| فایل | نقش |
|------|-----|
| `clinicpro/docker-compose.yml` | استک دیپلوی Coolify — باید mariadb/redis از آن حذف شود |
| `clinicpro/.env.coolify.example` | نمونه env vars برای Coolify — باید برای اتصال به دیتابیس مستقل به‌روز شود |
| `clinicpro/docs/DEPLOY.md` | راهنمای دیپلوی — باید مراحل ساخت Resource مستقل اضافه شود |
| `clinicpro/docker/entrypoint.sh` | فقط روی `app` (RUN_INIT=1) migration می‌زند — منطق بدون تغییر، فقط باید به DB مستقل وصل شود |

## وضعیت فعلی

استک فعلی mariadb/redis را داخل خودش دارد و `depends_on` به healthcheck آن‌ها گره خورده:

```yaml
# x-app-env
  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

# x-app-depends
  mariadb:
    condition: service_healthy
  redis:
    condition: service_started

services:
  app: ...
    depends_on: *app-depends
  worker-async: ...
    depends_on: *app-depends
  worker-scheduler: ...
    depends_on: *app-depends

  mariadb:
    image: mariadb:11.8
    ...
    volumes:
      - mariadb_data:/var/lib/mysql
    healthcheck: ...

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

volumes:
  jwt_keys:
  uploads_public:
  uploads_var:
  mariadb_data:    # ← متعلق به دیتابیس داخلی
  redis_data:      # ← متعلق به redis داخلی
```

## وظایف

### ۱. حذف سرویس‌های `mariadb` و `redis` از `docker-compose.yml`

- بلوک سرویس `mariadb` و `redis` کامل حذف شوند.
- volumeهای `mariadb_data` و `redis_data` از بخش `volumes:` حذف شوند (دیگر داخل این استک نیستند؛ داده‌ی DB مستقل توسط خود Resource نگه داشته می‌شود). `jwt_keys`, `uploads_public`, `uploads_var` بمانند.

### ۲. حذف `depends_on` به mariadb/redis

`x-app-depends` (و ارجاع `depends_on: *app-depends` در هر سه سرویس) دیگر بی‌معناست — این سرویس‌ها در استک نیستند. دو گزینه:

- **پیشنهادی:** anchor `x-app-depends` و همه‌ی `depends_on: *app-depends` حذف شوند. به‌جای آن، تحمل قطعی موقت در استارت‌آپ تضمین شود (وظیفه ۴).
- اگر می‌خواهی ترتیب استارت `app` قبل از workerها حفظ شود، فقط workerها می‌توانند `depends_on: [app]` ساده داشته باشند (بدون condition healthcheck).

نمونه‌ی نهایی موردانتظار (شِمای کلی):

```yaml
x-app-env: &app-env
  APP_ENV: prod
  APP_DEBUG: "0"
  APP_SECRET: ${APP_SECRET}
  # به دیتابیس/Redis مستقل Coolify اشاره می‌کند — مقدار واقعی از تب Environment Variables
  DATABASE_URL: ${DATABASE_URL}
  MESSENGER_TRANSPORT_DSN: ${MESSENGER_TRANSPORT_DSN}
  REDIS_URL: ${REDIS_URL}
  JWT_SECRET_KEY: "%kernel.project_dir%/config/jwt/private.pem"
  JWT_PUBLIC_KEY: "%kernel.project_dir%/config/jwt/public.pem"
  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

x-app-volumes: &app-volumes
  - jwt_keys:/app/config/jwt
  - uploads_public:/app/public/uploads
  - uploads_var:/app/var/uploads

services:
  app:
    image: clinicpro-app:latest
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      <<: *app-env
      RUN_INIT: "1"
    volumes: *app-volumes
    healthcheck:
      test: ["CMD", "php", "-r", "exit(@fsockopen('127.0.0.1', 80) ? 0 : 1);"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 60s

  worker-async:
    image: clinicpro-app:latest
    restart: unless-stopped
    command: php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M -v
    environment:
      <<: *app-env
      RUN_INIT: "0"
    volumes: *app-volumes
    depends_on: [app]

  worker-scheduler:
    image: clinicpro-app:latest
    restart: unless-stopped
    command: php bin/console messenger:consume scheduler_default --time-limit=3600 -v
    environment:
      <<: *app-env
      RUN_INIT: "0"
    volumes: *app-volumes
    depends_on: [app]

volumes:
  jwt_keys:
  uploads_public:
  uploads_var:
```

> توجه: `DATABASE_URL`/`REDIS_URL`/`MESSENGER_TRANSPORT_DSN` از hardcode داخل compose به `${...}` تبدیل شدند تا از Coolify (با hostname دیتابیس مستقل) تزریق شوند. `DB_PASSWORD`/`DB_ROOT_PASSWORD` دیگر در این compose استفاده نمی‌شوند (متعلق به Resource دیتابیس‌اند).

### ۳. به‌روزرسانی `.env.coolify.example`

- متغیرهای `DB_PASSWORD` / `DB_ROOT_PASSWORD` که مخصوص mariadb داخلی بودند را حذف یا به بخش «این‌ها را موقع ساخت Database Resource ست کن» منتقل کن.
- سه متغیر اتصال اضافه/مستندسازی شوند که به Resource مستقل اشاره می‌کنند. hostname باید `<service>-<uuid>` باشد (UUID از صفحه‌ی Resource در Coolify گرفته می‌شود):

```bash
# اتصال به MariaDB مستقل Coolify (hostname = mariadb-<uuid> از صفحه‌ی Resource)
DATABASE_URL="mysql://clinic:PASSWORD@mariadb-XXXXXXXX:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4"

# اتصال به Redis مستقل Coolify (hostname = redis-<uuid>)
REDIS_URL="redis://redis-XXXXXXXX:6379"
MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages"
```

- توضیح بده که اگر Redis مستقل Coolify رمز دارد، فرمت `redis://:PASSWORD@redis-<uuid>:6379` است.

### ۴. تحمل قطعی DB در استارت‌آپ (چون دیگر `depends_on: service_healthy` نیست)

چون اپ دیگر منتظر healthcheck دیتابیس نمی‌ماند، ممکن است هنگام بوت، DB هنوز آماده نباشد و `doctrine:migrations:migrate` در `entrypoint.sh` شکست بخورد. در `docker/entrypoint.sh`، قبل از migration یک انتظار کوتاه برای آماده‌شدن DB اضافه کن (مثلاً حلقه‌ای که با یک کوئری ساده‌ی Doctrine اتصال را چک می‌کند، چند بار با تأخیر retry):

```sh
# منتظر آماده‌شدن دیتابیس مستقل بمان (حداکثر ~60s) قبل از migration
if [ "${RUN_INIT:-1}" = "1" ]; then
    i=0
    until php bin/console dbal:run-sql "SELECT 1" >/dev/null 2>&1; do
        i=$((i+1))
        [ "$i" -ge 30 ] && echo "DB not reachable after 60s" && exit 1
        echo "waiting for database... ($i)"
        sleep 2
    done
    # سپس: JWT keygen + cache + migrations (منطق فعلی)
fi
```

منطق فعلی JWT keygen / cache:clear / cache:warmup / migrate دست‌نخورده بماند؛ فقط حلقه‌ی انتظار قبل از آن اضافه شود. (در صورت نبودن دستور `dbal:run-sql`، از `doctrine:query:sql "SELECT 1"` استفاده کن — اول با `ddev exec php bin/console list doctrine | grep -i sql` چک کن کدام موجود است.)

### ۵. به‌روزرسانی `docs/DEPLOY.md`

بخش «استک» و «متغیرهای محیطی» را اصلاح کن:
- جدول سرویس‌ها: `mariadb` و `redis` از استک اپ حذف شده‌اند و حالا **Database Resource مستقل** Coolify هستند.
- مراحل جدید: (الف) ساخت Resource مستقل MariaDB در Coolify، (ب) ساخت Resource مستقل Redis، (ج) فعال‌کردن **"Connect to Predefined Network"** روی استک اپ، (د) ست‌کردن `DATABASE_URL`/`REDIS_URL`/`MESSENGER_TRANSPORT_DSN` با hostname `<service>-<uuid>`.
- توضیح بده که volumeهای `mariadb_data`/`redis_data` دیگر در استک اپ نیستند؛ ماندگاری داده برعهده‌ی خود Resourceهاست.

## نکات مهم

- **هیچ `networks:` سفارشی** اضافه نشود؛ اتصال cross-resource فقط با "Connect to Predefined Network" + hostname `<service>-<uuid>`.
- مقادیر `DATABASE_URL`/`REDIS_URL`/`MESSENGER_TRANSPORT_DSN` نباید داخل compose hardcode شوند؛ از `${...}` و تب Coolify بیایند (چون UUID برای هر نصب فرق دارد).
- `serverVersion=mariadb-11.8.0` در `DATABASE_URL` حفظ شود (Doctrine برای تولید SQL درست لازم دارد) — مطمئن شو نسخه‌ی Resource مستقل MariaDB هم 11.8 انتخاب شود تا با migrationها هم‌خوان باشد.
- فقط سرویس `app` (`RUN_INIT=1`) migration می‌زند؛ workerها (`RUN_INIT=0`) نباید — این رفتار حفظ شود تا race در migration پیش نیاید.
- `image: clinicpro-app:latest` مشترک حفظ شود (app یک‌بار build، workerها reuse) — این فیکس قبلی برای جلوگیری از سه‌بار build است؛ خرابش نکن.
- بعد از تغییر، با `docker compose config` اعتبار YAML را چک کن.
- این تغییر فقط deploy/infra است؛ هیچ controller/route/Entity تغییر نمی‌کند، پس نیازی به آپدیت `docs/api/*` نیست (فقط `docs/DEPLOY.md`).
```
