Coolify-doc-driven production hardening of the deploy stack: - run the whole stack as non-root www-data; nginx on 8080 (non-privileged), pid in /tmp, user directive dropped (Coolify routes to any port) - docker/healthcheck.sh: hit real /health route via PHP (not just port probe) - split OPcache config into docker/php/opcache.ini - graceful shutdown: supervisord stopsignal/stopwaitsecs + worker stop_grace_period - APCu intentionally not added (Symfony cache uses redis) - DEPLOY.md: 8080 port, non-root, resource-limit guidance Verified on linux/amd64: non-root uid=82, /health 200, migrations run, worker process healthcheck OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
راهنمای دیپلوی ClinicPro (Coolify + Docker Compose)
این راهنما برای دیپلوی بکاند ClinicPro روی Coolify با استفاده از docker-compose.yml نوشته شده.
توجه: این
docker-compose.ymlفقط برای production است. محیط لوکال ازcompose.yamlخودِ ddev استفاده میکند — این دو را با هم اشتباه نگیر.
معماری استک
docker-compose.yml فقط سرویسهای اپلیکیشن را بالا میآورد. MariaDB و Redis جداگانه بهصورت Database Resource مستقل Coolify اجرا میشوند (نه داخل این compose):
| سرویس | نقش | نکته |
|---|---|---|
app |
PHP-FPM + Nginx (وب، non-root، پورت ۸۰۸۰) | تنها سرویسی که RUN_INIT=1 دارد؛ مهاجرت DB و تولید کلید JWT را اجرا میکند. دامنهها را به این سرویس (پورت ۸۰۸۰) وصل کن. |
worker-async |
مصرفکننده صف async (SMS و کارهای async) | messenger:consume async |
worker-scheduler |
زمانبند | هر ۱ دقیقه نوبتهای پرداختنشده را منقضی میکند |
| Resource مستقل Coolify | نقش |
|---|---|
| MariaDB 11.8 (Database Resource جدا) | پایگاهداده — مدیریت/بکاپ/ریاستارت مستقل |
| Redis (Database Resource جدا) | صف Messenger + کش |
ولومهای ماندگار استک اپ (در ریدیپلوی حفظ میشوند):
jwt_keys→ کلیدهای JWTuploads_publicوuploads_var→ فایلهای آپلودی
دادهی MariaDB و Redis توسط خودِ Resourceهای مستقل نگه داشته میشود (volume در آنها، نه در این استک).
پیشنیازها
- نمونهی Coolify در حال اجرا با Traefik (پیشفرض Coolify).
- ریپوی Git متصل به Coolify.
- رکوردهای DNS برای دامنهی API و همهی دامنههای فرانتاند که به سرور اشاره کنند.
مرحله ۱ — ساخت دیتابیسهای مستقل (MariaDB + Redis)
اول این دو Resource را جدا بساز (قبل از استک اپ):
- New Resource → Database → MariaDB، نسخه 11.8 (باید با
serverVersionدرDATABASE_URLو migrationها همخوان باشد). نام دیتابیسclinic_pro، یوزرclinic، یک رمز قوی ست کن. - New Resource → Database → Redis.
- از صفحهی هر Resource، Internal URL / hostname (به شکل
mariadb-<uuid>وredis-<uuid>) و credentials را یادداشت کن — در مرحله ۴ لازم میشود.
مرحله ۲ — ساخت منبع اپ (Resource) در Coolify
- New Resource → Docker Compose (Build Pack:
Docker Compose). - ریپو و برنچ را انتخاب کن.
- فیلد Compose file را روی
docker-compose.ymlبگذار. - "Connect to Predefined Network" را روی این استک فعال کن — تا اپ بتواند به Resourceهای مستقل MariaDB/Redis (که در شبکهی دیگری هستند) وصل شود.
networks:سفارشی تعریف نکن — شبکه را Coolify مدیریت میکند؛ شبکهی سفارشی روتینگ Traefik را میشکند.
مرحله ۳ — دامنهها
همهی دامنههای سرو شونده را به سرویس app اختصاص بده — هم دامنهی API و هم همهی دامنههای فرانتاند. Coolify لیست دامنهی جداشده با کاما را روی یک سرویس قبول میکند و TLS را خودش صادر میکند.
⚠️ پورت سرویس =
8080(نه ۸۰). کانتینر non-root اجرا میشود و nginx روی پورت غیرممتاز ۸۰۸۰ گوش میدهد. در Coolify port سرویسappرا روی ۸۰۸۰ بگذار (Traefik به هر پورتی روت میکند — طبق داک، هر پورتی مجاز است).
اجازهدادن CORS و host فرانتاندها از طریق متغیرهای
CORS_ALLOW_ORIGIN/ALLOWED_FRONTEND_HOSTSکنترل میشود، نه دامنهی Coolify.
مرحله ۴ — متغیرهای محیطی
از .env.coolify.example کپی کن و در تب Environment Variables منبع اپ Coolify بگذار.
اتصال به دیتابیسهای مستقل (الزامی)
چون MariaDB/Redis جدا هستند، رشتههای اتصال اینجا ست میشوند و به hostname داخلی Resource اشاره میکنند (mariadb-<uuid> / redis-<uuid> از مرحله ۱):
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 یکی باشد.
اسرار (الزامی — قبل از اولین دیپلوی)
APP_SECRET= # php -r "echo bin2hex(random_bytes(32));"
JWT_PASSPHRASE= # openssl rand -hex 32 (باید قبل از اولین استارت موجود باشد؛ کلید JWT با همین ساخته میشود)
رمز DB دیگر اینجا (
DB_PASSWORD/DB_ROOT_PASSWORD) ست نمیشود — هنگام ساخت Resource مستقل MariaDB تعیین میشود و داخلDATABASE_URLبالا قرار میگیرد.
⚠️
JWT_PASSPHRASEرا بعد از اولین دیپلوی عوض نکن — کلید JWT یکبار با همین passphrase تولید و روی ولومjwt_keysماندگار میشود. تغییرش همهی توکنها را میشکند.
در Coolify میتوانی بهجای هاردکد از magic var استفاده کنی:
APP_SECRET=${SERVICE_HEX_APPSECRET}
دامنهها و CORS
APP_BASE_URL=https://api.nobat724.com # دامنهی خودِ بکاند (برای callback پرداخت و URLهای مطلق)
ALLOWED_FRONTEND_HOSTS و CORS_ALLOW_ORIGIN از docker/frontend-domains.json تولید میشوند. برای اضافه/حذف دامنهی شهر:
# آن فایل را ویرایش کن، سپس:
php docker/gen-cors-env.php # روی سرور
# یا لوکال:
ddev exec php docker/gen-cors-env.php
خروجی را در Coolify جایگزین کن.
ریورسپراکسی
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)
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):
- مالکیت
var,public/uploads,config/jwtبهwww-dataداده میشود. - منتظر آمادهشدن DB مستقل میماند (تا ~۶۰ ثانیه؛ چون دیگر
depends_on: service_healthyنیست). - کلید JWT اگر روی ولوم نباشد ساخته میشود (
--skip-if-exists). - کش prod پاک و warmup میشود.
- مهاجرتهای DB با
--all-or-nothingاعمال میشوند (ترنزکشن).
ورکرها
RUN_INIT=0دارند تا مهاجرت/تولید کلید با هم تداخل نکنند.
مرحله ۶ — پس از اولین دیپلوی
ساخت اولین ادمین
# داخل کانتینر سرویس app
php bin/console app:create-admin
بررسی سلامت
- healthcheck سرویس
app:docker/healthcheck.shroute واقعی/healthرا روی پورت ۸۰۸۰ میزند (نه فقط چک پورت). - workerها: healthcheck زندهبودن پروسهی
messenger:consumeباps. - Swagger:
https://<APP_BASE_URL>/api/doc - پنل ادمین:
https://<APP_BASE_URL>/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 دستی). در هر ریدیپلوی:
- ایمیج دوباره build میشود (vendor + اسمبل فرانتاند multi-stage).
- مهاجرتهای جدید روی استارت
appاعمال میشوند. - ولومهای استک اپ (آپلودها، کلیدهای JWT) حفظ میشوند. دادهی MariaDB/Redis در Resourceهای مستقل مستقل از این ریدیپلوی سالم میماند.
عیبیابی
| نشانه | علت محتمل |
|---|---|
| ارورهای CORS در فرانت | CORS_ALLOW_ORIGIN با دامنه نمیخواند؛ از gen-cors-env.php بازتولید کن |
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 نشده |
| تأیید نماینده رد میشود | API_IR_TOKEN خالی است (fail-closed) |