13 KiB
استقرار Production روی Coolify
این پروژه با سه سرویس اجرا میشود:
nginx: فقط سرویسدهنده فایلهای/static/روی پورت 80web: اجرای Django با Gunicorn روی پورت داخلی 8000notification-worker: اجرای دائمیpython manage.py process_notificationsبرای SMS و Bale
PostgreSQL جزو این Compose نیست و از دیتابیس مدیریتشده موجود استفاده میشود. Supervisor لازم نیست؛ هر process یک container مستقل دارد و restart: unless-stopped بازیابی آن را انجام میدهد.
1. Volumeهای Coolify
پیش از اولین deploy سه Docker volume با همین نامها ایجاد کنید:
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
این راهنما برای حالت عادی Git-based Docker Compose Application است، نه گزینه Raw Compose Deployment. در این حالت Coolify باید routeهای Traefik را از فیلدهای Domains بسازد؛ routerهای کامل Traefik را بهصورت دستی داخل Compose تعریف نکنید.
در Configuration > General مقدارها را دقیقاً به شکل زیر وارد کنید:
Domains for web:https://connect.loolegostar.com:8000Domains for notification-worker: خالیDomains for nginx:https://connect.loolegostar.com/static,https://connect.loolegostar.com/media/branding,https://connect.loolegostar.com/protected-media
در Configuration > Advanced گزینه Strip Prefixes را خاموش کنید. در غیر این صورت Coolify پیشوند /static یا /media/branding را قبل از تحویل درخواست حذف میکند، در حالی که docker/nginx.conf عمداً همان مسیرهای کامل را match میکند. بعد از Save، گزینه Reload Compose File و سپس Redeploy را اجرا کنید.
Traefik خود Coolify ruleهای Host/PathPrefix، TLS و reverse proxy را تولید میکند. بهدلیل باگ رقابت route مسیرمحور با route عمومی در برخی نسخههای Coolify، Compose روی routerهای تولیدشدهی nginx صراحتاً priority=10000، یک service روی پورت 80 و شبکهی resource-specific را تنظیم میکند. ترتیب سه Domain بالا را تغییر ندهید، چون شماره routerهای 0 تا 2 از همین ترتیب ساخته میشود. Nginx جلوی Django قرار نمیگیرد و فقط فایلهای static و media را سرو میکند.
کانفیگ docker/nginx.conf هنگام build توسط Dockerfile.nginx داخل image قرار میگیرد. بنابراین در Git-based deployment هیچ bind mountای از فایل کانفیگ روی میزبان Coolify وجود ندارد و هر redeploy دقیقاً کانفیگ همان commit را اجرا میکند. entrypoint رسمی image مقدار MEDIA_SIGNING_SECRET را در template جایگزین و فایل نهایی را در /etc/nginx/conf.d/default.conf ایجاد میکند.
فایلهای آپلودی تیکت عمومی نیستند: Django ابتدا مجوز را بررسی میکند و سپس کاربر را با یک URL امضاشده پنجدقیقهای به /protected-media/ روی Nginx میفرستد. branding عمومی زیر /media/branding/ مستقیماً از Nginx سرو میشود.
3. متغیرهای ضروری
مقادیر زیر را در Environment Variables برنامه Coolify بسازید:
DJANGO_SECRET_KEY=<random-secret-at-least-50-characters>
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:<URL_ENCODED_PASSWORD>@<INTERNAL_HOST>: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=<separate-random-secret-at-least-32-characters>
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
SMS_PROVIDER_CLASS=apps.integrations.sms.sms0098.ZeroZeroNinetyEightSMSProvider
SMS0098_USERNAME=<username>
SMS0098_PASSWORD=<password>
SMS0098_FROM=<sender-number>
SMS0098_WSDL_URL=https://webservice.0098sms.com/service.asmx?WSDL
Behin SMS
SMS_PROVIDER_CLASS=apps.integrations.sms.behin.BehinSMSProvider
BEHIN_SMS_USERNAME=<username>
BEHIN_SMS_PASSWORD=<password>
BEHIN_SMS_SPECIAL_NUMBER=<sender-number>
BEHIN_SMS_WSDL_URL=http://panel.behinsms.com/SMSWS/SOAP.asmx?wsdl
HTTP JSON provider
SMS_PROVIDER_CLASS=apps.integrations.sms.http.HTTPSMSProvider
SMS_API_URL=https://provider.example/api/messages
SMS_API_TOKEN=<token>
SMS_SENDER=<sender>
5. تنظیم Bale و worker
BALE_API_BASE=https://tapi.bale.ai
BALE_BOT_TOKEN=<bot-token>
INTEGRATION_HTTP_TIMEOUT_SECONDS=10
MAX_NOTIFICATION_ATTEMPTS=5
SMS_MAX_RETRIES=3
SMS_RETRY_DELAY_SECONDS=180
SMS_DELIVERY_CHECK_DELAY_SECONDS=180
SMS_DELIVERY_MAX_CHECKS=15
در production نبودن BALE_BOT_TOKEN خطای قابلمشاهده ایجاد میکند و job را بیصدا SENT علامت نمیزند. شناسه گروه دیگر یک env سراسری نیست: برای هر واحد از مسیر x5admin → واحدهای سازمانی، فیلد «شناسه گروه بله» را جداگانه وارد کنید. خالی بودن این فیلد یعنی اعلانهای گروهی تیکت جدید و پاسخ مشتری برای همان واحد ارسال نمیشود؛ سامانه هرگز به گروه واحد دیگر fallback نمیکند. سه شماره اختیاری سرپرست نیز در همین صفحه تنظیم میشوند و فقط هنگام ایجاد تیکت جدید، پیامک کوتاه دریافت میکنند.
worker هر پنج ثانیه outbox دیتابیس را بررسی میکند و با قفل skip_locked از پردازش همزمان جلوگیری میکند. هر پیامک پس از شکست اولیه، دقیقاً SMS_MAX_RETRIES=3 بار با فاصله ثابت SMS_RETRY_DELAY_SECONDS=180 ثانیه دوباره امتحان میشود. MAX_NOTIFICATION_ATTEMPTS همچنان سقف تلاش کانالهای دیگر مانند بله است. پیامک دارای شناسه رهگیری پس از SMS_DELIVERY_CHECK_DELAY_SECONDS استعلام میشود؛ پاسخهای موقت 0098SMS (کدهای 6 و 8) هر سه دقیقه تا 15 نوبت، یعنی 45 دقیقه، دوباره بررسی میشوند.
سیاست تأخیر پیامک پاسخ کارشناس و اعلان پاسخ مشتری در Bale از مدل NotificationPolicy در پنل مدیریت دیتابیس کنترل میشود؛ متغیر قدیمی STAFF_REPLY_SMS_THROTTLE_SECONDS مرجع اصلی این رفتار نیست.
6. تنظیمات امنیتی
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
- volumeها و database خالی را ایجاد کنید.
- طبق
docs/PRODUCTION_SEED.mdسه فایل seed را داخل volumecustomer_ticket_seedقرار دهید. - Environment Variables را در Coolify وارد کنید.
- Domain سرویس web و سه Domain مسیرمحور nginx را مطابق بخش ۲ وارد،
Strip Prefixesرا خاموش و Domain سرویس worker را خالی نگه دارید؛ سپس Compose را deploy کنید. - log سرویس
webرا برای موفقیت migrate، import production seed، collectstatic و Gunicorn بررسی کنید. وقتی seed موجود است، bootstrap عمداً اجرا نمیشود تا snapshot تغییر نکند. - log سرویس
notification-workerرا بررسی کنید؛ container باید Running بماند. - با superuser منتقلشده از SQLite وارد شوید و با گیرنده آزمایشی مجاز یک SMS و یک پیام Bale واقعی بفرستید.
Migration شماره tickets.0016_scope_legacy_public_tickets_to_units فقط یکبار اجرا میشود و تیکتهای قدیمی بدون واحد را از روی واحد قطعی دستهبندی به owning_unit و assigned_unit صحیح متصل میکند. این migration همراه deploy عادی و دستور migrate خودکار اجرا میشود و اجرای دستی SQL لازم نیست. برای کنترل نتیجه در Terminal سرویس web:
python manage.py showmigrations tickets
python manage.py shell -c "from apps.tickets.models import Ticket; print(Ticket.objects.filter(owning_unit__isnull=True).count(), Ticket.objects.filter(assigned_unit__isnull=True).count())"
هر دو عدد باید صفر باشند.
8. کنترل سلامت و عیبیابی
GET /healthz/هم web server و هم اتصال دیتابیس را بررسی میکند.docker compose psبایدwebوnginxرا healthy و worker را Running نشان دهد.docker compose logs --tail=200 web notification-worker nginxخطاهای startup و provider را نمایش میدهد.- داخل کانتینر Nginx،
nginx -Tبایدlocation = /healthz/را نشان دهد وwget -S -O - http://127.0.0.1/healthz/باید پاسخ200برگرداند. پاسخ404در این تست یعنی image یا commit جدید deploy نشده است. - وضعیت outbox را میتوان در Django admin یا shell از مدل
NotificationJobبررسی کرد؛ FAILED وlast_errorدلیل خطا را نگه میدارند. - برای پردازش کنترلشده یک job مشخص:
python manage.py process_notifications --once --job-id <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 مورد اعتماد خود تغییر دهید.