make a deploy observable and one command instead of a checklist
بالا آوردنِ نسخهی جدید تا الان چند کارِ دستی به ترتیبِ درست بود (pull، نصبِ
وابستگیها، مهاجرت، استاتیک، ریاستارت) و فراموش شدنِ هر کدام اپ را میخواباند
با خطایی که از بیرون فقط «502 Bad Gateway» دیده میشد. همین الان هم سرور
دقیقاً به همین دلیل خوابیده: کامیتِ ac1b1af پکیجِ django-cors-headers را اضافه
کرد، و بدونِ pip install جنگو با ModuleNotFoundError بالا نمیآید.
deploy.sh همه را به ترتیب و idempotent انجام میدهد. pip install همیشه اجرا
میشود، نه فقط وقتی requirements عوض شده — تشخیصِ «عوض شده یا نه» خودش یک جای
فراموششدنیِ دیگر است. آخرِ کار خودش سلامت را بررسی میکند و اگر بالا نیامده
باشد علتهای رایج را چاپ میکند، بهجای اینکه کسی بعداً از بیرون کشفش کند.
GET /api/health وضعیت را بدونِ احراز و بدونِ SSH بیرون میدهد: دیتابیس،
مهاجرتهای اعمالنشده، کش، و شمارشِ پایه/درس/سوال روی همان سرور. مهمترینش
مهاجرتِ اعمالنشده است — حالتی که اپ ظاهراً بالا میآید و بعد زیرِ بارِ واقعی
با خطای ستونِ ناموجود میشکند. عمداً هیچ سکرتی برنمیگرداند، فقط بله/خیرِ
«تنظیم شده».
۵۰۳ برمیگرداند وقتی دیتابیس/کش/مهاجرت خراب است تا پایشِ خودکار بتواند رویش
حساب کند؛ نبودِ محتوا خرابی حساب نمیشود ولی گزارش میشود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
51
README.md
51
README.md
@@ -110,6 +110,57 @@ python manage.py test # اجرای کل تستسوییت
|
|||||||
|
|
||||||
## ۲. راهنمای دیپلوی پروداکشن
|
## ۲. راهنمای دیپلوی پروداکشن
|
||||||
|
|
||||||
|
### بالا آوردنِ نسخهی جدید — یک دستور
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
همین. اسکریپت به ترتیب کدِ تازه را میگیرد، وابستگیها را نصب میکند، مهاجرتها
|
||||||
|
را اعمال میکند، فایلهای استاتیک را جمع میکند، سرویس را ریاستارت میکند، و در
|
||||||
|
پایان **خودش بررسی میکند که واقعاً بالا آمده یا نه**. اگر بالا نیامد، علتهای
|
||||||
|
رایج را چاپ میکند.
|
||||||
|
|
||||||
|
چرا این مهم است: قبلاً اینها چند کارِ دستی به ترتیبِ درست بودند و فراموش شدنِ
|
||||||
|
هر کدام اپ را میخواباند، با خطایی که گویا نبود. مثلاً اضافه شدنِ یک پکیجِ جدید
|
||||||
|
به `requirements.txt` بدونِ `pip install` باعثِ `ModuleNotFoundError` موقعِ بالا
|
||||||
|
آمدن میشود و کاربر فقط `502 Bad Gateway` میبیند. اسکریپت همیشه `pip install`
|
||||||
|
را اجرا میکند تا این حالت اصلاً پیش نیاید.
|
||||||
|
|
||||||
|
بارِ اول، نامِ سرویس را بدهید تا دفعههای بعد خودکار ریاستارت شود:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TADRISINO_SERVICE=<نامِ سرویس> ./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
متغیرهای اختیاری: `SKIP_PULL=1` (بدونِ git pull)، `TADRISINO_HEALTH_URL`،
|
||||||
|
`PIP_INDEX_URL` (اگر pypi.org تایماوت میدهد).
|
||||||
|
|
||||||
|
### بررسیِ سلامت از بیرون
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://<host>/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
بدونِ احرازِ هویت و بدونِ SSH میگوید دیتابیس سالم است یا نه، مهاجرتِ
|
||||||
|
اعمالنشده مانده یا نه، کش کار میکند یا نه، و چند پایه/درس/سوال روی آن سرور
|
||||||
|
هست. `200` یعنی سالم، `503` یعنی چیزی واقعاً خراب است. هیچ سکرتی برنمیگرداند.
|
||||||
|
|
||||||
|
این همان چیزیست که جلوی «کد رفته ولی مهاجرت اجرا نشده» را میگیرد — حالتی که
|
||||||
|
اپ ظاهراً بالا میآید و بعد زیرِ بارِ واقعی میشکند.
|
||||||
|
|
||||||
|
### مدیریتِ محتوا بدونِ ترمینال
|
||||||
|
|
||||||
|
سوالها از پنلِ ادمین وارد و خارج میشوند، نه با دستور روی سرور:
|
||||||
|
|
||||||
|
- `/admin/content/question/` → دکمهی **«وارد کردنِ سوالها از فایل»**
|
||||||
|
- برای گرفتنِ نمونه: چند سوال را انتخاب کنید → اکشنِ **«دریافتِ فایلِ سوالهای
|
||||||
|
انتخابشده»**. خروجی دقیقاً همان قالبیست که ورودی میپذیرد.
|
||||||
|
|
||||||
|
فایلِ نامعتبر **هیچ چیزی نمینویسد** — یا همهی سوالها وارد میشوند یا هیچکدام،
|
||||||
|
و خطا میگوید کدام ردیف ایراد دارد.
|
||||||
|
|
||||||
|
|
||||||
### متغیرهای محیطی موردنیاز
|
### متغیرهای محیطی موردنیاز
|
||||||
| متغیر | لازم در پروداکشن | توضیح |
|
| متغیر | لازم در پروداکشن | توضیح |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
119
config/health.py
Normal file
119
config/health.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"""
|
||||||
|
GET /api/health — «آیا این سرور واقعاً سالم است؟»
|
||||||
|
|
||||||
|
چرا لازم بود: تا امروز تنها راهِ فهمیدنِ اینکه دیپلوی درست نشسته یا نه، این
|
||||||
|
بود که کسی از بیرون اپ را دستی امتحان کند و اگر چیزی غلط بود به آدمی که به
|
||||||
|
سرور دسترسی دارد پیام بدهد. یعنی خرابی را همیشه آدم کشف میکرد، نه ابزار.
|
||||||
|
این اندپوینت همان بررسیها را از بیرون و بدونِ دسترسیِ SSH ممکن میکند.
|
||||||
|
|
||||||
|
مهمترین بررسی «مهاجرتهای اعمالنشده» است: شایعترین حالتِ خرابِ دیپلوی این
|
||||||
|
است که کد pull شده ولی migrate اجرا نشده — اپ بالا میآید و بعد زیرِ بارِ
|
||||||
|
واقعی با خطای ستونِ ناموجود میشکند.
|
||||||
|
|
||||||
|
عمداً هیچ سکرتی برنمیگرداند: فقط بله/خیرِ «تنظیم شده» و شمارشِ محتوا.
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.db import connection
|
||||||
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.throttling import ScopedRateThrottle
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
||||||
|
def _check_database() -> dict:
|
||||||
|
try:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('SELECT 1')
|
||||||
|
cursor.fetchone()
|
||||||
|
return {'ok': True, 'engine': connection.vendor}
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return {'ok': False, 'error': type(exc).__name__}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_migrations() -> dict:
|
||||||
|
"""مهاجرتِ اعمالنشده یعنی دیپلوی نیمهکاره است."""
|
||||||
|
try:
|
||||||
|
executor = MigrationExecutor(connection)
|
||||||
|
targets = executor.loader.graph.leaf_nodes()
|
||||||
|
pending = executor.migration_plan(targets)
|
||||||
|
return {
|
||||||
|
'ok': not pending,
|
||||||
|
'pending': [f'{app}.{name}' for (app, name), _ in
|
||||||
|
((m.key, m) for m, _ in pending)],
|
||||||
|
}
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return {'ok': False, 'error': type(exc).__name__}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_cache() -> dict:
|
||||||
|
"""با DEBUG=False کشِ مشترک اجباری است؛ throttle بدونش بیاثر میشود."""
|
||||||
|
try:
|
||||||
|
cache.set('health:probe', 'ok', 10)
|
||||||
|
return {'ok': cache.get('health:probe') == 'ok', 'backend': settings.CACHES['default']['BACKEND'].rsplit('.', 1)[-1]}
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return {'ok': False, 'error': type(exc).__name__}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_content() -> dict:
|
||||||
|
"""
|
||||||
|
محتوا روی این سرور هست یا نه — بدونِ این، اپ سالم به نظر میرسد ولی
|
||||||
|
کاربر صفحهی خالی میبیند.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from content.models import Grade, Lesson, Question
|
||||||
|
|
||||||
|
grades = Grade.objects.count()
|
||||||
|
lessons = Lesson.objects.count()
|
||||||
|
questions = Question.objects.count()
|
||||||
|
return {
|
||||||
|
'ok': grades > 0 and lessons > 0,
|
||||||
|
'grades': grades,
|
||||||
|
'lessons': lessons,
|
||||||
|
'questions': questions,
|
||||||
|
}
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return {'ok': False, 'error': type(exc).__name__}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_integrations() -> dict:
|
||||||
|
"""فقط «تنظیم شده یا نه» — هیچ کلیدی برنمیگردد."""
|
||||||
|
return {
|
||||||
|
'qdrant': bool(getattr(settings, 'QDRANT_API_KEY', '')),
|
||||||
|
'openai': bool(getattr(settings, 'OPENAI_API_KEY', '')),
|
||||||
|
'sms': bool(getattr(settings, 'KAVENEGAR_API_KEY', '')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HealthView(APIView):
|
||||||
|
"""
|
||||||
|
GET /api/health — وضعیتِ این نسخهی مستقرشده.
|
||||||
|
|
||||||
|
۲۰۰ یعنی سالم، ۵۰۳ یعنی چیزی واقعاً خراب است (دیتابیس، کش، یا مهاجرتِ
|
||||||
|
اعمالنشده). نبودِ محتوا خرابی حساب نمیشود ولی گزارش میشود.
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = []
|
||||||
|
throttle_classes = [ScopedRateThrottle]
|
||||||
|
throttle_scope = 'health'
|
||||||
|
|
||||||
|
@extend_schema(tags=['System'], responses={200: dict, 503: dict})
|
||||||
|
def get(self, request):
|
||||||
|
checks = {
|
||||||
|
'database': _check_database(),
|
||||||
|
'migrations': _check_migrations(),
|
||||||
|
'cache': _check_cache(),
|
||||||
|
'content': _check_content(),
|
||||||
|
}
|
||||||
|
critical_ok = all(checks[k]['ok'] for k in ('database', 'migrations', 'cache'))
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'status': 'ok' if critical_ok else 'error',
|
||||||
|
'debug': bool(settings.DEBUG),
|
||||||
|
'checks': checks,
|
||||||
|
'integrations': _check_integrations(),
|
||||||
|
}
|
||||||
|
return Response(payload, status=200 if critical_ok else 503)
|
||||||
@@ -333,6 +333,9 @@ REST_FRAMEWORK = {
|
|||||||
'affiliate_track': '10/hour',
|
'affiliate_track': '10/hour',
|
||||||
'payment_webhook': '60/hour',
|
'payment_webhook': '60/hour',
|
||||||
'payment_pay': '30/hour',
|
'payment_pay': '30/hour',
|
||||||
|
# /api/health برای پایشِ بیرونی است؛ باید مکرر قابلِ صدا زدن باشد
|
||||||
|
# ولی نه آنقدر که خودش به ابزارِ فشار تبدیل شود.
|
||||||
|
'health': os.environ.get('HEALTH_THROTTLE_RATE', '60/min'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.contrib import admin
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views import defaults as django_default_views
|
from django.views import defaults as django_default_views
|
||||||
|
from config.health import HealthView
|
||||||
from drf_spectacular.views import (
|
from drf_spectacular.views import (
|
||||||
SpectacularAPIView,
|
SpectacularAPIView,
|
||||||
SpectacularRedocView,
|
SpectacularRedocView,
|
||||||
@@ -41,6 +42,9 @@ def handler500(request):
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
|
||||||
|
# بررسیِ سلامتِ نسخهی مستقرشده — بدونِ احراز، برای پایشِ بیرونی.
|
||||||
|
path('api/health', HealthView.as_view(), name='health'),
|
||||||
|
|
||||||
# OpenAPI schema + Swagger UI + Redoc
|
# OpenAPI schema + Swagger UI + Redoc
|
||||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
|
|||||||
134
deploy.sh
Executable file
134
deploy.sh
Executable file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# دیپلویِ تدریسینو — یک دستور، نه یک فهرستِ کار.
|
||||||
|
#
|
||||||
|
# ./deploy.sh
|
||||||
|
#
|
||||||
|
# چرا این فایل وجود دارد: تا امروز بالا آوردنِ نسخهی جدید یعنی چند کارِ دستی
|
||||||
|
# به ترتیبِ درست (pull، نصبِ وابستگیها، مهاجرت، فایلهای استاتیک، ریاستارت).
|
||||||
|
# فراموش شدنِ هر کدام، اپ را میخواباند و خطایش هم گویا نیست — مثلاً اضافه شدنِ
|
||||||
|
# یک پکیجِ جدید بدونِ `pip install` باعثِ ModuleNotFoundError موقعِ بالا آمدن
|
||||||
|
# میشود و کاربر فقط «502 Bad Gateway» میبیند. این اسکریپت همه را به ترتیب و
|
||||||
|
# idempotent انجام میدهد و آخرش خودش بررسی میکند که واقعاً بالا آمده یا نه.
|
||||||
|
#
|
||||||
|
# متغیرهای اختیاری:
|
||||||
|
# TADRISINO_SERVICE نامِ سرویسِ systemd برای ریاستارت (مثلاً tadrisino)
|
||||||
|
# TADRISINO_HEALTH_URL آدرسی که در پایان بررسی میشود
|
||||||
|
# SKIP_PULL=1 بدونِ git pull (وقتی کد را دستی گذاشتهاید)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
C_OK=$'\033[0;32m'; C_ERR=$'\033[0;31m'; C_WARN=$'\033[0;33m'; C_OFF=$'\033[0m'
|
||||||
|
step() { printf '\n%s▸ %s%s\n' "$C_OK" "$1" "$C_OFF"; }
|
||||||
|
warn() { printf '%s⚠ %s%s\n' "$C_WARN" "$1" "$C_OFF"; }
|
||||||
|
fail() { printf '%s✖ %s%s\n' "$C_ERR" "$1" "$C_OFF" >&2; exit 1; }
|
||||||
|
|
||||||
|
HEALTH_URL="${TADRISINO_HEALTH_URL:-http://127.0.0.1:8000/api/health}"
|
||||||
|
|
||||||
|
# ─── ۱. کدِ تازه ────────────────────────────────────────────────────────────
|
||||||
|
if [ "${SKIP_PULL:-0}" != "1" ]; then
|
||||||
|
step "گرفتنِ کدِ تازه"
|
||||||
|
git pull --ff-only || fail "git pull نشد. اگر تغییرِ محلی دارید اول تعیین تکلیفش کنید."
|
||||||
|
else
|
||||||
|
step "گرفتنِ کد رد شد (SKIP_PULL=1)"
|
||||||
|
fi
|
||||||
|
printf ' کامیتِ فعلی: %s\n' "$(git rev-parse --short HEAD) $(git log -1 --format=%s)"
|
||||||
|
|
||||||
|
# ─── ۲. محیطِ پایتون ────────────────────────────────────────────────────────
|
||||||
|
step "فعالسازیِ محیطِ مجازی"
|
||||||
|
if [ -d venv ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source venv/bin/activate
|
||||||
|
printf ' venv فعال شد\n'
|
||||||
|
elif [ -n "${VIRTUAL_ENV:-}" ]; then
|
||||||
|
printf ' از venv فعالِ فعلی استفاده میشود: %s\n' "$VIRTUAL_ENV"
|
||||||
|
else
|
||||||
|
warn "هیچ venv ای پیدا نشد؛ از پایتونِ سیستم استفاده میشود."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── ۳. وابستگیها ──────────────────────────────────────────────────────────
|
||||||
|
# همیشه اجرا میشود، نه فقط وقتی requirements عوض شده: تشخیصِ «عوض شده یا نه»
|
||||||
|
# خودش یک جای فراموششدنیِ دیگر است، و pip وقتی همهچیز نصب است سریع تمام میکند.
|
||||||
|
step "نصبِ وابستگیها"
|
||||||
|
PIP_ARGS=(-r requirements.txt --disable-pip-version-check)
|
||||||
|
if [ -n "${PIP_INDEX_URL:-}" ]; then
|
||||||
|
printf ' از ایندکسِ %s استفاده میشود\n' "$PIP_INDEX_URL"
|
||||||
|
fi
|
||||||
|
python -m pip install "${PIP_ARGS[@]}" \
|
||||||
|
|| fail "نصبِ وابستگیها شکست خورد. اگر pypi.org تایماوت میدهد، این را امتحان کنید:
|
||||||
|
PIP_INDEX_URL=https://mirror-pypi.runflare.com/simple ./deploy.sh"
|
||||||
|
|
||||||
|
# ─── ۴. دیتابیس ────────────────────────────────────────────────────────────
|
||||||
|
step "اعمالِ مهاجرتهای دیتابیس"
|
||||||
|
python manage.py migrate --noinput || fail "migrate شکست خورد؛ سرویس ریاستارت نشد."
|
||||||
|
|
||||||
|
# ─── ۵. فایلهای استاتیک ───────────────────────────────────────────────────
|
||||||
|
# فقط در حالتِ پروداکشن لازم است: با DEBUG=1 خودِ whitenoise از finders میخواند.
|
||||||
|
step "جمعآوریِ فایلهای استاتیک"
|
||||||
|
if python -c "
|
||||||
|
import django, os
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE','config.settings')
|
||||||
|
django.setup()
|
||||||
|
from django.conf import settings
|
||||||
|
raise SystemExit(0 if settings.DEBUG else 1)
|
||||||
|
" 2>/dev/null; then
|
||||||
|
printf ' DEBUG روشن است — لازم نیست.\n'
|
||||||
|
else
|
||||||
|
python manage.py collectstatic --noinput >/dev/null \
|
||||||
|
|| warn "collectstatic شکست خورد؛ پنلِ ادمین ممکن است بدونِ CSS بالا بیاید."
|
||||||
|
printf ' انجام شد.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── ۶. بررسیهای پیش از راهاندازی ────────────────────────────────────────
|
||||||
|
step "بررسیِ تنظیماتِ استقرار"
|
||||||
|
python manage.py check --deploy 2>&1 | sed 's/^/ /' || true
|
||||||
|
|
||||||
|
# ─── ۷. ریاستارت ──────────────────────────────────────────────────────────
|
||||||
|
step "ریاستارتِ سرویس"
|
||||||
|
SERVICE="${TADRISINO_SERVICE:-}"
|
||||||
|
if [ -z "$SERVICE" ] && command -v systemctl >/dev/null 2>&1; then
|
||||||
|
for candidate in tadrisino tadrisino-api mathapp gunicorn; do
|
||||||
|
if systemctl list-unit-files "${candidate}.service" >/dev/null 2>&1 \
|
||||||
|
&& systemctl cat "${candidate}.service" >/dev/null 2>&1; then
|
||||||
|
SERVICE="$candidate"; break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$SERVICE" ]; then
|
||||||
|
sudo systemctl restart "$SERVICE" && printf ' سرویسِ %s ریاستارت شد\n' "$SERVICE"
|
||||||
|
else
|
||||||
|
warn "سرویسِ systemd پیدا نشد؛ خودتان ریاستارت کنید، مثلاً:
|
||||||
|
sudo systemctl restart <نامِ سرویس>
|
||||||
|
و برای اینکه دفعهی بعد خودکار شود:
|
||||||
|
TADRISINO_SERVICE=<نامِ سرویس> ./deploy.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── ۸. آیا واقعاً بالا آمد؟ ───────────────────────────────────────────────
|
||||||
|
step "بررسیِ سلامت"
|
||||||
|
printf ' %s\n' "$HEALTH_URL"
|
||||||
|
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
BODY="$(curl -s -m 10 "$HEALTH_URL" 2>/dev/null || true)"
|
||||||
|
CODE="$(curl -s -m 10 -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo 000)"
|
||||||
|
if [ "$CODE" = "200" ]; then
|
||||||
|
printf '%s✓ سرویس سالم بالا آمد.%s\n' "$C_OK" "$C_OFF"
|
||||||
|
printf '%s\n' "$BODY" | python -m json.tool 2>/dev/null | sed 's/^/ /' || printf ' %s\n' "$BODY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s✖ سرویس بعد از ۳۰ ثانیه هنوز سالم نیست (آخرین کد: %s)%s\n' "$C_ERR" "$CODE" "$C_OFF" >&2
|
||||||
|
printf '%s\n' "$BODY" | head -c 600 >&2; echo >&2
|
||||||
|
cat >&2 <<'HINT'
|
||||||
|
|
||||||
|
لاگ را ببینید:
|
||||||
|
sudo journalctl -u <نامِ سرویس> -n 60 --no-pager
|
||||||
|
رایجترین علتها:
|
||||||
|
ModuleNotFoundError → وابستگیِ جدید نصب نشده (این اسکریپت باید حلش میکرد)
|
||||||
|
ImproperlyConfigured → متغیرِ محیطیِ لازم ست نیست (SECRET_KEY / CACHE_URL)
|
||||||
|
502 از nginx → پروسهی جنگو اصلاً بالا نیامده
|
||||||
|
HINT
|
||||||
|
exit 1
|
||||||
Reference in New Issue
Block a user