From b4273cfa8b682e74982635c475eeae25ef1cd64c Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Mon, 29 Jun 2026 16:11:54 +0330
Subject: [PATCH] feat(deploy): configure deployment for ClinicPro on Liara PHP
platform
---
.claude/prompt/deploy-liara-php.md | 280 +++++++++++++++++++++++++++++
.env.coolify.example | 17 ++
.github/workflows/ci.yml | 73 --------
.liaraignore | 49 +++++
docs/deploy-liara-php.md | 208 +++++++++++++++++++++
liara.json | 27 +--
liara_php.ini | 5 +
liara_pre_build.sh | 12 ++
liara_pre_start.sh | 25 +++
public/.htaccess | 35 ++++
supervisor.conf | 35 ++++
11 files changed, 683 insertions(+), 83 deletions(-)
create mode 100644 .claude/prompt/deploy-liara-php.md
delete mode 100644 .github/workflows/ci.yml
create mode 100644 .liaraignore
create mode 100644 docs/deploy-liara-php.md
create mode 100644 liara_php.ini
create mode 100755 liara_pre_build.sh
create mode 100755 liara_pre_start.sh
create mode 100644 public/.htaccess
create mode 100644 supervisor.conf
diff --git a/.claude/prompt/deploy-liara-php.md b/.claude/prompt/deploy-liara-php.md
new file mode 100644
index 00000000..05c39c82
--- /dev/null
+++ b/.claude/prompt/deploy-liara-php.md
@@ -0,0 +1,280 @@
+# دیپلوی ClinicPro (Symfony) روی لیارا با پلتفرم PHP (بدون داکر)
+
+## پروژه
+
+`clinicpro` (Backend Symfony 7.4 + پنل ادمین React 19).
+
+راهنما + تسک پیادهسازی برای دیپلوی روی **پلتفرم PHP لیارا** (buildpack بومی؛ لیارا خودش از سورس با `composer install` build میکند — بدون Dockerfile).
+
+> منابع: مستندات لیارا — paas/php/how-tos (set-envs، use-disk، set-logs، customize-php-ini، customize-htaccess، use-queues، set-http-security-headers، use-hooks، set-cron-job) و paas/laravel (quick-start، set-envs، set-cron-job، choose-version، manage-logs).
+
+---
+
+## زمینه
+
+تا الان دو مسیر دیپلوی برای این پروژه آماده شده:
+- **داکر/Coolify:** `Dockerfile` + `docker-compose.yml`
+- **داکر/لیارا:** `liara.json` (platform docker) + `liara-compose.yaml`
+
+این پرامپت مسیر **سومی** را میسازد: **پلتفرم PHP لیارا** (نوع برنامه = `php` هنگام «ساخت برنامهی جدید»). اینجا داکری در کار نیست؛ لیارا کد را میگیرد، `composer install` میزند و روی Apache + PHP-FPM سرو میکند.
+
+تفاوتهای کلیدی پلتفرم PHP که معماری را تعیین میکنند (از مستندات):
+- **Hookها فایلهای شلاند، نه JSON:** `liara_pre_build.sh` (قبل build، **بدون** دسترسی به env، مناسب apt) و `liara_pre_start.sh` (قبل start، **با** env، مناسب migration).
+- **Worker با `supervisor.conf`:** فایل `supervisor.conf` در ریشه؛ هر `[program]` یک دستور دلخواه (`php bin/console messenger:consume ...`) را زنده نگه میدارد. `$ROOT` = ریشهٔ اپ، `user=www-data`.
+- **php.ini با `liara_php.ini`** در ریشه.
+- **سرو با Apache + `.htaccess`؛ documentRoot باید `public/` شود.**
+- **دیسکها** در `liara.json` با مسیر **absolute**.
+- **نسخهٔ PHP:** 7.2 تا 8.4 (پیشفرض 7.4) — پروژه `php: >=8.2` میخواهد، پس باید 8.2+ انتخاب شود.
+
+---
+
+## مشکل / هدف
+
+اجرای کامل Symfony روی پلتفرم PHP لیارا، شامل:
+1. سرو front-controller سیمفونی از `public/` روی Apache.
+2. اجرای migration و تولید کلید JWT در زمان استقرار.
+3. اجرای دو مصرفکنندهٔ Messenger (`async` و `scheduler_default`) بهصورت پایدار.
+4. آپلودِ assetهای buildشدهٔ React (که `composer install` آنها را نمیسازد).
+5. اتصال به MariaDB و Redis مدیریتشدهٔ لیارا.
+
+---
+
+## فایلهای مرتبط
+
+| فایل | نقش | وضعیت |
+|------|-----|------|
+| `liara.json` | پیکربندی دیپلوی | **بازنویسی برای platform php** (فعلاً platform docker است) |
+| `public/.htaccess` | rewrite سیمفونی برای Apache | **وجود ندارد — ساخته شود** (apache-pack نصب نیست) |
+| `liara_pre_start.sh` | hook قبل start (migration/JWT) | **جدید** |
+| `supervisor.conf` | اجرای دو worker | **جدید** |
+| `liara_php.ini` | تنظیم php.ini | **جدید** |
+| `.liaraignore` | کنترل فایلهای آپلودی | **جدید** (تا `public/build` آپلود شود) |
+| `composer.json` | اسکریپتهای نصب | بدون تغییر (با احتیاط؛ پایین) |
+
+> ⚠️ **تداخل liara.json:** الان `liara.json` برای platform docker است. پلتفرم PHP به محتوای متفاوتی نیاز دارد. فقط **یک** مسیر را همزمان نگه دار. اگر PHP را انتخاب کردی، محتوای docker را با نسخهٔ PHP زیر جایگزین کن (یا برنچ جدا).
+
+---
+
+## وضعیت فعلی (کد واقعی)
+
+`composer.json` هنگام نصب این اسکریپتها را اجرا میکند (روی build لیارا هم اجرا میشوند):
+
+```json
+"auto-scripts": {
+ "cache:clear": "symfony-cmd",
+ "assets:install %PUBLIC_DIR%": "symfony-cmd"
+},
+"post-install-cmd": ["@auto-scripts"]
+```
+
+`.gitignore` اینها را نادیده میگیرد (پس بهصورت پیشفرض آپلود **نمیشوند**):
+
+```
+/public/build/ # ← خروجی yarn build؛ باید آپلود شود
+/config/jwt/*.pem # ← روی دیسک ساخته میشود
+/var/
+.env
+```
+
+`public/.htaccess` وجود ندارد؛ extensionهای موردنیاز Symfony: `pdo_mysql`, `intl`, `opcache`, و برای Messenger روی Redis: `redis` (phpredis).
+
+---
+
+## وظایف
+
+### ۱. ساخت `liara.json` برای پلتفرم PHP
+
+محتوای فعلی (platform docker) را با این جایگزین کن:
+
+```json
+{
+ "app": "clinicpro-api",
+ "platform": "php",
+ "port": 80,
+ "php": {
+ "version": "8.2"
+ },
+ "documentRoot": "public",
+ "disks": [
+ { "name": "jwt", "mountTo": "/var/www/config/jwt" },
+ { "name": "uploads", "mountTo": "/var/www/public/uploads" },
+ { "name": "var-uploads", "mountTo": "/var/www/var/uploads" }
+ ]
+}
+```
+
+نکات:
+- `documentRoot: "public"` تا Apache front-controller سیمفونی را سرو کند.
+- مسیر mount دیسکها **absolute** است؛ ریشهٔ اپ روی پلتفرم PHP معمولاً `/var/www` است — اگر در کنسول/لاگ مسیر دیگری دیدی، اصلاح کن.
+- کلید `php.version` و `documentRoot` را اگر کنسول نپذیرفت، از تنظیمات کنسول (Platform Settings) ست کن — مستند نسخهگزینی را از طریق کنسول هم ممکن میداند.
+- سه دیسک `jwt` / `uploads` / `var-uploads` را پیش از دیپلوی در کنسول بساز.
+
+### ۲. ساخت `public/.htaccess` (rewrite سیمفونی)
+
+چون `symfony/apache-pack` نصب نیست، یا نصبش کن:
+
+```bash
+ddev exec composer require symfony/apache-pack
+```
+
+(این بهصورت تعاملی `public/.htaccess` استاندارد سیمفونی را میسازد) — یا فایل را دستی بساز:
+
+```apache
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
+ RewriteRule .* - [E=BASE:%1]
+ RewriteCond %{HTTP:Authorization} .+
+ RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
+ RewriteCond %{ENV:REDIRECT_STATUS} =""
+ RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
+ RewriteCond %{REQUEST_FILENAME} -f
+ RewriteRule ^ - [L]
+ RewriteRule ^ %{ENV:BASE}/index.php [L]
+
+
+
+ RedirectMatch 307 ^/$ /index.php/
+
+
+```
+
+### ۳. ساخت `supervisor.conf` (دو worker مسنجر)
+
+روی پلتفرم PHP، خود لیارا Apache/PHP-FPM را اجرا میکند؛ `supervisor.conf` **فقط** workerها را اضافه میکند (نه وب). در ریشه بساز:
+
+```ini
+[program:worker-async]
+process_name=%(program_name)s_%(process_num)02d
+command=cd $ROOT && php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M -v
+autostart=true
+autorestart=true
+stopasgroup=true
+killasgroup=true
+numprocs=1
+user=www-data
+redirect_stderr=true
+stdout_logfile=/tmp/worker-async.log
+
+[program:worker-scheduler]
+process_name=%(program_name)s_%(process_num)02d
+command=cd $ROOT && php bin/console messenger:consume scheduler_default --time-limit=3600 -v
+autostart=true
+autorestart=true
+stopasgroup=true
+killasgroup=true
+numprocs=1
+user=www-data
+redirect_stderr=true
+stdout_logfile=/tmp/worker-scheduler.log
+```
+
+> جایگزینِ cron: اگر نخواهی scheduler بهصورت worker بماند، میتوانی همان کار را با Cron Job کنسول (`* * * * *` → دستوری که یک پیام scheduler را مصرف کند) انجام دهی. ولی worker async (SMS) حتماً باید در `supervisor.conf` بماند.
+
+### ۴. ساخت `liara_pre_start.sh` (migration + JWT + cache)
+
+در ریشه، با env در دسترس:
+
+```bash
+#!/bin/sh
+set -e
+
+echo "pre-start: waiting for database..."
+i=0
+until php bin/console doctrine:query:sql "SELECT 1" >/dev/null 2>&1; do
+ i=$((i + 1))
+ [ "$i" -ge 30 ] && { echo "DB not reachable after 60s" >&2; exit 1; }
+ sleep 2
+done
+
+# کلید JWT روی دیسک ماندگار jwt
+php bin/console lexik:jwt:generate-keypair --skip-if-exists --no-interaction
+
+php bin/console cache:clear --no-warmup
+php bin/console cache:warmup
+php bin/console doctrine:migrations:migrate --all-or-nothing --no-interaction
+```
+
+فایل را اجرایی کن: `chmod +x liara_pre_start.sh`.
+
+### ۵. ساخت `liara_php.ini`
+
+برای پشتیبانی از آپلود (سقف اپ ۵MB ولی هدر/چندبخشی بزرگتر) و زمان اجرا:
+
+```ini
+memory_limit = 256M
+upload_max_filesize = 16M
+post_max_size = 24M
+max_execution_time = 120
+file_uploads = On
+```
+
+### ۶. ساخت `.liaraignore` تا assetهای build آپلود شوند
+
+`.gitignore` پوشهٔ `/public/build/` را نادیده میگیرد و لیارا بهصورت پیشفرض از gitignore پیروی میکند → assetها آپلود نمیشوند. با ساخت `.liaraignore` کنترل را بهدست بگیر (فقط واقعاً غیرلازمها را کنار بگذار، `public/build` را **نگه دار**):
+
+```
+.git
+.ddev
+.claude
+.github
+.vscode
+node_modules
+tests
+var/cache
+var/log
+docs
+graphify-out
+*.sql
+*.sql.gz
+.DS_Store
+```
+
+پیش از هر دیپلوی، assetها را **محلی** build کن (لیارا `yarn build` نمیزند):
+
+```bash
+ddev exec yarn install --frozen-lockfile
+ddev exec yarn build # خروجی در public/build
+```
+
+### ۷. دیتابیس و Redis مدیریتشده + متغیرهای محیطی
+
+مثل مسیر داکر:
+1. در کنسول، **MariaDB 11.8** و **Redis** بساز، شبکهٔ خصوصی فعال، برنامه را به همان شبکه وصل کن.
+2. envها را با `liara env set ... --app clinicpro-api` یا تب Environment ست کن (مرجع کامل: `.env.liara.example`). حداقل:
+ - `APP_ENV=prod`, `APP_DEBUG=0`
+ - `APP_SECRET`, `JWT_PASSPHRASE` (ثابت)
+ - `DATABASE_URL` (هاست خصوصی MariaDB، `serverVersion=mariadb-11.8.0`)
+ - `REDIS_URL`, `MESSENGER_TRANSPORT_DSN` (هاست خصوصی Redis)
+ - `JWT_SECRET_KEY`/`JWT_PUBLIC_KEY` (مسیر `config/jwt/*.pem`)
+ - `APP_BASE_URL`, `DEFAULT_URI`, `TRUSTED_PROXIES`
+ - `ALLOWED_FRONTEND_HOSTS`, `CORS_ALLOW_ORIGIN` (خروجی `php docker/gen-cors-env.php`)
+ - `API_IR_BASE_URL`, `API_IR_TOKEN`
+3. **مهم:** `APP_ENV=prod` باید موقع **build** هم موجود باشد، چون `composer install` اسکریپت `cache:clear` را اجرا میکند.
+
+### ۸. استقرار و تأیید
+
+```bash
+liara deploy # از ریشهٔ clinicpro، با liara.json platform php
+```
+
+سپس:
+- `GET https:///health` → 200
+- لاگ: در کنسول/`liara logs` هر دو worker (`worker-async`, `worker-scheduler`) بالا باشند
+- ورود `/admin` و تست یک endpoint
+
+---
+
+## نکات مهم
+
+- **ext-redis ریسک اصلی است:** اگر پلتفرم PHP لیارا افزونهٔ `redis` (phpredis) را نداشته باشد، `REDIS_URL` و `MESSENGER_TRANSPORT_DSN: redis://...` کار نمیکنند. اول در محیط لیارا چک کن (`php -m | grep redis`). اگر نبود:
+ - برای Messenger به transport مبتنی بر **Doctrine** سوییچ کن (`MESSENGER_TRANSPORT_DSN=doctrine://default`) و جدول مسنجر را با migration بساز،
+ - برای cache هم به adapter فایلسیستم/Doctrine برگرد.
+- **اسکریپتهای composer در build:** `cache:clear` در prod بدون DB معمولاً سالم است؛ اگر build به خاطر اسکریپتها شکست، میتوان نصب را بدون اسکریپت کرد و همهچیز را به `liara_pre_start.sh` سپرد (هماهنگ با کاری که `Dockerfile` با `--no-scripts` میکند).
+- **ماندگاری کلید JWT:** دیسک `jwt` حتماً mount شود؛ وگرنه هر دیپلوی کلید نو میسازد و توکنها باطل میشوند.
+- **assetها فراموش نشوند:** اگر `public/build` آپلود نشود، پنل ادمین لود نمیشود — قبل دیپلوی `yarn build` و `.liaraignore` درست.
+- **documentRoot:** اگر روی `public` ست نشود، Apache فایلهای ریشه را سرو میکند و اپ ۴۰۴/۴۰۳ میدهد.
+- **هماهنگی نسخهٔ DB:** `serverVersion` در `DATABASE_URL` با نسخهٔ MariaDB مدیریتشده یکی باشد (11.8).
+- این تغییر فقط زیرساخت دیپلوی است و هیچ route/endpoint را عوض نمیکند → نیازی بهروزرسانی `docs/api/*` نیست.
+- **یک مسیر را انتخاب کن:** Docker (`liara.json` فعلی + `liara-compose.yaml`) یا PHP platform (این پرامپت). همزمان هر دو `liara.json` نگهداشتنی نیست.
+```
diff --git a/.env.coolify.example b/.env.coolify.example
index e0d6bba4..5a5e9e0c 100644
--- a/.env.coolify.example
+++ b/.env.coolify.example
@@ -60,3 +60,20 @@ API_IR_TOKEN=
# from the DB ("Site Settings"), NOT from env. No need to set them here.
# • REFRESH_TOKEN_TTL / OTP_TTL / MAX_FILE_SIZE_BYTES are fixed in the compose file.
# • MariaDB & Redis are separate Coolify resources — see DATABASE_URL/REDIS_URL above.
+
+
+
+
+
+APP_ENV=prod
+APP_DEBUG=0
+APP_SECRET=...
+JWT_PASSPHRASE=...
+JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
+JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
+TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
+ALLOWED_FRONTEND_HOSTS=... # همان خروجی gen-cors-env.php
+CORS_ALLOW_ORIGIN=... # همان
+API_IR_BASE_URL=https://s.api.ir
+API_IR_TOKEN=...
+REFRESH_TOKEN_TTL / OTP_TTL / MAX_FILE_SIZE_BYTES / UPLOAD_DIR
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 836ae23b..00000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-name: CI
-
-on:
- push:
- branches: [main, backend-audit]
- pull_request:
-
-jobs:
- backend:
- name: PHP — phpstan + migrate-on-empty-db + phpunit
- runs-on: ubuntu-latest
-
- services:
- mariadb:
- image: mariadb:11.8
- env:
- MARIADB_ROOT_PASSWORD: root
- MARIADB_DATABASE: db
- MARIADB_USER: db
- MARIADB_PASSWORD: db
- ports:
- - 3306:3306
- options: >-
- --health-cmd="healthcheck.sh --connect --innodb_initialized"
- --health-interval=10s --health-timeout=5s --health-retries=20
- redis:
- image: redis:7
- ports:
- - 6379:6379
- options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=10
-
- env:
- # Real env vars take precedence over .env/.env.test. doctrine's when@test
- # config appends `_test`, so this `db` becomes `db_test` for the test run.
- DATABASE_URL: "mysql://db:db@127.0.0.1:3306/db?serverVersion=11.8.0-MariaDB&charset=utf8mb4"
- REDIS_URL: "redis://127.0.0.1:6379"
- APP_ENV: test
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.3'
- extensions: pdo_mysql, intl, redis, gd, zip, mbstring, bcmath
- coverage: none
-
- - name: Install dependencies
- run: composer install --no-interaction --prefer-dist --no-progress
-
- - name: Create test database
- run: |
- mysql -h127.0.0.1 -uroot -proot -e "CREATE DATABASE IF NOT EXISTS db_test; GRANT ALL ON db_test.* TO 'db'@'%'; FLUSH PRIVILEGES;"
-
- - name: Generate JWT keypair
- run: php bin/console lexik:jwt:generate-keypair --skip-if-exists --env=test
-
- # phpstan's symfony extension reads the dev container XML (see phpstan.neon),
- # so warm the dev cache first.
- - name: Warm dev cache (for phpstan container)
- run: php bin/console cache:warmup --env=dev
- env:
- APP_ENV: dev
-
- - name: PHPStan (baselined — fails only on NEW errors)
- run: php vendor/bin/phpstan analyse --no-progress
-
- - name: Migrate on empty DB (smoke)
- run: php bin/console doctrine:migrations:migrate --no-interaction --env=test
-
- - name: PHPUnit
- run: php bin/phpunit
diff --git a/.liaraignore b/.liaraignore
new file mode 100644
index 00000000..397e0703
--- /dev/null
+++ b/.liaraignore
@@ -0,0 +1,49 @@
+# Liara upload excludes. When this file exists Liara uses it instead of .gitignore,
+# so public/build (gitignored) IS uploaded — required, the platform never runs yarn.
+# Build assets locally first: `ddev exec yarn build`.
+
+.git
+.github
+.ddev
+.claude
+.agents
+.vscode
+.idea
+node_modules
+
+# Composer rebuilds vendor on the platform.
+vendor
+
+# Local env & dev secrets — a minimal prod .env is written by liara_pre_build.sh.
+.env
+.env.local
+.env.*.local
+.env.dev
+.env.test
+.env.coolify.example
+.env.liara.example
+config/jwt/*.pem
+
+# Docker/Coolify deployment artifacts (irrelevant on the PHP platform).
+Dockerfile
+.dockerignore
+docker
+docker-compose.yml
+liara-compose.yaml
+
+# Tests, static analysis, runtime state, docs.
+tests
+phpunit.dist.xml
+.phpunit.cache
+phpstan.neon
+phpstan-baseline.neon
+var/cache
+var/log
+docs
+graphify-out
+
+# DB dumps & OS noise.
+*.sql
+*.sql.gz
+.DS_Store
+*.log
diff --git a/docs/deploy-liara-php.md b/docs/deploy-liara-php.md
new file mode 100644
index 00000000..497d269e
--- /dev/null
+++ b/docs/deploy-liara-php.md
@@ -0,0 +1,208 @@
+# راهنمای دیپلوی ClinicPro روی لیارا (پلتفرم PHP)
+
+دیپلوی بکاند Symfony روی **پلتفرم PHP لیارا** (نوع برنامه `php` هنگام «ساخت برنامهی جدید»). اینجا داکری در کار نیست؛ لیارا کد را میگیرد، خودش `composer install` میزند و روی Apache + PHP-FPM سرو میکند.
+
+> مسیر جایگزین (داکر) در [docs/deploy-liara.md](deploy-liara.md) است. **همزمان فقط یک مسیر** را نگه دار — هر دو از یک `liara.json` استفاده میکنند.
+
+---
+
+## نمای کلی
+
+```
+┌──────────────── Liara private network ─────────────────┐
+│ │
+│ ┌──────────────────────────────┐ ┌──────────────┐ │
+│ │ clinicpro-api (PHP app) │ │ MariaDB 11.8 │ │
+│ │ • Apache + PHP-FPM (docroot │◄─►│ (managed) │ │
+│ │ = public/) :80 │ └──────────────┘ │
+│ │ • supervisor.conf: │ ┌──────────────┐ │
+│ │ worker-async │◄─►│ Redis │ │
+│ │ worker-scheduler │ │ (managed) │ │
+│ └──────────────────────────────┘ └──────────────┘ │
+│ disks: jwt / uploads / var-uploads │
+└────────────────────────────────────────────────────────┘
+ │ :80
+ ▼ https://
+```
+
+همهٔ پروسهها (وب + هر دو worker) در **یک** کانتینر اجرا میشوند؛ workerها کانتینر جدا ندارند (برخلاف نسخهٔ docker-compose).
+
+---
+
+## فایلهای دخیل (همه ساخته شدهاند)
+
+| فایل | نقش |
+|---|---|
+| [`liara.json`](../liara.json) | `platform: php`، `php.version: 8.2`، `documentRoot: public`، دیسکها |
+| [`public/.htaccess`](../public/.htaccess) | rewrite front-controller سیمفونی روی Apache |
+| [`liara_pre_build.sh`](../liara_pre_build.sh) | قبل `composer install`: ساخت `.env` مینیمال prod |
+| [`liara_pre_start.sh`](../liara_pre_start.sh) | قبل start: انتظار DB، تولید JWT، cache، migration |
+| [`supervisor.conf`](../supervisor.conf) | دو worker مسنجر (`async` + `scheduler_default`) |
+| [`liara_php.ini`](../liara_php.ini) | memory_limit، حجم آپلود، زمان اجرا |
+| [`.liaraignore`](../.liaraignore) | کنترل آپلود (تا `public/build` آپلود شود، سکرتها مستثنی) |
+| [`.env.liara.example`](../.env.liara.example) | مرجع متغیرهای محیطی |
+
+---
+
+## پیشنیاز
+
+```bash
+npm i -g @liara/cli
+liara login
+```
+
+مقادیر آماده:
+```bash
+php -r "echo bin2hex(random_bytes(32)).\"\n\";" # APP_SECRET
+openssl rand -hex 32 # JWT_PASSPHRASE
+ddev exec php docker/gen-cors-env.php # ALLOWED_FRONTEND_HOSTS + CORS_ALLOW_ORIGIN
+```
+
+---
+
+## گام ۱ — build محلی assetها (حیاتی)
+
+پلتفرم PHP لیارا **`yarn` اجرا نمیکند** و assetهای React را نمیسازد. قبل هر دیپلوی محلی build کن (خروجی در `public/build`؛ `.liaraignore` آن را آپلود میکند):
+
+```bash
+ddev exec yarn install --frozen-lockfile
+ddev exec yarn build
+```
+
+> اگر این مرحله را رد کنی، پنل ادمین `/admin` لود نمیشود.
+
+---
+
+## گام ۲ — ساخت دیتابیس و Redis
+
+کنسول لیارا:
+1. **MariaDB 11.8** بساز (هماهنگ با `serverVersion=mariadb-11.8.0`).
+2. **Redis** بساز.
+3. روی هر دو **شبکهٔ خصوصی** فعال؛ هاست خصوصی/کاربر/رمز را یادداشت کن.
+
+---
+
+## گام ۳ — ساخت برنامهٔ PHP
+
+1. New App → نوع **PHP** → نسخهٔ **8.2** (پروژه `php: >=8.2` میخواهد) → شناسه `clinicpro-api`.
+2. آن را به **همان شبکهٔ خصوصیِ** DB و Redis وصل کن.
+
+---
+
+## گام ۴ — ساخت دیسکهای ماندگار
+
+در صفحهٔ برنامه، بخش Disks، این سه دیسک را با همین نامها بساز (mount از `liara.json`):
+
+| نام | mountTo | محتوا |
+|---|---|---|
+| `jwt` | `/var/www/config/jwt` | کلید JWT (نباید هر دیپلوی نو شود) |
+| `uploads` | `/var/www/public/uploads` | فایلهای عمومی |
+| `var-uploads` | `/var/www/var/uploads` | فایلهای خصوصی |
+
+> ⚠️ ریشهٔ اپ فرض شده `/var/www`. اگر در لاگ مسیر دیگری دیدی، `mountTo` در `liara.json` را اصلاح کن.
+
+---
+
+## گام ۵ — متغیرهای محیطی
+
+از روی [`.env.liara.example`](../.env.liara.example) ست کن (تب Environment یا CLI). یکبار روی app؛ workerها همان را میگیرند.
+
+```bash
+liara env set APP_ENV=prod APP_DEBUG=0 --app clinicpro-api
+liara env set APP_SECRET=<...> JWT_PASSPHRASE=<...> --app clinicpro-api
+liara env set DATABASE_URL="mysql://:@:3306/?serverVersion=mariadb-11.8.0&charset=utf8mb4" --app clinicpro-api
+liara env set REDIS_URL="redis://:@:6379" --app clinicpro-api
+liara env set MESSENGER_TRANSPORT_DSN="redis://:@:6379/messages" --app clinicpro-api
+liara env set JWT_SECRET_KEY='%kernel.project_dir%/config/jwt/private.pem' JWT_PUBLIC_KEY='%kernel.project_dir%/config/jwt/public.pem' --app clinicpro-api
+liara env set APP_BASE_URL=https:// DEFAULT_URI=https:// --app clinicpro-api
+liara env set TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1" --app clinicpro-api
+liara env set ALLOWED_FRONTEND_HOSTS="