refactor: separate MariaDB and Redis into independent Coolify resources

This commit is contained in:
hamed
2026-06-28 11:28:29 +03:30
parent 471f43248b
commit 42bb723333
6 changed files with 323 additions and 72 deletions
@@ -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"** روی استک اپ فعال شود و دیتابیس با شناسه‌ی کامل `<service>-<uuid>` به‌عنوان hostname رفرنس داده شود (مثل `postgresql-abc123...`؛ برای ما `mariadb-<uuid>` / `redis-<uuid>`).
- **هرگز `networks:` سفارشی** در compose تعریف نکن — Coolify خودش مدیریت می‌کند؛ شبکه‌ی سفارشی routing Traefik را می‌شکند و باعث قطعی متناوب می‌شود.
- **مقادیر اتصال (DATABASE_URL / REDIS_URL / MESSENGER_TRANSPORT_DSN)** دیگر به نام سرویس داخلی اشاره نمی‌کنند؛ از تب Environment Variables در Coolify ست می‌شوند و به hostname دیتابیس مستقل (`mariadb-<uuid>` / `redis-<uuid>`) اشاره می‌کنند.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `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 باید `<service>-<uuid>` باشد (UUID از صفحه‌ی Resource در Coolify گرفته می‌شود):
```bash
# اتصال به MariaDB مستقل Coolify (hostname = mariadb-<uuid> از صفحه‌ی Resource)
DATABASE_URL="mysql://clinic:PASSWORD@mariadb-XXXXXXXX:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4"
# اتصال به Redis مستقل Coolify (hostname = redis-<uuid>)
REDIS_URL="redis://redis-XXXXXXXX:6379"
MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages"
```
- توضیح بده که اگر Redis مستقل Coolify رمز دارد، فرمت `redis://:PASSWORD@redis-<uuid>: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 `<service>-<uuid>`.
- توضیح بده که volumeهای `mariadb_data`/`redis_data` دیگر در استک اپ نیستند؛ ماندگاری داده برعهده‌ی خود Resourceهاست.
## نکات مهم
- **هیچ `networks:` سفارشی** اضافه نشود؛ اتصال cross-resource فقط با "Connect to Predefined Network" + hostname `<service>-<uuid>`.
- مقادیر `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`).
```
+22 -6
View File
@@ -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-<uuid> / redis-<uuid>).
# 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.
+17 -42
View File
@@ -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-<uuid> / redis-<uuid>).
# 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-<uuid> / redis-<uuid> 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:
+14
View File
@@ -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
+2 -2
View File
@@ -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
+48 -22
View File
@@ -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-<uuid>` و `redis-<uuid>`) و 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-<uuid>` / `redis-<uuid>` از مرحله ۱):
```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-<uuid>`) و رمز/نسخه‌ی Resource درست است |
| `redis` در دسترس نیست / صف کار نمی‌کند | `REDIS_URL`/`MESSENGER_TRANSPORT_DSN` به `redis-<uuid>` درست اشاره نمی‌کند یا رمز جا افتاده |
| ۴۰۱/توکن نامعتبر بعد از ری‌دیپلوی | `JWT_PASSPHRASE` تغییر کرده یا ولوم `jwt_keys` پاک شده |
| IPها/HTTPS اشتباه پشت پراکسی | `TRUSTED_PROXIES` ست نشده |
| مهاجرت اجرا نشد | فقط `app` با `RUN_INIT=1` اجرا می‌کند؛ مطمئن شو override نشده |