Files
clinicpro/.claude/prompt/coolify-docker-production-hardening.md

13 KiB
Raw Permalink Blame History

سخت‌سازی و بازبینی Production استک Docker برای Coolify (MariaDB/Redis مستقل)

پروژه

clinicpro (backend / deploy / infra)

⚠️ قانون شماره ۱ — اول مستندات، بعد کد

قبل از هرگونه تغییر در فایل‌های Docker، مستندات رسمی Coolify را با WebFetch بخوان و هر تصمیم را بر پایه‌ی آن‌ها بگیر، نه دانش قبلی. دستِ‌کم این صفحات:

موضوعاتی که باید از داک تأیید شوند (نه از حافظه): 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 جز coolify external.

۱. اجرای غیر-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 -r get_headers یا اگر در image موجود است curl -fsS؛ از روی Dockerfile مطمئن شو کدام در runtime هست — curl در build استفاده شده ولی شاید در runtime نمانده باشد، چک کن).
  • در Dockerfile کپی + executable کن، در docker-compose.yml healthcheck سرویس 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}).
  • مطمئن شو فقط شبکه‌ی coolify external است و هر ۳ سرویس به آن وصل‌اند.
  • 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