# استقرار 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 customer_ticket_seed ``` در صورت انتخاب نام دیگر، متغیرهای `STATIC_VOLUME_NAME`، `MEDIA_VOLUME_NAME` و `SEED_VOLUME_NAME` را مطابق آن تنظیم کنید. media حاوی آپلودهای ماندگار است. static در هر startup دوباره با `collectstatic` تکمیل می‌شود اما برای اشتراک بین web و Nginx volume لازم دارد. سه فایل تولیدشده در `production-seed/` باید پیش از اولین startup داخل ریشه volume سوم قرار گیرند. این Compose هر سه سرویس را به شبکه خارجی `coolify` متصل می‌کند تا hostname داخلی PostgreSQL نصب‌شده در Coolify از داخل `web` و `notification-worker` قابل resolve باشد. شبکه باید از قبل روی سرور Coolify موجود باشد؛ Compose به دلیل `external: true` آن را ایجاد نمی‌کند. ## 2. دامنه‌ها و Traefik خود Coolify دامنه اصلی HTTPS را در Coolify مستقیماً به سرویس `web` و پورت `8000` متصل کنید. مقدار Domain سرویس web باید دقیقاً یک URL و بدون فاصله یا کامای انتهایی باشد. Traefik خود Coolify TLS، reverse proxy و هدر `X-Forwarded-Proto` را مدیریت می‌کند؛ Nginx جلوی Django قرار نمی‌گیرد. فیلد Domain سرویس‌های `nginx` و `notification-worker` را در UI کاملاً خالی بگذارید. Compose برای Nginx routerهای صریح HTTP/HTTPS با priority برابر 1000 می‌سازد و `/static/`، `/media/branding/` و `/protected-media/` را بدون StripPrefix به پورت 80 می‌فرستد. hostname این ruleها از `ASSET_HOST` خوانده می‌شود. معماری 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 SEED_VOLUME_NAME=customer_ticket_seed MEDIA_SIGNING_SECRET= REQUIRE_PRODUCTION_SEED=1 ``` نام دیتابیس انتهای 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. طبق `docs/PRODUCTION_SEED.md` سه فایل seed را داخل volume `customer_ticket_seed` قرار دهید. 3. Environment Variables را در Coolify وارد کنید. 4. `ASSET_HOST` را روی hostname عمومی بگذارید، Domain سرویس web را بدون کامای انتهایی تنظیم کنید و Domainهای nginx/worker را خالی نگه دارید؛ سپس Compose را deploy کنید. 5. log سرویس `web` را برای موفقیت migrate، import production seed، collectstatic و Gunicorn بررسی کنید. وقتی seed موجود است، bootstrap عمداً اجرا نمی‌شود تا snapshot تغییر نکند. 6. log سرویس `notification-worker` را بررسی کنید؛ container باید Running بماند. 7. با superuser منتقل‌شده از SQLite وارد شوید و با گیرنده آزمایشی مجاز یک 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 مورد اعتماد خود تغییر دهید.