143 lines
9.3 KiB
Markdown
143 lines
9.3 KiB
Markdown
# استقرار 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` متصل کنید. 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=<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
|
|
|
|
```dotenv
|
|
SMS_PROVIDER_CLASS=apps.integrations.sms.sms0098.ZeroZeroNinetyEightSMSProvider
|
|
SMS0098_USERNAME=<username>
|
|
SMS0098_PASSWORD=<password>
|
|
SMS0098_FROM=<sender-number>
|
|
SMS0098_API_URL=http://www.0098sms.com/sendsmslink.aspx
|
|
```
|
|
|
|
### Behin SMS
|
|
|
|
```dotenv
|
|
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
|
|
|
|
```dotenv
|
|
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
|
|
|
|
```dotenv
|
|
BALE_API_BASE=https://tapi.bale.ai
|
|
BALE_BOT_TOKEN=<bot-token>
|
|
BALE_GROUP_CHAT_ID=<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. Compose را deploy، دامنه اصلی را به `web:8000` و pathهای static/media گفتهشده را به `nginx:80` متصل کنید.
|
|
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 <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 مورد اعتماد خود تغییر دهید.
|