- 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.
18 KiB
آمادهسازی پروژه 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→ ارسال SMSmessenger:consume scheduler_default→ انقضای نوبتهای پرداختنشده (هر ۱ دقیقه؛ transportschedule://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دارند تا فقط سرویسappmigration/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
سند گامبهگام فارسی شامل:
- Coolify UI: New Resource → Git repo → Build Pack = Docker Compose → مشخص کردن
docker-compose.coolify.yamlبهعنوان Compose file و branch. - Domain: اختصاص دامنه به سرویس
app(پورت 80) از UI. - Environment Variables: ارجاع به
.env.coolify.example+ کدامها الزامیاند. - Persistent Storage: توضیح volumeها (
jwt_keys,uploads_public,uploads_var,mariadb_data,redis_data) و اهمیت persist بودنjwt_keys(وگرنه هر دیپلوی همهٔ لاگینها باطل میشود). - اولین راهاندازی: بعد از اولین دیپلوی، اجرای
php bin/console app:create-adminداخل کانتینرapp(از Terminal کولیفای). - Workerها: توضیح اینکه
worker-asyncوworker-schedulerدر همین compose بالا میآیند و اگر scheduler اجرا نشود نوبتهای پرداختنشده آزاد نمیشوند. - Healthcheck: سرویس app روی
/یا یک endpoint عمومی (ازconfig/packages/security.yamlلیست public_endpoints را بررسی کن).
نکات مهم (محدودیتها و edge caseها)
- MariaDB نه PostgreSQL:
compose.yamlموجود (postgres) فقط ddev است و نامرتبط؛ دست نزن. compose جدید MariaDB 11.8 با DSNmysql://...&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 نشوند.