From 42bb72333327d038f8a69787a6c033ccf824f901 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 28 Jun 2026 11:28:29 +0330 Subject: [PATCH] refactor: separate MariaDB and Redis into independent Coolify resources --- .../split-db-redis-to-coolify-resources.md | 220 ++++++++++++++++++ .env.coolify.example | 28 ++- docker-compose.yml | 59 ++--- docker/entrypoint.sh | 14 ++ docker/php/php.ini | 4 +- docs/DEPLOY.md | 70 ++++-- 6 files changed, 323 insertions(+), 72 deletions(-) create mode 100644 .claude/prompt/split-db-redis-to-coolify-resources.md diff --git a/.claude/prompt/split-db-redis-to-coolify-resources.md b/.claude/prompt/split-db-redis-to-coolify-resources.md new file mode 100644 index 00000000..c3ff867a --- /dev/null +++ b/.claude/prompt/split-db-redis-to-coolify-resources.md @@ -0,0 +1,220 @@ +# جدا کردن MariaDB و Redis از docker-compose به Resourceهای مستقل Coolify + +## پروژه + +`clinicpro` (backend / deploy) + +## زمینه + +الان `clinicpro/docker-compose.yml` یک استک کامل است: `app` + `worker-async` + `worker-scheduler` + `mariadb` + `redis`. هر سه سرویس اپ از یک image مشترک (`clinicpro-app:latest`) استفاده می‌کنند و `mariadb`/`redis` داخل همین compose به‌صورت سرویس تعریف شده‌اند با volumeهای محلی (`mariadb_data`, `redis_data`). + +کاربر می‌خواهد در Coolify، **MariaDB و Redis را به‌صورت Database Resource مستقل** بسازد و مدیریت کند (بکاپ، monitoring، آپدیت جدا، ری‌استارت مستقل از اپ). یعنی این دو باید از `docker-compose.yml` اپ حذف شوند و اپ به آن‌ها از طریق شبکه‌ی Coolify وصل شود. + +## هدف + +`docker-compose.yml` فقط شامل سرویس‌های اپلیکیشن باشد (`app` + دو worker). MariaDB و Redis از compose حذف شوند و اپ به Resourceهای مستقل Coolify وصل شود. + +## قوانین Coolify (از داکیومنت‌ها — رعایت اجباری) + +- **شبکه:** Coolify برای هر Resource یک شبکه‌ی bridge ایزوله (بر اساس UUID) می‌سازد. سرویس‌های داخل یک compose با **نام سرویس** همدیگر را می‌بینند. اما برای دسترسی به یک Resource **دیگر** (دیتابیس مستقل)، باید گزینه‌ی **"Connect to Predefined Network"** روی استک اپ فعال شود و دیتابیس با شناسه‌ی کامل `-` به‌عنوان hostname رفرنس داده شود (مثل `postgresql-abc123...`؛ برای ما `mariadb-` / `redis-`). +- **هرگز `networks:` سفارشی** در compose تعریف نکن — Coolify خودش مدیریت می‌کند؛ شبکه‌ی سفارشی routing Traefik را می‌شکند و باعث قطعی متناوب می‌شود. +- **مقادیر اتصال (DATABASE_URL / REDIS_URL / MESSENGER_TRANSPORT_DSN)** دیگر به نام سرویس داخلی اشاره نمی‌کنند؛ از تب Environment Variables در Coolify ست می‌شوند و به hostname دیتابیس مستقل (`mariadb-` / `redis-`) اشاره می‌کنند. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `clinicpro/docker-compose.yml` | استک دیپلوی Coolify — باید mariadb/redis از آن حذف شود | +| `clinicpro/.env.coolify.example` | نمونه env vars برای Coolify — باید برای اتصال به دیتابیس مستقل به‌روز شود | +| `clinicpro/docs/DEPLOY.md` | راهنمای دیپلوی — باید مراحل ساخت Resource مستقل اضافه شود | +| `clinicpro/docker/entrypoint.sh` | فقط روی `app` (RUN_INIT=1) migration می‌زند — منطق بدون تغییر، فقط باید به DB مستقل وصل شود | + +## وضعیت فعلی + +استک فعلی mariadb/redis را داخل خودش دارد و `depends_on` به healthcheck آن‌ها گره خورده: + +```yaml +# x-app-env + 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 + +# x-app-depends + mariadb: + condition: service_healthy + redis: + condition: service_started + +services: + app: ... + depends_on: *app-depends + worker-async: ... + depends_on: *app-depends + worker-scheduler: ... + depends_on: *app-depends + + mariadb: + image: mariadb:11.8 + ... + volumes: + - mariadb_data:/var/lib/mysql + healthcheck: ... + + redis: + image: redis:7-alpine + command: redis-server --appendonly yes + volumes: + - redis_data:/data + +volumes: + jwt_keys: + uploads_public: + uploads_var: + mariadb_data: # ← متعلق به دیتابیس داخلی + redis_data: # ← متعلق به redis داخلی +``` + +## وظایف + +### ۱. حذف سرویس‌های `mariadb` و `redis` از `docker-compose.yml` + +- بلوک سرویس `mariadb` و `redis` کامل حذف شوند. +- volumeهای `mariadb_data` و `redis_data` از بخش `volumes:` حذف شوند (دیگر داخل این استک نیستند؛ داده‌ی DB مستقل توسط خود Resource نگه داشته می‌شود). `jwt_keys`, `uploads_public`, `uploads_var` بمانند. + +### ۲. حذف `depends_on` به mariadb/redis + +`x-app-depends` (و ارجاع `depends_on: *app-depends` در هر سه سرویس) دیگر بی‌معناست — این سرویس‌ها در استک نیستند. دو گزینه: + +- **پیشنهادی:** anchor `x-app-depends` و همه‌ی `depends_on: *app-depends` حذف شوند. به‌جای آن، تحمل قطعی موقت در استارت‌آپ تضمین شود (وظیفه ۴). +- اگر می‌خواهی ترتیب استارت `app` قبل از workerها حفظ شود، فقط workerها می‌توانند `depends_on: [app]` ساده داشته باشند (بدون condition healthcheck). + +نمونه‌ی نهایی موردانتظار (شِمای کلی): + +```yaml +x-app-env: &app-env + APP_ENV: prod + APP_DEBUG: "0" + APP_SECRET: ${APP_SECRET} + # به دیتابیس/Redis مستقل Coolify اشاره می‌کند — مقدار واقعی از تب Environment Variables + DATABASE_URL: ${DATABASE_URL} + MESSENGER_TRANSPORT_DSN: ${MESSENGER_TRANSPORT_DSN} + REDIS_URL: ${REDIS_URL} + JWT_SECRET_KEY: "%kernel.project_dir%/config/jwt/private.pem" + JWT_PUBLIC_KEY: "%kernel.project_dir%/config/jwt/public.pem" + 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 + +x-app-volumes: &app-volumes + - jwt_keys:/app/config/jwt + - uploads_public:/app/public/uploads + - uploads_var:/app/var/uploads + +services: + app: + image: clinicpro-app:latest + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + environment: + <<: *app-env + RUN_INIT: "1" + volumes: *app-volumes + healthcheck: + test: ["CMD", "php", "-r", "exit(@fsockopen('127.0.0.1', 80) ? 0 : 1);"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + + worker-async: + image: clinicpro-app:latest + restart: unless-stopped + command: php bin/console messenger:consume async --time-limit=3600 --memory-limit=128M -v + environment: + <<: *app-env + RUN_INIT: "0" + volumes: *app-volumes + depends_on: [app] + + worker-scheduler: + image: clinicpro-app:latest + restart: unless-stopped + command: php bin/console messenger:consume scheduler_default --time-limit=3600 -v + environment: + <<: *app-env + RUN_INIT: "0" + volumes: *app-volumes + depends_on: [app] + +volumes: + jwt_keys: + uploads_public: + uploads_var: +``` + +> توجه: `DATABASE_URL`/`REDIS_URL`/`MESSENGER_TRANSPORT_DSN` از hardcode داخل compose به `${...}` تبدیل شدند تا از Coolify (با hostname دیتابیس مستقل) تزریق شوند. `DB_PASSWORD`/`DB_ROOT_PASSWORD` دیگر در این compose استفاده نمی‌شوند (متعلق به Resource دیتابیس‌اند). + +### ۳. به‌روزرسانی `.env.coolify.example` + +- متغیرهای `DB_PASSWORD` / `DB_ROOT_PASSWORD` که مخصوص mariadb داخلی بودند را حذف یا به بخش «این‌ها را موقع ساخت Database Resource ست کن» منتقل کن. +- سه متغیر اتصال اضافه/مستندسازی شوند که به Resource مستقل اشاره می‌کنند. hostname باید `-` باشد (UUID از صفحه‌ی Resource در Coolify گرفته می‌شود): + +```bash +# اتصال به MariaDB مستقل Coolify (hostname = mariadb- از صفحه‌ی Resource) +DATABASE_URL="mysql://clinic:PASSWORD@mariadb-XXXXXXXX:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4" + +# اتصال به Redis مستقل Coolify (hostname = redis-) +REDIS_URL="redis://redis-XXXXXXXX:6379" +MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages" +``` + +- توضیح بده که اگر Redis مستقل Coolify رمز دارد، فرمت `redis://:PASSWORD@redis-:6379` است. + +### ۴. تحمل قطعی DB در استارت‌آپ (چون دیگر `depends_on: service_healthy` نیست) + +چون اپ دیگر منتظر healthcheck دیتابیس نمی‌ماند، ممکن است هنگام بوت، DB هنوز آماده نباشد و `doctrine:migrations:migrate` در `entrypoint.sh` شکست بخورد. در `docker/entrypoint.sh`، قبل از migration یک انتظار کوتاه برای آماده‌شدن DB اضافه کن (مثلاً حلقه‌ای که با یک کوئری ساده‌ی Doctrine اتصال را چک می‌کند، چند بار با تأخیر retry): + +```sh +# منتظر آماده‌شدن دیتابیس مستقل بمان (حداکثر ~60s) قبل از migration +if [ "${RUN_INIT:-1}" = "1" ]; then + i=0 + until php bin/console dbal:run-sql "SELECT 1" >/dev/null 2>&1; do + i=$((i+1)) + [ "$i" -ge 30 ] && echo "DB not reachable after 60s" && exit 1 + echo "waiting for database... ($i)" + sleep 2 + done + # سپس: JWT keygen + cache + migrations (منطق فعلی) +fi +``` + +منطق فعلی JWT keygen / cache:clear / cache:warmup / migrate دست‌نخورده بماند؛ فقط حلقه‌ی انتظار قبل از آن اضافه شود. (در صورت نبودن دستور `dbal:run-sql`، از `doctrine:query:sql "SELECT 1"` استفاده کن — اول با `ddev exec php bin/console list doctrine | grep -i sql` چک کن کدام موجود است.) + +### ۵. به‌روزرسانی `docs/DEPLOY.md` + +بخش «استک» و «متغیرهای محیطی» را اصلاح کن: +- جدول سرویس‌ها: `mariadb` و `redis` از استک اپ حذف شده‌اند و حالا **Database Resource مستقل** Coolify هستند. +- مراحل جدید: (الف) ساخت Resource مستقل MariaDB در Coolify، (ب) ساخت Resource مستقل Redis، (ج) فعال‌کردن **"Connect to Predefined Network"** روی استک اپ، (د) ست‌کردن `DATABASE_URL`/`REDIS_URL`/`MESSENGER_TRANSPORT_DSN` با hostname `-`. +- توضیح بده که volumeهای `mariadb_data`/`redis_data` دیگر در استک اپ نیستند؛ ماندگاری داده برعهده‌ی خود Resourceهاست. + +## نکات مهم + +- **هیچ `networks:` سفارشی** اضافه نشود؛ اتصال cross-resource فقط با "Connect to Predefined Network" + hostname `-`. +- مقادیر `DATABASE_URL`/`REDIS_URL`/`MESSENGER_TRANSPORT_DSN` نباید داخل compose hardcode شوند؛ از `${...}` و تب Coolify بیایند (چون UUID برای هر نصب فرق دارد). +- `serverVersion=mariadb-11.8.0` در `DATABASE_URL` حفظ شود (Doctrine برای تولید SQL درست لازم دارد) — مطمئن شو نسخه‌ی Resource مستقل MariaDB هم 11.8 انتخاب شود تا با migrationها هم‌خوان باشد. +- فقط سرویس `app` (`RUN_INIT=1`) migration می‌زند؛ workerها (`RUN_INIT=0`) نباید — این رفتار حفظ شود تا race در migration پیش نیاید. +- `image: clinicpro-app:latest` مشترک حفظ شود (app یک‌بار build، workerها reuse) — این فیکس قبلی برای جلوگیری از سه‌بار build است؛ خرابش نکن. +- بعد از تغییر، با `docker compose config` اعتبار YAML را چک کن. +- این تغییر فقط deploy/infra است؛ هیچ controller/route/Entity تغییر نمی‌کند، پس نیازی به آپدیت `docs/api/*` نیست (فقط `docs/DEPLOY.md`). +``` diff --git a/.env.coolify.example b/.env.coolify.example index 6d257a47..e0d6bba4 100644 --- a/.env.coolify.example +++ b/.env.coolify.example @@ -2,11 +2,25 @@ # Coolify Environment Variables — ClinicPro (Docker Compose) # ------------------------------------------------------------ # Copy these into the Coolify resource's "Environment Variables" tab. -# Only the variables referenced as ${...} in docker-compose.yml -# need to be set here. DATABASE_URL / MESSENGER_TRANSPORT_DSN / REDIS_URL -# are built INSIDE the compose file from the mariadb/redis service names. +# Only the variables referenced as ${...} in docker-compose.yml need to be set. +# +# MariaDB and Redis are SEPARATE Coolify Database Resources (not in the compose). +# So DATABASE_URL / REDIS_URL / MESSENGER_TRANSPORT_DSN must be set HERE, pointing +# at those resources by their internal hostname (mariadb- / redis-). +# Steps: +# 1. Create a standalone MariaDB 11.8 resource + a standalone Redis resource. +# 2. Enable "Connect to Predefined Network" on this app stack. +# 3. Copy each resource's internal hostname/credentials into the URLs below. # ============================================================ +# ── Database / Redis connections (point at the SEPARATE Coolify resources) ── +# Replace mariadb-XXXXXXXX / redis-XXXXXXXX with the real hostname shown on each +# resource's page (Internal URL). serverVersion MUST match the MariaDB resource (11.8). +DATABASE_URL="mysql://clinic:DB_PASSWORD@mariadb-XXXXXXXX:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4" +REDIS_URL="redis://redis-XXXXXXXX:6379" +MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages" +# If the Redis resource has a password: redis://:PASSWORD@redis-XXXXXXXX:6379 + # ── Backend's own domain (single URL — used for payment callbacks & absolute URLs) ── # This is the API host, NOT a frontend domain. APP_BASE_URL=https://api.nobat724.com @@ -24,11 +38,12 @@ CORS_ALLOW_ORIGIN='^https://(ahvaz\-nobat\.ir|arak\-nobat\.ir|ardabil\-nobat\.ir # ── Secrets (REQUIRED — set before the first deploy) ── APP_SECRET= # php -r "echo bin2hex(random_bytes(32));" JWT_PASSPHRASE= # openssl rand -hex 32 (must exist before first start: JWT keypair is generated with it) -DB_PASSWORD= # application DB user password -DB_ROOT_PASSWORD= # MariaDB root password + +# NOTE: DB_PASSWORD / DB_ROOT_PASSWORD are NO LONGER set here. The DB credentials +# now belong to the standalone MariaDB resource — set them when you create that +# resource, then embed the user password inside DATABASE_URL above. # Tip: in Coolify you may use magic vars instead of hardcoding, e.g. -# DB_PASSWORD=${SERVICE_PASSWORD_DB} # APP_SECRET=${SERVICE_HEX_APPSECRET} # ── Reverse proxy (Coolify/Traefik) ── @@ -44,3 +59,4 @@ API_IR_TOKEN= # • SMS keys (kavenegar/rangineh) and payment gateway keys (mellat/sep) are read # from the DB ("Site Settings"), NOT from env. No need to set them here. # • REFRESH_TOKEN_TTL / OTP_TTL / MAX_FILE_SIZE_BYTES are fixed in the compose file. +# • MariaDB & Redis are separate Coolify resources — see DATABASE_URL/REDIS_URL above. diff --git a/docker-compose.yml b/docker-compose.yml index e377257e..04b97ab7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,11 @@ # Coolify deployment — Build Pack: Docker Compose -# This is the production deploy stack (MariaDB + Redis). The ddev local env uses -# its own compose.yaml; do not confuse the two. +# This is the production APP stack (app + workers ONLY). MariaDB and Redis are +# run as SEPARATE Coolify Database Resources, not part of this compose. +# - Create a standalone MariaDB resource and a standalone Redis resource in Coolify. +# - Enable "Connect to Predefined Network" on THIS app stack. +# - Set DATABASE_URL / REDIS_URL / MESSENGER_TRANSPORT_DSN in Coolify's env tab, +# pointing at the resources via their internal hostnames (mariadb- / redis-). +# The ddev local env uses its own compose.yaml; do not confuse the two. # Select this file (docker-compose.yml) as the Compose file in the Coolify resource. # Do NOT define custom `networks:` — Coolify manages the network; custom ones # break Traefik routing (per Coolify docs). @@ -10,9 +15,13 @@ x-app-env: &app-env 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 + # Connection strings point at the SEPARATE Coolify DB/Redis resources. + # Real values are injected from Coolify's Environment Variables tab — the + # hostname is mariadb- / redis- from each resource's page, so they + # cannot be hardcoded here (the UUID differs per installation). + DATABASE_URL: ${DATABASE_URL} + MESSENGER_TRANSPORT_DSN: ${MESSENGER_TRANSPORT_DSN} + REDIS_URL: ${REDIS_URL} JWT_SECRET_KEY: "%kernel.project_dir%/config/jwt/private.pem" JWT_PUBLIC_KEY: "%kernel.project_dir%/config/jwt/public.pem" JWT_PASSPHRASE: ${JWT_PASSPHRASE} @@ -34,13 +43,6 @@ x-app-volumes: &app-volumes - uploads_public:/app/public/uploads - uploads_var:/app/var/uploads -# Shared dependency gate on DB + Redis being up. -x-app-depends: &app-depends - mariadb: - condition: service_healthy - redis: - condition: service_started - services: # Web app (PHP-FPM + Nginx). # In Coolify, assign ALL serving domains to THIS service (port 80) — the backend @@ -51,7 +53,7 @@ services: app: # Shared image tag — app builds it once; the two workers reuse the SAME image # (image: below, no build:). Without this Coolify builds the Dockerfile 3×, - # tripling apk/pecl network fetches and intermittently failing on the host. + # tripling apk network fetches and intermittently failing on the host. image: clinicpro-app:latest build: context: . @@ -61,7 +63,6 @@ services: <<: *app-env RUN_INIT: "1" # runs JWT keygen + migrations on start (only this service) volumes: *app-volumes - depends_on: *app-depends healthcheck: test: ["CMD", "php", "-r", "exit(@fsockopen('127.0.0.1', 80) ? 0 : 1);"] interval: 15s @@ -78,7 +79,7 @@ services: <<: *app-env RUN_INIT: "0" volumes: *app-volumes - depends_on: *app-depends + depends_on: [app] # Scheduler consumer — expires unpaid appointments every minute. Reuses app's image. worker-scheduler: @@ -89,35 +90,9 @@ services: <<: *app-env RUN_INIT: "0" volumes: *app-volumes - depends_on: *app-depends - - mariadb: - image: mariadb:11.8 - restart: unless-stopped - 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 - start_period: 30s - - redis: - image: redis:7-alpine - restart: unless-stopped - command: redis-server --appendonly yes - volumes: - - redis_data:/data + depends_on: [app] volumes: jwt_keys: uploads_public: uploads_var: - mariadb_data: - redis_data: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 3c7b7fa8..d5f7066f 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -8,6 +8,20 @@ chown -R www-data:www-data var public/uploads config/jwt 2>/dev/null || true # One-time init tasks — only the web service runs these (RUN_INIT=1). # Workers set RUN_INIT=0 so DB migrations / JWT generation don't race. if [ "${RUN_INIT:-1}" = "1" ]; then + # MariaDB is a SEPARATE Coolify resource now, so there is no compose + # depends_on: service_healthy gate. Wait for it to accept connections + # (up to ~60s) before migrating, otherwise the first boot races the DB. + i=0 + until php bin/console doctrine:query:sql "SELECT 1" >/dev/null 2>&1; do + i=$((i + 1)) + if [ "$i" -ge 30 ]; then + echo "Database not reachable after 60s — aborting." >&2 + exit 1 + fi + echo "waiting for database... ($i)" + sleep 2 + done + # Generate JWT keypair if not already persisted on the jwt_keys volume. php bin/console lexik:jwt:generate-keypair --skip-if-exists --no-interaction diff --git a/docker/php/php.ini b/docker/php/php.ini index b1ff81e0..f3ba8a1f 100644 --- a/docker/php/php.ini +++ b/docker/php/php.ini @@ -1,7 +1,7 @@ ; Production PHP settings for ClinicPro on Coolify -memory_limit = 256M +memory_limit = 1024M upload_max_filesize = 16M -post_max_size = 16M +post_max_size = 32M max_execution_time = 60 expose_php = Off date.timezone = Asia/Tehran diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 5bb8da39..9d070de6 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -8,22 +8,25 @@ ## معماری استک -`docker-compose.yml` پنج سرویس بالا می‌آورد: +`docker-compose.yml` فقط سرویس‌های **اپلیکیشن** را بالا می‌آورد. **MariaDB و Redis جداگانه** به‌صورت Database Resource مستقل Coolify اجرا می‌شوند (نه داخل این compose): | سرویس | نقش | نکته | |---|---|---| | `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` (ماندگار) | -**ولوم‌های ماندگار** (داده‌ها در ری‌دیپلوی حفظ می‌شوند): +| Resource مستقل Coolify | نقش | +|---|---| +| **MariaDB 11.8** (Database Resource جدا) | پایگاه‌داده — مدیریت/بکاپ/ری‌استارت مستقل | +| **Redis** (Database Resource جدا) | صف Messenger + کش | + +**ولوم‌های ماندگار استک اپ** (در ری‌دیپلوی حفظ می‌شوند): - `jwt_keys` → کلیدهای JWT - `uploads_public` و `uploads_var` → فایل‌های آپلودی -- `mariadb_data` → داده DB -- `redis_data` → داده Redis + +> داده‌ی MariaDB و Redis توسط خودِ Resourceهای مستقل نگه داشته می‌شود (volume در آن‌ها، نه در این استک). --- @@ -35,16 +38,27 @@ --- -## مرحله ۱ — ساخت منبع (Resource) در Coolify +## مرحله ۱ — ساخت دیتابیس‌های مستقل (MariaDB + Redis) + +اول این دو Resource را جدا بساز (قبل از استک اپ): + +1. **New Resource → Database → MariaDB**، نسخه **11.8** (باید با `serverVersion` در `DATABASE_URL` و migrationها هم‌خوان باشد). نام دیتابیس `clinic_pro`، یوزر `clinic`، یک رمز قوی ست کن. +2. **New Resource → Database → Redis**. +3. از صفحه‌ی هر Resource، **Internal URL / hostname** (به شکل `mariadb-` و `redis-`) و credentials را یادداشت کن — در مرحله ۴ لازم می‌شود. + +--- + +## مرحله ۲ — ساخت منبع اپ (Resource) در Coolify 1. **New Resource → Docker Compose** (Build Pack: `Docker Compose`). 2. ریپو و برنچ را انتخاب کن. 3. فیلد **Compose file** را روی `docker-compose.yml` بگذار. -4. `networks:` سفارشی تعریف **نکن** — شبکه را Coolify مدیریت می‌کند؛ شبکه‌ی سفارشی روتینگ Traefik را می‌شکند. +4. **"Connect to Predefined Network"** را روی این استک **فعال کن** — تا اپ بتواند به Resourceهای مستقل MariaDB/Redis (که در شبکه‌ی دیگری هستند) وصل شود. +5. `networks:` سفارشی تعریف **نکن** — شبکه را Coolify مدیریت می‌کند؛ شبکه‌ی سفارشی روتینگ Traefik را می‌شکند. --- -## مرحله ۲ — دامنه‌ها +## مرحله ۳ — دامنه‌ها همه‌ی دامنه‌های سرو شونده را به سرویس **`app`** (پورت 80) اختصاص بده — هم دامنه‌ی API و هم همه‌ی دامنه‌های فرانت‌اند. Coolify لیست دامنه‌ی جدا‌شده با کاما را روی یک سرویس قبول می‌کند و TLS را خودش صادر می‌کند. @@ -52,27 +66,37 @@ --- -## مرحله ۳ — متغیرهای محیطی +## مرحله ۴ — متغیرهای محیطی -از `.env.coolify.example` کپی کن و در تب **Environment Variables** منبع Coolify بگذار. +از `.env.coolify.example` کپی کن و در تب **Environment Variables** منبع اپ Coolify بگذار. -فقط متغیرهایی که در `docker-compose.yml` به‌صورت `${...}` ارجاع شده‌اند لازم‌اند. `DATABASE_URL` / `MESSENGER_TRANSPORT_DSN` / `REDIS_URL` داخل خود compose از روی نام سرویس‌ها ساخته می‌شوند. +### اتصال به دیتابیس‌های مستقل (الزامی) + +چون MariaDB/Redis جدا هستند، رشته‌های اتصال **اینجا** ست می‌شوند و به hostname داخلی Resource اشاره می‌کنند (`mariadb-` / `redis-` از مرحله ۱): + +```bash +DATABASE_URL="mysql://clinic:DB_PASSWORD@mariadb-XXXXXXXX:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4" +REDIS_URL="redis://redis-XXXXXXXX:6379" +MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages" +# اگر Redis رمز دارد: redis://:PASSWORD@redis-XXXXXXXX:6379 +``` + +> `serverVersion=mariadb-11.8.0` باید با نسخه‌ی Resource مستقل MariaDB یکی باشد. ### اسرار (الزامی — قبل از اولین دیپلوی) ```bash APP_SECRET= # php -r "echo bin2hex(random_bytes(32));" JWT_PASSPHRASE= # openssl rand -hex 32 (باید قبل از اولین استارت موجود باشد؛ کلید JWT با همین ساخته می‌شود) -DB_PASSWORD= # رمز کاربر DB اپلیکیشن -DB_ROOT_PASSWORD= # رمز root مریادی‌بی ``` +> رمز DB دیگر اینجا (`DB_PASSWORD`/`DB_ROOT_PASSWORD`) ست نمی‌شود — هنگام ساخت Resource مستقل MariaDB تعیین می‌شود و داخل `DATABASE_URL` بالا قرار می‌گیرد. + > ⚠️ `JWT_PASSPHRASE` را بعد از اولین دیپلوی عوض نکن — کلید JWT یک‌بار با همین passphrase تولید و روی ولوم `jwt_keys` ماندگار می‌شود. تغییرش همه‌ی توکن‌ها را می‌شکند. در Coolify می‌توانی به‌جای هاردکد از magic var استفاده کنی: ```bash -DB_PASSWORD=${SERVICE_PASSWORD_DB} APP_SECRET=${SERVICE_HEX_APPSECRET} ``` @@ -115,20 +139,21 @@ API_IR_TOKEN= # خالی => fail-closed (تأیید نماینده رد می --- -## مرحله ۴ — دیپلوی +## مرحله ۵ — دیپلوی روی **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` اعمال می‌شوند (ترنزکشن). +2. منتظر آماده‌شدن DB مستقل می‌ماند (تا ~۶۰ ثانیه؛ چون دیگر `depends_on: service_healthy` نیست). +3. کلید JWT اگر روی ولوم نباشد ساخته می‌شود (`--skip-if-exists`). +4. کش prod پاک و warmup می‌شود. +5. مهاجرت‌های DB با `--all-or-nothing` اعمال می‌شوند (ترنزکشن). > ورکرها `RUN_INIT=0` دارند تا مهاجرت/تولید کلید با هم تداخل نکنند. --- -## مرحله ۵ — پس از اولین دیپلوی +## مرحله ۶ — پس از اولین دیپلوی ### ساخت اولین ادمین @@ -151,7 +176,7 @@ push روی برنچ متصل (یا Deploy دستی). در هر ری‌دیپل - ایمیج دوباره build می‌شود (vendor + اسمبل فرانت‌اند multi-stage). - مهاجرت‌های جدید روی استارت `app` اعمال می‌شوند. -- ولوم‌ها حفظ می‌شوند (DB، آپلودها، کلیدهای JWT، Redis سالم می‌مانند). +- ولوم‌های استک اپ (آپلودها، کلیدهای JWT) حفظ می‌شوند. داده‌ی MariaDB/Redis در Resourceهای مستقل مستقل از این ری‌دیپلوی سالم می‌ماند. --- @@ -160,7 +185,8 @@ push روی برنچ متصل (یا Deploy دستی). در هر ری‌دیپل | نشانه | علت محتمل | |---|---| | ارورهای CORS در فرانت | `CORS_ALLOW_ORIGIN` با دامنه نمی‌خواند؛ از `gen-cors-env.php` بازتولید کن | -| `app` بالا نمی‌آید، منتظر DB می‌ماند | healthcheck `mariadb` رد نشده؛ لاگ mariadb را ببین | +| `app` با «waiting for database...» می‌ماند و بعد ۶۰ ثانیه می‌میرد | اپ به Resource مستقل MariaDB نمی‌رسد؛ چک کن: «Connect to Predefined Network» فعال است، hostname در `DATABASE_URL` درست (`mariadb-`) و رمز/نسخه‌ی Resource درست است | +| `redis` در دسترس نیست / صف کار نمی‌کند | `REDIS_URL`/`MESSENGER_TRANSPORT_DSN` به `redis-` درست اشاره نمی‌کند یا رمز جا افتاده | | ۴۰۱/توکن نامعتبر بعد از ری‌دیپلوی | `JWT_PASSPHRASE` تغییر کرده یا ولوم `jwt_keys` پاک شده | | IPها/HTTPS اشتباه پشت پراکسی | `TRUSTED_PROXIES` ست نشده | | مهاجرت اجرا نشد | فقط `app` با `RUN_INIT=1` اجرا می‌کند؛ مطمئن شو override نشده |