From e966e1180740b7eec2f3c200578ec68432145485 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sat, 27 Jun 2026 21:27:58 +0330 Subject: [PATCH] feat: update Docker configuration and add deployment documentation for Coolify --- .dockerignore | 4 + Dockerfile | 3 +- docker/php/zz-pool.conf | 7 ++ docs/DEPLOY.md | 167 ++++++++++++++++++++++++++++++++ package-lock.json | 10 +- package.json | 6 -- templates/public/home.html.twig | 49 +++++++++- yarn.lock | 8 +- 8 files changed, 233 insertions(+), 21 deletions(-) create mode 100644 docker/php/zz-pool.conf create mode 100644 docs/DEPLOY.md diff --git a/.dockerignore b/.dockerignore index b38a5502..84421bc3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,6 +23,10 @@ /phpstan.neon # Local env & secrets +# NOTE: .env IS kept in the image — Symfony's Dotenv::bootEnv requires it to +# exist. Its APP_ENV=dev default is harmless in prod: compose sets the real +# APP_ENV=prod env var, and Dotenv never overwrites an already-set var +# (provided FPM clear_env=no, see docker/php/zz-pool.conf). .env.local .env.*.local /config/jwt/*.pem diff --git a/Dockerfile b/Dockerfile index 936b5331..80747bb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN composer install \ # ============================================================ # Stage 2 — Frontend assets (Webpack Encore / React 19) # ============================================================ -FROM node:20-alpine AS assets +FROM node:22-alpine AS assets WORKDIR /app # vendor is required: package.json references "@symfony/ux-react": "file:vendor/symfony/ux-react/assets" COPY --from=vendor /app/vendor ./vendor @@ -50,6 +50,7 @@ COPY --from=assets /app/public/build ./public/build # Container configuration. COPY docker/php/php.ini /usr/local/etc/php/conf.d/zz-app.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 diff --git a/docker/php/zz-pool.conf b/docker/php/zz-pool.conf new file mode 100644 index 00000000..216f5e84 --- /dev/null +++ b/docker/php/zz-pool.conf @@ -0,0 +1,7 @@ +; Production FPM pool overrides for ClinicPro. +; Default www.conf ships clear_env = yes, which strips the container +; environment (APP_ENV=prod, DATABASE_URL, ...) from PHP worker processes. +; Without this, PHP falls back to the baked .env (APP_ENV=dev) and boots the +; wrong environment. Keep the container env so compose-provided vars reach PHP. +[www] +clear_env = no diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 00000000..5bb8da39 --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,167 @@ +# راهنمای دیپلوی ClinicPro (Coolify + Docker Compose) + +این راهنما برای دیپلوی بک‌اند ClinicPro روی **Coolify** با استفاده از `docker-compose.yml` نوشته شده. + +> توجه: این `docker-compose.yml` فقط برای **production** است. محیط لوکال از `compose.yaml` خودِ ddev استفاده می‌کند — این دو را با هم اشتباه نگیر. + +--- + +## معماری استک + +`docker-compose.yml` پنج سرویس بالا می‌آورد: + +| سرویس | نقش | نکته | +|---|---|---| +| `app` | PHP-FPM + Nginx (وب) | تنها سرویسی که `RUN_INIT=1` دارد؛ مهاجرت DB و تولید کلید JWT را اجرا می‌کند. دامنه‌ها را به این سرویس (پورت 80) وصل کن. | +| `worker-async` | مصرف‌کننده صف async (SMS و کارهای async) | `messenger:consume async` | +| `worker-scheduler` | زمان‌بند | هر ۱ دقیقه نوبت‌های پرداخت‌نشده را منقضی می‌کند | +| `mariadb` | پایگاه‌داده MariaDB 11.8 | healthcheck دارد؛ بقیه منتظرش می‌مانند | +| `redis` | صف Messenger + کش | `appendonly yes` (ماندگار) | + +**ولوم‌های ماندگار** (داده‌ها در ری‌دیپلوی حفظ می‌شوند): + +- `jwt_keys` → کلیدهای JWT +- `uploads_public` و `uploads_var` → فایل‌های آپلودی +- `mariadb_data` → داده DB +- `redis_data` → داده Redis + +--- + +## پیش‌نیازها + +- نمونه‌ی Coolify در حال اجرا با Traefik (پیش‌فرض Coolify). +- ریپوی Git متصل به Coolify. +- رکوردهای DNS برای دامنه‌ی API و همه‌ی دامنه‌های فرانت‌اند که به سرور اشاره کنند. + +--- + +## مرحله ۱ — ساخت منبع (Resource) در Coolify + +1. **New Resource → Docker Compose** (Build Pack: `Docker Compose`). +2. ریپو و برنچ را انتخاب کن. +3. فیلد **Compose file** را روی `docker-compose.yml` بگذار. +4. `networks:` سفارشی تعریف **نکن** — شبکه را Coolify مدیریت می‌کند؛ شبکه‌ی سفارشی روتینگ Traefik را می‌شکند. + +--- + +## مرحله ۲ — دامنه‌ها + +همه‌ی دامنه‌های سرو شونده را به سرویس **`app`** (پورت 80) اختصاص بده — هم دامنه‌ی API و هم همه‌ی دامنه‌های فرانت‌اند. Coolify لیست دامنه‌ی جدا‌شده با کاما را روی یک سرویس قبول می‌کند و TLS را خودش صادر می‌کند. + +> اجازه‌دادن CORS و host فرانت‌اندها از طریق متغیرهای `CORS_ALLOW_ORIGIN` / `ALLOWED_FRONTEND_HOSTS` کنترل می‌شود، نه دامنه‌ی Coolify. + +--- + +## مرحله ۳ — متغیرهای محیطی + +از `.env.coolify.example` کپی کن و در تب **Environment Variables** منبع Coolify بگذار. + +فقط متغیرهایی که در `docker-compose.yml` به‌صورت `${...}` ارجاع شده‌اند لازم‌اند. `DATABASE_URL` / `MESSENGER_TRANSPORT_DSN` / `REDIS_URL` داخل خود compose از روی نام سرویس‌ها ساخته می‌شوند. + +### اسرار (الزامی — قبل از اولین دیپلوی) + +```bash +APP_SECRET= # php -r "echo bin2hex(random_bytes(32));" +JWT_PASSPHRASE= # openssl rand -hex 32 (باید قبل از اولین استارت موجود باشد؛ کلید JWT با همین ساخته می‌شود) +DB_PASSWORD= # رمز کاربر DB اپلیکیشن +DB_ROOT_PASSWORD= # رمز root مریادی‌بی +``` + +> ⚠️ `JWT_PASSPHRASE` را بعد از اولین دیپلوی عوض نکن — کلید JWT یک‌بار با همین passphrase تولید و روی ولوم `jwt_keys` ماندگار می‌شود. تغییرش همه‌ی توکن‌ها را می‌شکند. + +در Coolify می‌توانی به‌جای هاردکد از magic var استفاده کنی: + +```bash +DB_PASSWORD=${SERVICE_PASSWORD_DB} +APP_SECRET=${SERVICE_HEX_APPSECRET} +``` + +### دامنه‌ها و CORS + +```bash +APP_BASE_URL=https://api.nobat724.com # دامنه‌ی خودِ بک‌اند (برای callback پرداخت و URLهای مطلق) +``` + +`ALLOWED_FRONTEND_HOSTS` و `CORS_ALLOW_ORIGIN` از `docker/frontend-domains.json` **تولید** می‌شوند. برای اضافه/حذف دامنه‌ی شهر: + +```bash +# آن فایل را ویرایش کن، سپس: +php docker/gen-cors-env.php # روی سرور +# یا لوکال: +ddev exec php docker/gen-cors-env.php +``` + +خروجی را در Coolify جایگزین کن. + +### ریورس‌پراکسی + +```bash +TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1 +``` + +تا Symfony به هدرهای `X-Forwarded-*` ترافیک اعتماد کند. + +### api.ir (استعلام هویت — Shahkar / IbanMatch) + +```bash +API_IR_BASE_URL=https://s.api.ir +API_IR_TOKEN= # خالی => fail-closed (تأیید نماینده رد می‌شود) +``` + +### چیزهایی که env لازم ندارند + +- کلیدهای SMS (kavenegar/rangineh) و درگاه پرداخت (mellat/sep) از **DB ("تنظیمات سایت")** خوانده می‌شوند، نه env. +- `REFRESH_TOKEN_TTL` / `OTP_TTL` / `MAX_FILE_SIZE_BYTES` در خود compose ثابت‌اند. + +--- + +## مرحله ۴ — دیپلوی + +روی **Deploy** بزن. در اولین استارت به‌صورت خودکار این‌ها اتفاق می‌افتد (`entrypoint.sh` + `RUN_INIT=1` روی سرویس `app`): + +1. مالکیت `var`, `public/uploads`, `config/jwt` به `www-data` داده می‌شود. +2. کلید JWT اگر روی ولوم نباشد ساخته می‌شود (`--skip-if-exists`). +3. کش prod پاک و warmup می‌شود. +4. مهاجرت‌های DB با `--all-or-nothing` اعمال می‌شوند (ترنزکشن). + +> ورکرها `RUN_INIT=0` دارند تا مهاجرت/تولید کلید با هم تداخل نکنند. + +--- + +## مرحله ۵ — پس از اولین دیپلوی + +### ساخت اولین ادمین + +```bash +# داخل کانتینر سرویس app +php bin/console app:create-admin +``` + +### بررسی سلامت + +- healthcheck سرویس `app`: `php fsockopen 127.0.0.1:80`. +- Swagger: `https:///api/doc` +- پنل ادمین: `https:///admin` + +--- + +## دیپلوی‌های بعدی + +push روی برنچ متصل (یا Deploy دستی). در هر ری‌دیپلوی: + +- ایمیج دوباره build می‌شود (vendor + اسمبل فرانت‌اند multi-stage). +- مهاجرت‌های جدید روی استارت `app` اعمال می‌شوند. +- ولوم‌ها حفظ می‌شوند (DB، آپلودها، کلیدهای JWT، Redis سالم می‌مانند). + +--- + +## عیب‌یابی + +| نشانه | علت محتمل | +|---|---| +| ارورهای CORS در فرانت | `CORS_ALLOW_ORIGIN` با دامنه نمی‌خواند؛ از `gen-cors-env.php` بازتولید کن | +| `app` بالا نمی‌آید، منتظر DB می‌ماند | healthcheck `mariadb` رد نشده؛ لاگ mariadb را ببین | +| ۴۰۱/توکن نامعتبر بعد از ری‌دیپلوی | `JWT_PASSPHRASE` تغییر کرده یا ولوم `jwt_keys` پاک شده | +| IPها/HTTPS اشتباه پشت پراکسی | `TRUSTED_PROXIES` ست نشده | +| مهاجرت اجرا نشد | فقط `app` با `RUN_INIT=1` اجرا می‌کند؛ مطمئن شو override نشده | +| تأیید نماینده رد می‌شود | `API_IR_TOKEN` خالی است (fail-closed) | diff --git a/package-lock.json b/package-lock.json index 59418851..dbab6a1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,12 +49,6 @@ "typescript": "^5.0.0", "webpack": "^5.72", "webpack-cli": "^6.0.0" - }, - "optionalDependencies": { - "@tailwindcss/oxide-darwin-arm64": "^4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "^4.3.0", - "lightningcss-darwin-arm64": "^1.32.0", - "lightningcss-linux-arm64-gnu": "^1.32.0" } }, "node_modules/@adobe/css-tools": { @@ -15902,6 +15896,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15969,6 +15964,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -20035,6 +20031,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20118,6 +20115,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index d98cb403..e9189ef6 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,6 @@ "zod": "^3.0.0", "zustand": "^5.0.0" }, - "optionalDependencies": { - "@tailwindcss/oxide-darwin-arm64": "^4.3.0", - "@tailwindcss/oxide-linux-arm64-gnu": "^4.3.0", - "lightningcss-darwin-arm64": "^1.32.0", - "lightningcss-linux-arm64-gnu": "^1.32.0" - }, "license": "UNLICENSED", "private": true, "scripts": { diff --git a/templates/public/home.html.twig b/templates/public/home.html.twig index d4518109..66938339 100644 --- a/templates/public/home.html.twig +++ b/templates/public/home.html.twig @@ -59,7 +59,7 @@
- ۹۵٪ + ۹۵٪ رضایت بیماران
@@ -68,7 +68,7 @@
- ۳۸ + ۳۸ نوبت امروز
@@ -553,10 +553,12 @@ var btn2 = document.getElementById('openRegModal2'); if (btn1) btn1.addEventListener('click', openRegModal); + if (btn2) btn2.addEventListener('click', openRegModal); + document.querySelectorAll('.reg-card').forEach(function (card) { card.addEventListener('click', function () { document.querySelectorAll('.reg-card').forEach(function (c) { @@ -567,6 +569,7 @@ var radio = card.querySelector('input[type=radio]'); if (radio) radio.checked = true; + selectedType = card.dataset.type; submitBtn.disabled = false; submitBtn.style.opacity = '1'; @@ -582,6 +585,7 @@ var msg = document.getElementById('regMsg'); if (! selectedType) return; + if (! name) { showMsg('نام را وارد کنید', 'error'); return; @@ -613,7 +617,9 @@ return r.json(); if (data.success) { showMsg('درخواست شما ثبت شد. پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود.', 'success'); submitBtn.textContent = 'ارسال شد ✓'; -setTimeout(function() { closeRegModal(); }, 2200); +setTimeout(function () { +closeRegModal(); +}, 2200); } else { var errMsg = (data.errors && data.errors[0]) ? data.errors[0].message : 'خطایی رخ داد'; showMsg(errMsg, 'error'); @@ -663,6 +669,8 @@ msg.style.border = type === 'success' ? '1px solid #bbf7d0' : '1px solid #fecaca + +
@@ -764,6 +772,7 @@ navLinks.style.boxShadow = '0 16px 30px oklch(0.4 0.08 285 / 0.1)'; }); + var sections = [ 'hero', 'features', @@ -780,6 +789,7 @@ var el = document.getElementById(id); if (el && el.offsetTop <= y) cur = id; + }); links.forEach(function (a) { a.classList.toggle('active', a.getAttribute('href') === '#' + cur); @@ -805,6 +815,7 @@ reveals.forEach(function (el) { if (inView(el)) reveal(el); + }); if ('IntersectionObserver' in window) { var io = new IntersectionObserver(function (es) { @@ -822,6 +833,7 @@ reveals.forEach(function (el) { if (! el.classList.contains('in')) io.observe(el); + }); } var ticking = false; @@ -829,12 +841,14 @@ function sweep() { if (ticking) return; + ticking = true; requestAnimationFrame(function () { reveals.forEach(function (el) { if (inView(el)) reveal(el); + }); ticking = false; }); @@ -845,6 +859,33 @@ setTimeout(function () { reveals.forEach(reveal); }, 2800); } + +// نماد اعتماد الکترونیکی (Enamad) — فقط بعد از لود کامل صفحه تزریق می‌شود تا +// درخواست تصویرِ دامنه‌ی بیرونی با منابع خود سایت رقابت نکند و سرعت لود پایین نیاید. +function mountEnamad() { +var slot = document.getElementById('enamad-seal'); +if (! slot) +return; + +var a = document.createElement('a'); +a.setAttribute('referrerpolicy', 'origin'); +a.target = '_blank'; +a.href = 'https://trustseal.enamad.ir/?id=648891&Code=tudyxbILEtrmS2VZHeGr1LfmkrEerqeF'; +var img = document.createElement('img'); +img.setAttribute('referrerpolicy', 'origin'); +img.src = 'https://trustseal.enamad.ir/logo.aspx?id=648891&Code=tudyxbILEtrmS2VZHeGr1LfmkrEerqeF'; +img.alt = 'نماد اعتماد الکترونیکی'; +img.style.cursor = 'pointer'; +img.setAttribute('code', 'tudyxbILEtrmS2VZHeGr1LfmkrEerqeF'); +img.loading = 'lazy'; +a.appendChild(img); +slot.appendChild(a); +} +if (document.readyState === 'complete') { +mountEnamad(); +} else { +window.addEventListener('load', mountEnamad); +} })(); diff --git a/yarn.lock b/yarn.lock index c1cb1a3a..a057bb08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2744,12 +2744,12 @@ source-map-js "^1.2.1" tailwindcss "4.3.0" -"@tailwindcss/oxide-darwin-arm64@^4.3.0", "@tailwindcss/oxide-darwin-arm64@4.3.0": +"@tailwindcss/oxide-darwin-arm64@4.3.0": version "4.3.0" resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz" integrity sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ== -"@tailwindcss/oxide-linux-arm64-gnu@^4.3.0", "@tailwindcss/oxide-linux-arm64-gnu@4.3.0": +"@tailwindcss/oxide-linux-arm64-gnu@4.3.0": version "4.3.0" resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz" integrity sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg== @@ -4849,12 +4849,12 @@ leaflet@^1.9.0, leaflet@^1.9.4: resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz" integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA== -lightningcss-darwin-arm64@^1.32.0, lightningcss-darwin-arm64@1.32.0: +lightningcss-darwin-arm64@1.32.0: version "1.32.0" resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz" integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== -lightningcss-linux-arm64-gnu@^1.32.0, lightningcss-linux-arm64-gnu@1.32.0: +lightningcss-linux-arm64-gnu@1.32.0: version "1.32.0" resolved "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz" integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==