Files
clinicpro/.claude/prompt/coolify-symfony-deploy.md
T
hamed cffc88db05 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.
2026-06-25 21:27:28 +03:30

390 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# آماده‌سازی پروژه 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 نشوند.