# آماده‌سازی پروژه 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 نشوند.