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
+331 -147
View File
@@ -1,205 +1,389 @@
# آماده‌سازی پروژه ClinicPro برای دیپلوی روی Coolify (Symfony + Nixpacks)
# آماده‌سازی پروژه ClinicPro برای دیپلوی روی Coolify با Docker Compose
## زمینه
پروژه قرار است روی **Coolify** با Build Pack = **Nixpacks** دیپلوی شود (طبق https://coolify.io/docs/applications/symfony).
در حال حاضر پروژه فقط برای محیط لوکال **ddev** پیکربندی شده است (`compose.yaml`، `.ddev/`).
برای Coolify نیاز به این موارد داریم که هیچ‌کدام الان وجود ندارند:
پروژه روی **Coolify** دیپلوی می‌شود، اما به‌جای Build Pack پیش‌فرض (Nixpacks)، از **Docker Compose** به‌عنوان build pack استفاده می‌کنیم — یعنی یک `docker-compose.yaml` در ریشهٔ پروژه «single source of truth» است و Coolify آن را اجرا می‌کند (طبق https://coolify.io/docs/builds/packs/docker-compose).
1. کنترل کامل بر فرایند **build** — چون این پروژه علاوه بر PHP یک فرانت‌اند **React 19 + Webpack Encore** دارد که باید با `yarn build` کامپایل شود (Nixpacks پیش‌فرض Symfony فقط PHP را build می‌کند و دارایی‌های `public/build/` ساخته نمی‌شوند).
2. تولید **کلیدهای JWT** (lexik) هنگام دیپلوی — این کلیدها در `.gitignore` هستند و در ریپو نیستند.
3. اجرای **migrations** بعد از هر دیپلوی.
4. اجرای **worker**های Messenger:
- `messenger:consume async` → ارسال SMS
- `messenger:consume scheduler_default` → انقضای نوبت‌های پرداخت‌نشده (هر ۱ دقیقه)
5. تنظیم **Trusted Proxies** (چون Coolify پشت Traefik/reverse-proxy است؛ بدون آن `https`، IP کلاینت و کوکی‌ها درست کار نمی‌کنند).
6. یک فایل **`.env.coolify.example`** که همهٔ متغیرهای محیطی لازم را برای وارد کردن در داشبورد Coolify فهرست کند.
این یعنی ما باید **همهٔ image‌ها و سرویس‌ها را خودمان تعریف کنیم**: یک Dockerfile مرحله‌ای (multi-stage) برای ساخت اپ، و یک compose که اپ + worker‌ها را بالا بیاورد. هیچ‌کدام از این فایل‌ها الان وجود ندارند.
نکتهٔ مهم معماری: دیتابیس پروژه **MariaDB/MySQL** است (نه PostgreSQL که داک Coolify مثال می‌زند). Redis برای Messenger و OTP استفاده می‌شود.
**وضعیت فعلی پروژه:**
- 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 مشخص می‌کنیم.
## هدف
پروژه را طوری «coolify-ready» کن که با Build Pack = Nixpacks و چند متغیر محیطی، بدون تغییر در منطق برنامه، روی Coolify بالا بیاید — شامل build فرانت‌اند، تولید کلید JWT، migration، و راه‌اندازی worker‌ها.
ساخت یک image تولیدی کامل (PHP-FPM + Nginx + دارایی‌های build‌شدهٔ فرانت‌اند) و یک `docker-compose.coolify.yaml` که روی Coolify با Docker Compose build pack بالا بیاید — شامل اپ وب، دو worker، و اتصال به سرویس‌های MariaDB و Redis. **بدون تغییر منطق برنامه.**
## فایل‌های مرتبط
| فایل | نقش | وضعیت |
|------|-----|-------|
| `nixpacks.toml` | کنترل فازهای build/install/start برای Nixpacks | **باید ساخته شود** |
| `config/packages/framework.yaml` | افزودن `trusted_proxies` و `trusted_headers` | باید ویرایش شود |
| `.env.coolify.example` | فهرست کامل env برای داشبورد Coolify | **باید ساخته شود** |
| `docs/deploy/coolify.md` | راهنمای گام‌به‌گام دیپلوی | **باید ساخته شود** |
| `.env.example` | مرجع موجود متغیرها (فقط خواندن) | بدون تغییر |
| `package.json` | اسکریپت `build` = `encore production` | بدون تغییر |
| `config/packages/messenger.yaml` | تعریف transport های `async` و `scheduler_default` | بدون تغییر (مرجع worker) |
## وضعیت فعلی
- `package.json` اسکریپت build دارد: `"build": "encore production --progress"`.
- خروجی build در `public/build/` می‌رود (در `.gitignore` است → باید حین دیپلوی ساخته شود).
- کلیدهای JWT در `config/jwt/*.pem` (در `.gitignore`).
- `framework.yaml` فعلی هیچ `trusted_proxies` ندارد:
```yaml
framework:
secret: '%env(APP_SECRET)%'
session: true
```
- متغیرهای کلیدی از `.env.example`: `APP_ENV`, `APP_SECRET`, `DATABASE_URL` (mysql), `JWT_*`, `CORS_ALLOW_ORIGIN`, `MESSENGER_TRANSPORT_DSN` (redis), `REDIS_URL`, `API_IR_*`, `APP_BASE_URL`, `ALLOWED_FRONTEND_HOSTS`, `DEFAULT_URI`, `MAX_FILE_SIZE_BYTES`, `UPLOAD_DIR`.
| `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` | راهنمای گام‌به‌گام | **باید ساخته شود** |
## وظایف
### ۱. ساخت `nixpacks.toml` در ریشهٔ پروژه
### ۱. ساخت `Dockerfile` چندمرحله‌ای
هدف: علاوه بر نصب وابستگی‌های PHP، فرانت‌اند را هم build کن و کلید JWT بساز. Nixpacks باید هم PHP و هم Node/Yarn را در محیط build داشته باشد.
سه stage: یکی برای وابستگی‌های PHP (composer)، یکی برای build فرانت‌اند (node/yarn)، و stage نهایی runtime.
```toml
# nixpacks.toml — کنترل دیپلوی Coolify
[phases.setup]
nixPkgs = ["php82", "php82Packages.composer", "nodejs_20", "yarn", "openssl"]
```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
[phases.install]
cmds = [
"composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist",
"yarn install --frozen-lockfile"
]
# ---------- 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
[phases.build]
# build دارایی‌های فرانت‌اند (React/Encore) → public/build
# و تولید کلید JWT اگر وجود نداشته باشد (idempotent)
cmds = [
"yarn build",
"php bin/console lexik:jwt:generate-keypair --skip-if-exists --no-interaction"
]
# ---------- 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)
[start]
# Nixpacks خودش php-fpm/nginx را با NIXPACKS_PHP_ROOT_DIR اجرا می‌کند؛
# این بخش را فقط در صورت نیاز override کن. در حالت عادی خالی بماند.
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"]
```
**نکات:**
- نسخهٔ `php82` باید با `"php": ">=8.2"` در `composer.json` هم‌خوان باشد. اگر Nixpacks نسخهٔ PHP را خودکار از composer تشخیص می‌دهد، می‌توان `php` را از `nixPkgs` حذف کرد — ولی صریح بودن امن‌تر است.
- `lexik:jwt:generate-keypair --skip-if-exists` باعث می‌شود اگر کلید از قبل (مثلاً از volume) وجود داشته باشد، دوباره ساخته نشود. **هشدار را در docs ذکر کن:** اگر کلید JWT بین دیپلوی‌ها persist نشود، همهٔ توکن‌های صادرشده باطل می‌شوند → بهتر است `JWT_SECRET_KEY`/`JWT_PUBLIC_KEY` به‌صورت محتوای base64 در env داده شوند یا روی volume مانت شوند. هر دو گزینه را در docs توضیح بده.
- اگر `JWT_PASSPHRASE` در env تنظیم نشده باشد، تولید کلید fail می‌شود — این را در docs قید کن.
- نسخهٔ 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 را حذف کن تا سبک‌تر شود.
### ۲. افزودن Trusted Proxies به `config/packages/framework.yaml`
### ۲. ساخت `docker/entrypoint.sh`
Coolify پشت Traefik است. بدون این تنظیم، Symfony پروتکل `https` و IP واقعی را تشخیص نمی‌دهد.
کارهای استارت‌آپ که نباید در 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
# Coolify/Traefik reverse proxy
trusted_proxies: '%env(TRUSTED_PROXIES)%'
trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port']
```
و در `.env.coolify.example` مقدار پیش‌فرض امن بده (طبق داک Coolify که از REMOTE_ADDR یا رنج شبکهٔ داکر استفاده می‌شود):
مقدار پیش‌فرض در `.env.coolify.example` (رنج شبکهٔ داخلی داکر):
```
# IP/رنج reverse proxy؛ برای Coolify معمولاً رنج شبکهٔ داخلی داکر یا 'REMOTE_ADDR'
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
```
**نکته:** اگر `TRUSTED_PROXIES` خالی بماند، Symfony خطا نمی‌دهد ولی پشت پراکسی درست کار نمی‌کند؛ مقدار پیش‌فرض را در env بگذار تا اگر کاربر فراموش کرد، رفتار منطقی باشد. اگر ترجیح می‌دهی fallback داشته باشی از `'%env(default::TRUSTED_PROXIES)%'` با مقدار پیش‌فرض در `.env` استفاده کن.
### ۹. ساخت `.env.coolify.example`
### ۳. ساخت `.env.coolify.example`
فهرست کامل و دسته‌بندی‌شدهٔ تمام متغیرهایی که باید در داشبورد Coolify (Environment Variables) وارد شوند. از روی `.env.example` بساز ولی این موارد Coolify-specific را اضافه کن:
فهرست تمام متغیرهایی که در داشبورد Coolify باید وارد شوند (آن‌هایی که در compose با `${...}` ارجاع شده‌اند):
```env
# ───── Coolify / Nixpacks (الزامی طبق داک Coolify) ─────
APP_ENV=prod
APP_DEBUG=0
NIXPACKS_PHP_FALLBACK_PATH=/index.php
NIXPACKS_PHP_ROOT_DIR=/app/public
# Port Exposure را در UI روی 80 بگذار
# ───── Symfony Core ─────
APP_SECRET= # php -r "echo bin2hex(random_bytes(32));"
DEFAULT_URI=https://your-domain.com
# ── دامنه ──
APP_BASE_URL=https://your-domain.com
ALLOWED_FRONTEND_HOSTS=your-domain.com
# ───── Reverse Proxy (Coolify/Traefik) ─────
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
# ───── Database (MariaDB/MySQL service در Coolify) ─────
DATABASE_URL="mysql://USER:PASS@HOST:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4"
# ───── JWT (lexik) ─────
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE= # الزامی برای تولید کلید در فاز build
# ───── CORS ─────
CORS_ALLOW_ORIGIN='^https://your-domain\.com$'
# ───── Messenger / Redis (Redis service در Coolify) ─────
MESSENGER_TRANSPORT_DSN=redis://HOST:6379/messages
REDIS_URL=redis://HOST:6379
# ── امنیتی (الزامی) ──
APP_SECRET= # php -r "echo bin2hex(random_bytes(32));"
JWT_PASSPHRASE= # openssl rand -hex 32
DB_PASSWORD=
DB_ROOT_PASSWORD=
# ───── Auth / OTP ─────
REFRESH_TOKEN_TTL=2592000
OTP_TTL=1200
# ── 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 ──
API_IR_BASE_URL=https://s.api.ir
API_IR_TOKEN=
# ───── File Upload ─────
MAX_FILE_SIZE_BYTES=5242880
UPLOAD_DIR=var/uploads
# توجه: DATABASE_URL / MESSENGER_TRANSPORT_DSN / REDIS_URL در خودِ compose
# با نام سرویس (mariadb/redis) ساخته می‌شوند و نیازی به تعریف در UI ندارند.
# کلیدهای SMS و درگاه پرداخت از DB («تنظیمات سایت») خوانده می‌شوند، نه env.
```
**نکات مهم برای این فایل:**
- `serverVersion` در `DATABASE_URL` را با MariaDB هم‌خوان کن (`mariadb-11.8.0`)، نه `8.0`، چون پروژه روی MariaDB 11.8 است.
- توضیح بده که `HOST` در DSNها = نام internal service که Coolify برای DB/Redis می‌دهد.
- کلیدهای SMS و درگاه پرداخت از DB («تنظیمات سایت») خوانده می‌شوند، نه env — این را با کامنت ذکر کن.
### ۱۰. ساخت راهنمای `docs/deploy/coolify.md`
### ۴. ساخت راهنمای `docs/deploy/coolify.md`
یک سند گام‌به‌گام فارسی شامل:
1. **تنظیمات Coolify UI:**
- Build Pack = `nixpacks`
- Port Exposure = `80`
- Health Check Path = `/api/doc` یا یک endpoint سبک (بررسی کن چه endpoint عمومی‌ای برای healthcheck مناسب است؛ از `config/packages/security.yaml` لیست public_endpoints را ببین).
2. **Environment Variables:** ارجاع به `.env.coolify.example` + متغیرهای الزامی Nixpacks.
3. **Post-Deployment Command** (در بخش مربوطهٔ Coolify):
```
php bin/console doctrine:migrations:migrate --all-or-nothing --no-interaction
```
4. **Persistent Storage (Volumes)** — این بخش حیاتی است، حتماً توضیح بده:
- `config/jwt/` → تا کلیدهای JWT بین دیپلوی‌ها باقی بمانند (در غیر این صورت همهٔ لاگین‌ها باطل می‌شوند).
- `public/uploads/` و `var/uploads/` → فایل‌های آپلودی کاربران.
- `var/log/` (اختیاری).
5. **Worker‌ها (سرویس‌های جداگانه در Coolify):** توضیح بده که باید دو سرویس/کانتینر اضافه با همان image ولی start command متفاوت ساخته شوند، یا از Supervisor در همان کانتینر استفاده شود:
- `php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M`
- `php bin/console messenger:consume scheduler_default --time-limit=3600`
گزینهٔ توصیه‌شده را مشخص کن (در Coolify معمولاً سرویس جداگانه تمیزتر است؛ یا یک `supervisord.conf` در همان کانتینر).
6. **اولین راه‌اندازی:** ساخت admin اولیه:
```
php bin/console app:create-admin
```
### ۵. (اختیاری ولی توصیه‌شده) فایل `supervisord.conf` برای worker‌ها
اگر تصمیم گرفتی worker‌ها در همان کانتینر اجرا شوند، یک `supervisord.conf` بساز که هر دو consumer را مدیریت کند و در `nixpacks.toml` به phase setup اضافه‌اش کن. در غیر این صورت، در docs گزینهٔ «سرویس جداگانه در Coolify» را به‌عنوان روش پیش‌فرض توضیح بده و این فایل را نساز.
تصمیم پیشنهادی: **سرویس جداگانه در Coolify** (ساده‌تر، بدون نیاز به supervisor و بدون پیچیده کردن کانتینر اصلی). فقط در docs مستند کن.
سند گام‌به‌گام فارسی شامل:
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:** داک رسمی Coolify مثال PostgreSQL می‌زند؛ این پروژه MySQL/MariaDB است. همه‌جا DSN را `mysql://...` با `serverVersion=mariadb-11.8.0` نگه دار.
- **build فرانت‌اند الزامی است:** بدون `yarn build`، صفحات admin (و دارایی‌های React) لود نمی‌شوند چون `public/build/` خالی می‌ماند. این تفاوت اصلی با setup پیش‌فرض Symfony در داک Coolify است.
- **JWT keypair persistence:** اگر volume برای `config/jwt/` تعریف نشود، هر دیپلوی کلید جدید می‌سازد و کاربران logout می‌شوند. این مهم‌ترین نکتهٔ عملیاتی است — حتماً در docs برجسته شود.
- **`JWT_PASSPHRASE` باید قبل از build در env باشد** وگرنه `lexik:jwt:generate-keypair` در فاز build شکست می‌خورد.
- **Scheduler:** transport `scheduler_default` نوبت‌های پرداخت‌نشده را هر دقیقه منقضی می‌کند؛ اگر worker آن اجرا نشود، نوبت‌های رزرو ولی پرداخت‌نشده آزاد نمی‌شوند. این رفتار را در docs ذکر کن.
- **هیچ منطق برنامه‌ای را تغییر نده** — فقط فایل‌های infra/config اضافه یا ویرایش شوند.
- **بدون تغییر در ddev:** فایل‌های `compose.yaml` و `.ddev/` دست‌نخورده بمانند تا محیط لوکال نشکند.
- بعد از تغییر `framework.yaml`، `ddev exec php bin/console cache:clear` بزن تا مطمئن شوی config معتبر است.
- این تغییرات API را عوض نمی‌کنند، پس نیازی به به‌روزرسانی `docs/api/*` نیست؛ ولی فایل جدید `docs/deploy/coolify.md` اضافه می‌شود.
- **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 نشوند.