diff --git a/.claude/prompt/coolify-docker-production-hardening.md b/.claude/prompt/coolify-docker-production-hardening.md new file mode 100644 index 00000000..9fadbf78 --- /dev/null +++ b/.claude/prompt/coolify-docker-production-hardening.md @@ -0,0 +1,159 @@ +# سخت‌سازی و بازبینی 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-`/`redis-` وصل می‌شود. هیچ `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 | grep messenger:consume` | +| کاربر اجرا | **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): + +```bash +# 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 diff --git a/Dockerfile b/Dockerfile index fe289a57..095c4d49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,19 +85,36 @@ RUN [ -f .env ] || printf 'APP_ENV=prod\nAPP_DEBUG=0\n' > .env # Container configuration. COPY docker/php/php.ini /usr/local/etc/php/conf.d/zz-app.ini +COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/zz-opcache.ini COPY docker/php/zz-pool.conf /usr/local/etc/php-fpm.d/zz-pool.conf 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 \ +COPY docker/healthcheck.sh /usr/local/bin/healthcheck.sh + +# Make nginx run as the non-root www-data user: +# - drop the `user nginx;` directive (ignored + warns when master isn't root) +# - put the pid file in a www-data-writable path (default /run/nginx is root-only) +RUN sed -i '/^user /d' /etc/nginx/nginx.conf \ + && sed -i '1i pid /tmp/nginx.pid;' /etc/nginx/nginx.conf + +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh \ && mkdir -p var/cache var/log var/uploads public/uploads config/jwt \ # Source may be copied with restrictive (0600) host perms; ensure the runtime # user (www-data) can read all app files — opcache preload runs as www-data # and otherwise fails with "Permission denied" on /app/config/preload.php. && chmod -R a+rX /app \ - && chown -R www-data:www-data var public/uploads config/jwt + && chown -R www-data:www-data var public/uploads config/jwt \ + # nginx (run as www-data) needs to write its temp/cache/log dirs. + && chown -R www-data:www-data /var/lib/nginx /var/log/nginx 2>/dev/null || true -EXPOSE 80 +# Drop privileges: the entrypoint, supervisord, php-fpm and nginx all run as +# www-data. nginx binds 8080 (non-privileged) so root is never required. +USER www-data + +EXPOSE 8080 +HEALTHCHECK --interval=15s --timeout=5s --retries=5 --start-period=60s \ + CMD ["/usr/local/bin/healthcheck.sh"] ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/docker-compose.yml b/docker-compose.yml index d8b5c4f9..8b70b2a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,11 +64,9 @@ services: RUN_INIT: "1" # runs JWT keygen + migrations on start (only this service) volumes: *app-volumes networks: [coolify] - # Hit the real /health route through nginx+fpm — verifies the app actually - # boots and serves (not just that port 80 is open). Uses PHP (always present) - # so we don't depend on curl/wget being in the image; fails on non-2xx. + # Hit the real /health route (nginx on 8080, non-root) via docker/healthcheck.sh. healthcheck: - test: ["CMD", "php", "-r", "$h=@get_headers('http://127.0.0.1:80/health'); exit($h && strpos($h[0],'200')!==false ? 0 : 1);"] + test: ["CMD", "/usr/local/bin/healthcheck.sh"] interval: 15s timeout: 5s retries: 5 @@ -85,6 +83,9 @@ services: volumes: *app-volumes networks: [coolify] depends_on: [app] + # Messenger consumers handle SIGTERM gracefully — give the in-flight message + # time to finish before the container is killed. + stop_grace_period: 30s # No port to probe — just confirm the consumer process is alive (busybox ps). healthcheck: test: ["CMD-SHELL", "ps -o args 2>/dev/null | grep -q '[m]essenger:consume async' || exit 1"] @@ -104,6 +105,7 @@ services: volumes: *app-volumes networks: [coolify] depends_on: [app] + stop_grace_period: 30s healthcheck: test: ["CMD-SHELL", "ps -o args 2>/dev/null | grep -q '[m]essenger:consume scheduler_default' || exit 1"] interval: 30s diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 00000000..489358c0 --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Container healthcheck — hits the real Symfony /health route through nginx+fpm, +# so a green status means the app actually boots and serves (not just that the +# port is open). Uses PHP (always present in the image) to avoid depending on +# curl/wget. nginx listens on 8080 (non-root). Exit 0 = healthy, 1 = unhealthy. +exec php -r '$h=@get_headers("http://127.0.0.1:8080/health"); exit($h && strpos($h[0]," 200")!==false ? 0 : 1);' diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index cec0df51..9aebc9fc 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -1,5 +1,8 @@ server { - listen 80 default_server; + # Non-privileged port so the whole stack can run as www-data (non-root). + # Coolify/Traefik routes to whatever port the service exposes — assign 8080 + # as the service port in Coolify. + listen 8080 default_server; server_name _; root /app/public; diff --git a/docker/php/opcache.ini b/docker/php/opcache.ini new file mode 100644 index 00000000..78e80ac7 --- /dev/null +++ b/docker/php/opcache.ini @@ -0,0 +1,13 @@ +; OPcache — production tuning for ClinicPro. +; validate_timestamps=0: code never changes at runtime in an immutable image, so +; skip stat() checks for max throughput. (A new deploy = a new image.) +; preload: warms the Symfony container/classes into shared memory at FPM start; +; runs as www-data, so app files must be readable by www-data (see Dockerfile). +opcache.enable = 1 +opcache.enable_cli = 0 +opcache.memory_consumption = 256 +opcache.max_accelerated_files = 20000 +opcache.validate_timestamps = 0 +opcache.interned_strings_buffer = 16 +opcache.preload = /app/config/preload.php +opcache.preload_user = www-data diff --git a/docker/php/php.ini b/docker/php/php.ini index f3ba8a1f..0160ae7b 100644 --- a/docker/php/php.ini +++ b/docker/php/php.ini @@ -6,16 +6,6 @@ max_execution_time = 60 expose_php = Off date.timezone = Asia/Tehran -; OPcache (production) -opcache.enable = 1 -opcache.enable_cli = 0 -opcache.memory_consumption = 256 -opcache.max_accelerated_files = 20000 -opcache.validate_timestamps = 0 -opcache.interned_strings_buffer = 16 -opcache.preload = /app/config/preload.php -opcache.preload_user = www-data - ; Realpath cache (perf) realpath_cache_size = 4096k realpath_cache_ttl = 600 diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 93313321..c617c9ab 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -1,14 +1,28 @@ [supervisord] nodaemon=true -user=root +# Run the whole stack as the non-root www-data user (security hardening). +# nginx listens on 8080 (non-privileged) so root is not needed to bind the port. +user=www-data logfile=/dev/stdout logfile_maxbytes=0 -pidfile=/run/supervisord.pid +pidfile=/tmp/supervisord.pid + +[unix_http_server] +file=/tmp/supervisor.sock + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface [program:php-fpm] command=php-fpm -F autorestart=true priority=10 +# Forward SIGTERM (graceful) and give workers time to finish in-flight requests. +stopsignal=TERM +stopwaitsecs=15 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr @@ -18,6 +32,8 @@ stderr_logfile_maxbytes=0 command=nginx -g 'daemon off;' autorestart=true priority=20 +stopsignal=QUIT +stopwaitsecs=15 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 9d070de6..0ce03776 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -12,7 +12,7 @@ | سرویس | نقش | نکته | |---|---|---| -| `app` | PHP-FPM + Nginx (وب) | تنها سرویسی که `RUN_INIT=1` دارد؛ مهاجرت DB و تولید کلید JWT را اجرا می‌کند. دامنه‌ها را به این سرویس (پورت 80) وصل کن. | +| `app` | PHP-FPM + Nginx (وب، non-root، پورت ۸۰۸۰) | تنها سرویسی که `RUN_INIT=1` دارد؛ مهاجرت DB و تولید کلید JWT را اجرا می‌کند. دامنه‌ها را به این سرویس (پورت ۸۰۸۰) وصل کن. | | `worker-async` | مصرف‌کننده صف async (SMS و کارهای async) | `messenger:consume async` | | `worker-scheduler` | زمان‌بند | هر ۱ دقیقه نوبت‌های پرداخت‌نشده را منقضی می‌کند | @@ -60,7 +60,9 @@ ## مرحله ۳ — دامنه‌ها -همه‌ی دامنه‌های سرو شونده را به سرویس **`app`** (پورت 80) اختصاص بده — هم دامنه‌ی API و هم همه‌ی دامنه‌های فرانت‌اند. Coolify لیست دامنه‌ی جدا‌شده با کاما را روی یک سرویس قبول می‌کند و TLS را خودش صادر می‌کند. +همه‌ی دامنه‌های سرو شونده را به سرویس **`app`** اختصاص بده — هم دامنه‌ی API و هم همه‌ی دامنه‌های فرانت‌اند. Coolify لیست دامنه‌ی جدا‌شده با کاما را روی یک سرویس قبول می‌کند و TLS را خودش صادر می‌کند. + +> ⚠️ **پورت سرویس = `8080`** (نه ۸۰). کانتینر non-root اجرا می‌شود و nginx روی پورت غیرممتاز ۸۰۸۰ گوش می‌دهد. در Coolify port سرویس `app` را روی **۸۰۸۰** بگذار (Traefik به هر پورتی روت می‌کند — طبق داک، هر پورتی مجاز است). > اجازه‌دادن CORS و host فرانت‌اندها از طریق متغیرهای `CORS_ALLOW_ORIGIN` / `ALLOWED_FRONTEND_HOSTS` کنترل می‌شود، نه دامنه‌ی Coolify. @@ -164,12 +166,24 @@ php bin/console app:create-admin ### بررسی سلامت -- healthcheck سرویس `app`: `php fsockopen 127.0.0.1:80`. +- healthcheck سرویس `app`: `docker/healthcheck.sh` route واقعی `/health` را روی پورت ۸۰۸۰ می‌زند (نه فقط چک پورت). +- workerها: healthcheck زنده‌بودن پروسه‌ی `messenger:consume` با `ps`. - Swagger: `https:///api/doc` - پنل ادمین: `https:///admin` --- +## امنیت و منابع + +- **non-root:** کل استک (supervisord + php-fpm + nginx) با کاربر `www-data` اجرا می‌شود؛ nginx روی پورت غیرممتاز ۸۰۸۰. هیچ پروسه‌ای root نیست. +- **بدون secret در image/repo:** همه‌ی مقادیر حساس از تب Environment Variables کولیفای (`${...}`)؛ `.env` مخزن gitignore است و در image یک `.env` حداقلی فقط `APP_ENV=prod` ساخته می‌شود. +- **Resource Limits:** داک Coolify limits را از UI منبع می‌گیرد (نه لزوماً از compose). پیشنهاد شروع: + - `app`: حافظه ~۵۱۲MB–۱GB، CPU ~۱ + - هر worker: حافظه ~۲۵۶MB، CPU ~۰٫۵ + در صفحه‌ی هر سرویس Coolify تنظیم کن و با مصرف واقعی تنظیم نهایی کن. + +--- + ## دیپلوی‌های بعدی push روی برنچ متصل (یا Deploy دستی). در هر ری‌دیپلوی: