# استقرار Production روی Coolify این پروژه با سه سرویس اجرا می‌شود: - `nginx`: فقط سرویس‌دهنده فایل‌های `/static/` روی پورت 80 - `web`: اجرای Django با Gunicorn روی پورت داخلی 8000 - `notification-worker`: اجرای دائمی `python manage.py process_notifications` برای SMS و Bale PostgreSQL جزو این Compose نیست و از دیتابیس مدیریت‌شده موجود استفاده می‌شود. Supervisor لازم نیست؛ هر process یک container مستقل دارد و `restart: unless-stopped` بازیابی آن را انجام می‌دهد. ## 1. Volumeهای Coolify پیش از اولین deploy دو Docker volume با همین نام‌ها ایجاد کنید: ```text customer_ticket_static customer_ticket_media ``` در صورت انتخاب نام دیگر، متغیرهای `STATIC_VOLUME_NAME` و `MEDIA_VOLUME_NAME` را مطابق آن تنظیم کنید. media حاوی آپلودهای ماندگار است. static در هر startup دوباره با `collectstatic` تکمیل می‌شود اما برای اشتراک بین web و Nginx volume لازم دارد. ## 2. دامنه‌ها و Traefik خود Coolify دامنه اصلی HTTPS را در Coolify مستقیماً به سرویس `web` و پورت `8000` متصل کنید. Traefik خود Coolify TLS، reverse proxy و هدر `X-Forwarded-Proto` را مدیریت می‌کند؛ Nginx جلوی Django قرار نمی‌گیرد. برای همان دامنه، routeهای `/static/`، `/media/branding/` و `/protected-media/` را به سرویس `nginx` و پورت `80` بدهید و مطمئن شوید routeهای path-based نسبت به route عمومی web اولویت بالاتری دارند. برای `notification-worker` هیچ domain یا host port عمومی نسازید. اگر نسخه Coolify شما تعریف چند route روی یک دامنه را از UI پشتیبانی نمی‌کند، routing rule متناظر Traefik را برای این سه path به nginx اضافه کنید؛ معماری containerها همچنان بدون proxy_pass بین Nginx و Django است. فایل‌های آپلودی تیکت عمومی نیستند: Django ابتدا مجوز را بررسی می‌کند و سپس کاربر را با یک URL امضاشده پنج‌دقیقه‌ای به `/protected-media/` روی Nginx می‌فرستد. branding عمومی زیر `/media/branding/` مستقیماً از Nginx سرو می‌شود. ## 3. متغیرهای ضروری مقادیر زیر را در Environment Variables برنامه Coolify بسازید: ```dotenv DJANGO_SECRET_KEY= DJANGO_ALLOWED_HOSTS=support.example.com CSRF_TRUSTED_ORIGINS=https://support.example.com PUBLIC_BASE_URL=https://support.example.com WEBOTP_HOST=support.example.com TIME_ZONE=Asia/Tehran DATABASE_URL=postgresql://postgres:@:5432/customerTicket DB_CONN_MAX_AGE=60 DB_CONNECT_TIMEOUT=10 DB_SSLMODE=prefer DATABASE_WAIT_TIMEOUT=60 STATIC_VOLUME_NAME=customer_ticket_static MEDIA_VOLUME_NAME=customer_ticket_media MEDIA_SIGNING_SECRET= ``` نام دیتابیس انتهای URL باید نام واقعی دیتابیس باشد. اگر دیتابیس ساخته‌شده `customerTicket` است، URL باید به `/customerTicket` ختم شود؛ URLی که به `/postgres` ختم می‌شود به دیتابیس پیش‌فرض `postgres` متصل خواهد شد. اگر password شامل `@`، `:`، `/`، `#` یا `%` است آن را URL-encode کنید. می‌توانید به‌جای `DATABASE_URL` آن را خالی بگذارید و `POSTGRES_DB`، `POSTGRES_USER`، `POSTGRES_PASSWORD`، `POSTGRES_HOST` و `POSTGRES_PORT` را جداگانه تعریف کنید. یکی از این دو روش را انتخاب کنید. ## 4. تنظیم SMS فقط provider مورد استفاده را انتخاب و secretهای همان provider را پر کنید. ### 0098SMS ```dotenv SMS_PROVIDER_CLASS=apps.integrations.sms.sms0098.ZeroZeroNinetyEightSMSProvider SMS0098_USERNAME= SMS0098_PASSWORD= SMS0098_FROM= SMS0098_API_URL=http://www.0098sms.com/sendsmslink.aspx ``` ### Behin SMS ```dotenv SMS_PROVIDER_CLASS=apps.integrations.sms.behin.BehinSMSProvider BEHIN_SMS_USERNAME= BEHIN_SMS_PASSWORD= BEHIN_SMS_SPECIAL_NUMBER= BEHIN_SMS_WSDL_URL=http://panel.behinsms.com/SMSWS/SOAP.asmx?wsdl ``` ### HTTP JSON provider ```dotenv SMS_PROVIDER_CLASS=apps.integrations.sms.http.HTTPSMSProvider SMS_API_URL=https://provider.example/api/messages SMS_API_TOKEN= SMS_SENDER= ``` ## 5. تنظیم Bale و worker ```dotenv BALE_API_BASE=https://tapi.bale.ai BALE_BOT_TOKEN= BALE_GROUP_CHAT_ID= INTEGRATION_HTTP_TIMEOUT_SECONDS=10 MAX_NOTIFICATION_ATTEMPTS=5 ``` در production نبودن `BALE_BOT_TOKEN` خطای قابل‌مشاهده ایجاد می‌کند و job را بی‌صدا SENT علامت نمی‌زند. worker هر پنج ثانیه outbox دیتابیس را بررسی می‌کند، با قفل `skip_locked` از پردازش هم‌زمان جلوگیری می‌کند و خطاها را با backoff تا سقف `MAX_NOTIFICATION_ATTEMPTS` دوباره امتحان می‌کند. سیاست تأخیر پیامک پاسخ کارشناس و اعلان پاسخ مشتری در Bale از مدل `NotificationPolicy` در پنل مدیریت دیتابیس کنترل می‌شود؛ متغیر قدیمی `STAFF_REPLY_SMS_THROTTLE_SECONDS` مرجع اصلی این رفتار نیست. ## 6. تنظیمات امنیتی ```dotenv SECURE_SSL_REDIRECT=true SECURE_HSTS_SECONDS=31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS=true SECURE_HSTS_PRELOAD=false ``` `SECURE_HSTS_PRELOAD` را فقط وقتی true کنید که تمام subdomainها همیشه HTTPS هستند. `DJANGO_ALLOWED_HOSTS` فقط hostname و `CSRF_TRUSTED_ORIGINS` origin کامل همراه `https://` است. برای چند مقدار از کاما استفاده کنید. ## 7. ترتیب اولین deploy 1. volumeها و database خالی را ایجاد کنید. 2. Environment Variables را در Coolify وارد کنید. 3. Compose را deploy، دامنه اصلی را به `web:8000` و pathهای static/media گفته‌شده را به `nginx:80` متصل کنید. 4. log سرویس `web` را برای موفقیت migrate، bootstrap، collectstatic و Gunicorn بررسی کنید. 5. log سرویس `notification-worker` را بررسی کنید؛ container باید Running بماند. 6. یک‌بار superuser بسازید: ```bash docker compose exec web python manage.py createsuperuser ``` 7. در پنل مدیریت، SMS templateها و `NotificationPolicy`ها را بررسی کنید و با گیرنده آزمایشی مجاز یک SMS و یک پیام Bale واقعی بفرستید. ## 8. کنترل سلامت و عیب‌یابی - `GET /healthz/` هم web server و هم اتصال دیتابیس را بررسی می‌کند. - `docker compose ps` باید `web` و `nginx` را healthy و worker را Running نشان دهد. - `docker compose logs --tail=200 web notification-worker nginx` خطاهای startup و provider را نمایش می‌دهد. - وضعیت outbox را می‌توان در Django admin یا shell از مدل `NotificationJob` بررسی کرد؛ FAILED و `last_error` دلیل خطا را نگه می‌دارند. - برای پردازش کنترل‌شده یک job مشخص: `python manage.py process_notifications --once --job-id `. ارسال اعلان ماهیت at-least-once دارد: اگر provider پیام را بپذیرد ولی process پیش از ثبت نتیجه در PostgreSQL قطع شود، retry ممکن است پیام تکراری بسازد. `dedupe_key` جلوی ساخت job تکراری در برنامه را می‌گیرد، اما تضمین exactly-once به پشتیبانی idempotency از طرف provider نیاز دارد. ## 9. Mirrorهای ایرانی Dockerfile به‌طور پیش‌فرض PyPI لیارا را از آدرس `https://package-mirror.liara.ir/repository/pypi/simple` و mirror Debian لیارا را استفاده می‌کند. اگر mirror Debian در محیط شما متفاوت است، build argument با نام `DEBIAN_MIRROR_HOST` را تغییر دهید. برای image مربوط به Nginx نیز می‌توانید `NGINX_IMAGE` را به مسیر registry mirror مورد اعتماد خود تغییر دهید.