13 KiB
سختسازی و بازبینی Production استک Docker برای Coolify (MariaDB/Redis مستقل)
پروژه
clinicpro (backend / deploy / infra)
⚠️ قانون شماره ۱ — اول مستندات، بعد کد
قبل از هرگونه تغییر در فایلهای Docker، مستندات رسمی Coolify را با WebFetch بخوان و هر تصمیم را بر پایهی آنها بگیر، نه دانش قبلی. دستِکم این صفحات:
- https://coolify.io/docs/get-started/introduction
- https://coolify.io/docs/applications/build-packs/docker-compose
- https://coolify.io/docs/knowledge-base/docker/compose
- https://coolify.io/docs/databases/mariadb
- https://coolify.io/docs/databases/redis
موضوعاتی که باید از داک تأیید شوند (نه از حافظه): Docker Compose Build Pack، Connect to Predefined Network، Environment Variables، Healthcheck، Persistent Storage (Volumes)، Reverse Proxy/Traefik، Domains، Resource Limits، Raw Docker Compose.
اگر دانش قبلی با داک تناقض داشت → داک ملاک است. هر بخش که در داک پیدا نشد، در گزارش ذکر کن که «در داک نبود، بر اساس best practice تصمیم گرفته شد».
زمینه
استک دیپلوی این پروژه قبلاً ساخته و چند بار روی Coolify دیپلوی شده و چند باگ رفع شده (node20→22، FPM clear_env، preload perms، باگهای migration روی DB خالی، codeload flaky، نبود symfony/redis-messenger، اتصال به DB/Redis مستقل، healthcheck). این تسک یک بازبینی کامل و سختسازی استکِ موجود است تا کاملاً با مستندات Coolify همخوان شود و در دیپلوی خطا ندهد — نه ساخت از صفر. فایلهای فعلی را بهبود بده، دوبارهسازی نکن.
معماری تثبیتشده: MariaDB و Redis، Database Resourceهای مستقل Coolify هستند (نه داخل compose). اپ از طریق شبکهی predefined (coolify) و hostname mariadb-<uuid>/redis-<uuid> وصل میشود. هیچ localhost و هیچ hardcode.
وضعیت فعلی پروژه (واقعی — بررسی شده)
| واقعیت | مقدار |
|---|---|
| PHP | >=8.2 (image: php:8.2-fpm-alpine) |
| Symfony | 7.4.* |
| اکستنشنهای نصبشده | pdo_mysql, intl, opcache, redis (phpredis 6.1.0 از سورس GitHub) |
| اکستنشنهای لازمِ composer | فقط ext-ctype, ext-iconv (هر دو bundled) |
| Symfony cache adapter | cache.adapter.redis با %env(REDIS_URL)% (config/packages/cache.yaml) |
| Session | session.storage.factory.mock_file (فایلی، نه redis) |
| Messenger transports | async (redis، نیاز symfony/redis-messenger ✔ نصب شد)، scheduler_default، failed (doctrine) |
| سرویسها | app (RUN_INIT=1) + worker-async + worker-scheduler؛ image مشترک clinicpro-app:latest |
| شبکه | networks: [coolify] external روی هر ۳ سرویس |
| Healthcheck فعلی | app: php get_headers /health؛ workerها: `ps |
| کاربر اجرا | root (supervisord user=root, بدون USER در Dockerfile) ⚠️ |
| APCu | نصب نیست (cache از redis است؛ APCu فقط برای کش لوکال opcache/symfony مفید) |
| Graceful shutdown | supervisord PID 1، بدون STOPSIGNAL/stopwaitsecs تنظیمشده ⚠️ |
فایلهای مرتبط
| فایل | نقش |
|---|---|
clinicpro/Dockerfile |
multi-stage (vendor→assets→runtime)؛ نصب اکستنشن، perms، entrypoint |
clinicpro/docker-compose.yml |
استک Coolify (app + 2 worker)، networks، healthcheck |
clinicpro/.dockerignore |
استثناها |
clinicpro/docker/entrypoint.sh |
wait-for-DB، JWT keygen، cache، migrate (فقط RUN_INIT=1) |
clinicpro/docker/supervisord.conf |
php-fpm + nginx |
clinicpro/docker/php/php.ini |
opcache + perf |
clinicpro/docker/php/zz-pool.conf |
FPM pool: clear_env = no |
clinicpro/docker/nginx/default.conf |
nginx |
clinicpro/src/Shared/Controller/HealthController.php |
route /health |
clinicpro/docs/DEPLOY.md |
راهنمای دیپلوی |
clinicpro/.env.coolify.example |
نمونه env |
وظایف
هر وظیفه: اول داکِ مرتبط را بخوان، بعد اعمال کن، بعد با
docker buildx build --platform linux/amd64تست کن. هیچ مقدار hardcode، فقط${VAR}. هیچ custom network جزcoolifyexternal.
۱. اجرای غیر-root (Security Hardening)
الان کانتینر root اجرا میشود. باید non-root شود بدون شکستن preload (که الان opcache.preload_user = www-data است) و nginx/fpm.
- در
supervisord.conf، php-fpm pool از قبل بهصورت www-data اجرا میشود؛ ولی master supervisord و nginx master root هستند. بررسی کن آیا میتوان کل استک را باUSER www-dataاجرا کرد یا nginx master نیاز root دارد (برای bind پورت ۸۰). - اگر nginx به پورت ۸۰ نیاز root دارد: یا nginx را روی پورت 8080 (non-privileged) ببر و در compose/healthcheck و Coolify port را ۸۰۸۰ کن (Traefik به هر پورتی روت میکند)، سپس
USER www-dataدر Dockerfile +user=www-dataدر supervisord. - مطمئن شو
var/cache,var/log,var/uploads,public/uploads,config/jwtبرای www-data نوشتنیاند (chown موجود است؛ تکمیلش کن). - داک Coolify دربارهی port/Traefik را بخوان تا مطمئن شوی تغییر پورت به ۸۰۸۰ مشکلی ایجاد نمیکند.
اگر non-root باعث ریسک شکستن شد و در زمان محدود قابلاطمینان نبود، حداقل nginx/fpm workerها را non-root نگه دار (الان هستند) و در گزارش توضیح بده چرا master root ماند.
۲. healthcheck.sh مستقل + بازبینی healthcheckها
پرامپت یک docker/healthcheck.sh میخواهد. بهجای دستور inline طولانی در compose:
- یک
docker/healthcheck.shبساز که/healthرا چک کند (باphp -rget_headers یا اگر در image موجود استcurl -fsS؛ از روی Dockerfile مطمئن شو کدام در runtime هست —curlدر build استفاده شده ولی شاید در runtime نمانده باشد، چک کن). - در Dockerfile کپی + executable کن، در
docker-compose.ymlhealthcheck سرویسappرا به["CMD","/usr/local/bin/healthcheck.sh"]تغییر بده. - healthcheckهای worker (ps grep) را نگه دار یا به اسکریپت منتقل کن.
- مقادیر
interval/timeout/retries/start_periodرا با توصیهی داک Coolify (اگر دارد) همسو کن.
۳. opcache.ini جدا + بازبینی perfها
پرامپت opcache.ini جدا میخواهد. الان opcache داخل php.ini است.
- بخش opcache را از
php.iniبه یکdocker/php/opcache.iniمنتقل کن و در Dockerfile جداگانه کپی کن (/usr/local/etc/php/conf.d/). تنظیمات فعلی opcache دستنخورده بماند (preload, validate_timestamps=0, ...). - بررسی کن
realpath_cache,memory_limit,upload_max_filesize/post_max_size(الان 16M) باMAX_FILE_SIZE_BYTES=5242880(۵MB در env) همخوان است — اگر آپلود تا ۵MB است، 16M کافی است، نگه دار.
۴. APCu — تصمیم مستند
- چون Symfony cache از redis است نه apcu، نصب APCu اجباری نیست. ولی اگر میخواهی کش متادیتای Doctrine/annotation روی apcu لوکال باشد (سریعتر از redis برای read-heavy)، APCu را با همان روش source (یا
docker-php-ext-.../pecl-free) اضافه کن. - تصمیم را در گزارش مستند کن: یا «APCu لازم نیست چون cache=redis» یا «اضافه شد برای X». hardcode نکن؛ اگر اضافه شد، در runtime enable شود.
۵. Graceful shutdown / signal handling
- supervisord را برای shutdown تمیز تنظیم کن:
stopsignal,stopwaitsecsبرای php-fpm/nginx، و مطمئن شو سیگنال SIGTERM از داکر به فرزندها میرسد. - workerها (
messenger:consume) به SIGTERM پاسخ میدهند (Symfony Messenger خودش graceful است با--time-limit)؛ مطمئن شو داکر مستقیم پروسهی consume را اجرا میکند (الانcommand: php bin/console messenger:consume ...مستقیم است ✔) وstop_grace_periodدر compose برای workerها کافی است (پیشفرض 10s؛ شاید 30s بهتر باشد تا پیام در حال پردازش تمام شود).
۶. Resource limits (طبق داک Coolify)
- داک Coolify دربارهی Resource Limits را بخوان. اگر در compose پشتیبانی میشود (
deploy.resources.limitsدر سطح Coolify یا UI)، توصیهی mem/cpu برایappو workerها را درdocs/DEPLOY.mdمستند کن. اگر Coolify limits را از UI میگیرد نه compose، فقط در داک ذکر کن.
۷. بازبینی نهایی همخوانی با Coolify + بهروزرسانی DEPLOY.md
- مطمئن شو هیچ
localhost/127.0.0.1برای DB/Redis نیست (همه از${DATABASE_URL}/${REDIS_URL}/${MESSENGER_TRANSPORT_DSN}). - مطمئن شو فقط شبکهی
coolifyexternal است و هر ۳ سرویس به آن وصلاند. docs/DEPLOY.mdرا با هر تغییر (پورت non-root، healthcheck.sh، resource limits) بهروز کن.- چکلیست نهایی را در انتهای گزارش بده (پایین).
تست (اجباری — بعد از تغییرات)
روی linux/amd64 (معماری Coolify):
# build کامل
docker buildx build --platform linux/amd64 --target runtime --load -t clinicpro-test -f Dockerfile .
# اکستنشنها لود؟
docker run --rm --platform linux/amd64 --entrypoint php clinicpro-test -m | grep -iE 'pdo_mysql|intl|redis|opcache|apcu'
# non-root؟
docker run --rm --platform linux/amd64 --entrypoint id clinicpro-test
# boot prod بدون خطا (با env ساختگی)
docker run --rm --platform linux/amd64 -e APP_ENV=prod -e APP_DEBUG=0 -e APP_SECRET=x \
-e DATABASE_URL="mysql://u:p@127.0.0.1:3306/d?serverVersion=mariadb-11.8.0" \
-e JWT_PASSPHRASE=x -e CORS_ALLOW_ORIGIN='^x$' -e APP_BASE_URL=http://x \
-e ALLOWED_FRONTEND_HOSTS=x -e TRUSTED_PROXIES=10.0.0.0/8 -e API_IR_BASE_URL=x -e API_IR_TOKEN= \
-e MESSENGER_TRANSPORT_DSN=redis://127.0.0.1:6379/m -e REDIS_URL=redis://127.0.0.1:6379 \
--entrypoint php clinicpro-test bin/console about | grep -iE 'environment|debug'
# اتصال واقعی DB+Redis مستقل + migration + /health (مثل سشنهای قبل: کانتینر mariadb+redis روی یک docker network بساز،
# سپس app را با DATABASE_URL/REDIS_URL به آنها وصل کن، RUN_INIT=1، و /health را curl کن)
# اعتبار YAML
docker compose config # با env ساختگی
موارد ۱..۱۴ پرامپت اصلی (healthcheck، logها، اتصال DB/Redis، cache، migration، response، perms، restart، graceful shutdown، production mode) را با همین روش پوشش بده. هر مشکل را همانجا رفع کن.
نکات مهم
- هیچ MariaDB/Redis در compose — مستقلاند. compose فقط
app+ ۲ worker. - هیچ custom network جز
coolify(external) — طبق داک، شبکهی سفارشی routing Traefik را میشکند. - هیچ مقدار hardcode — همه
${VAR}؛ اسرار از تب Coolify. هیچ پسورد/توکن در repo یا image. - logها روی stdout/stderr — supervisord الان همین کار را میکند (
/dev/stdout)؛ حفظ کن. serverVersion=mariadb-11.8.0درDATABASE_URLو نسخهی Resource مستقل MariaDB باید یکی باشند.- فقط سرویس
app(RUN_INIT=1) migration میزند؛ workerها نباید (race). - image مشترک
clinicpro-app:latest(app build، workerها reuse) حفظ شود — خرابش نکن. - entrypoint فعلی wait-for-DB دارد (
doctrine:query:sql "SELECT 1")؛ اگر چیزی شکستی، آن را نگهدار. - تغییر صرفاً infra است؛ هیچ controller/Entity عوض نمیشود، پس
docs/api/*نیاز به آپدیت ندارد — فقطdocs/DEPLOY.md. - بعد از اتمام، چکلیست نهایی بده:
- ✅ Build موفق · ✅ Image Production Ready · ✅ Coolify Compatible · ✅ Compose Compatible
- ✅ Connect to Predefined Network · ✅ MariaDB External · ✅ Redis External
- ✅ Security (non-root یا توضیح) · ✅ Performance (opcache/fpm/nginx) · ✅ Healthcheck معتبر
- ✅ Zero Hardcoded Secret · ✅ Ready for Deployment