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