Files
clinicpro/.claude/prompt/coolify-symfony-deploy.md
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

18 KiB
Raw Permalink Blame History

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

# ---------- 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 زنده نیاز دارند):

#!/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:

[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:

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

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» باشد.

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 کلاینت اشتباه تشخیص داده می‌شود.

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 با ${...} ارجاع شده‌اند):

# ── دامنه ──
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 نشوند.