The README had grown into a 295-line mixture of onboarding, an ops runbook, a content-authoring workflow, bulk question generation and a Docker log stack. A reader arriving at the repo could not tell in ten seconds what this service is, whether it is live, or where to start, and five of its claims no longer matched the code: - it said to export .env values by hand; settings.py has loaded .env through python-dotenv for a while now - it listed six variables as "everything the app actually reads"; .env.example has carried many more than that for some time - it called DJANGO_SECRET_KEY, DJANGO_ALLOWED_HOSTS and CACHE_URL required in production; all three now have working defaults, and the app boots without them on purpose so a partial .env cannot take the service down - it claimed there is no insecure SECRET_KEY fallback; there is one, under DEBUG - it required Python 3.12 while the project runs on 3.11 (Docker uses 3.12) The README now states what the service is, where it runs, how healthy it is and how to get it running in six commands, then links onward. Procedures moved to docs/operations.md, docs/content-authoring.md, docs/architecture.md and docs/docker.md, which is where someone follows a procedure rather than skims it. Nothing was dropped; the deploy one-liner now includes the separate sudo restart that deploy.sh cannot perform itself, and backup/restore is written down for the first time. Every number in the README was measured rather than quoted: content counts from the live /api/health and content/seed/curriculum.json, 55 endpoints from the generated OpenAPI schema, 481 tests from a full run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
عملیات
دیپلوی، پیکربندی، سلامت و بازیابی. برای شروعِ توسعهی محلی README.md را
ببینید.
دیپلوی
ssh -t administrator@78.38.35.249 \
'cd ~/mathapp-v2 && ./deploy.sh; sudo systemctl restart zareian-mathapp'
دو بار رمز میپرسد — یکبار برای ssh و یکبار برای sudo. منتظرِ هر پرامپت
بمانید؛ چسباندنِ چند خط با هم، متنِ صفشده را به پرامپتِ رمز میدهد و بعد از
سه تلاش قفل میشوید.
deploy.sh به ترتیب: fetch، برگرداندنِ درخت به origin/<branch>، نصبِ
وابستگیها، migrate، seed_content، collectstatic، ریاستارت، و در پایان
poll کردنِ /api/health.
دو رفتارش را بدانید:
- سرور را تعمیر میکند، جلویش نمیایستد. فایلِ دستیویرایششده قبلاً
git pull --ff-onlyرا میشکست و چون بقیهی مراحل رد میشدند، سرور بیصدا روی کدِ قدیمی میماند. حالا تغییراتِ محلی در.deploy-backups/بهصورتِ patch ذخیره و بعد باgit reset --hardدور ریخته میشوند، و کار ادامه مییابد. هر اجرای کامل، سرور را دقیقاً روی همان چیزی میگذارد که در Gitea است. - ریاستارت را اثبات میکند. زمانِ
ActiveEnterTimestampرا قبل و بعد مقایسه میکند و اگر عوض نشده باشد با کدِ غیرِصفر خارج میشود. قبلاً فقط آدرسِ عمومی را چک میکرد و پروسهی قدیمی همان200را میداد — سرور دو روز کدِ قدیمی سرو کرد در حالی که همهی فایلها روی دیسک تازه بودند.
فایل روی دیسک، فایلِ دیپلویشده نیست. قبل از باور کردنِ هر چیزی:
systemctl show zareian-mathapp -p ActiveEnterTimestamp
متغیرهای اختیاری: SKIP_PULL=1، TADRISINO_SERVICE، TADRISINO_HEALTH_URL،
PIP_INDEX_URL (وقتی pypi.org تایماوت میدهد).
بررسیِ سلامت
curl https://api.math.tadrisino2.ir/api/health
بدونِ احرازِ هویت و بدونِ SSH میگوید: دیتابیس سالم است یا نه، مهاجرتِ
اعمالنشده مانده یا نه، کش کار میکند یا نه، چند پایه/درس/سوال روی آن سرور
هست، کدام سرویسِ بیرونی تنظیم شده (فقط بله/خیر)، و چه هشدارهای امنیتیای
فعالاند. 200 سالم، 503 خرابیِ واقعی. هیچ سکرتی برنمیگرداند.
مهمترین بررسیاش «مهاجرتِ اعمالنشده» است: شایعترین حالتِ دیپلویِ خراب این
است که کد رفته ولی migrate اجرا نشده — اپ بالا میآید و بعد زیرِ بارِ واقعی
با خطای ستونِ ناموجود میشکند.
تأییدِ سریعِ اینکه API واقعاً جواب میدهد:
curl -i https://api.math.tadrisino2.ir/api/schema/
curl -i -X POST https://api.math.tadrisino2.ir/api/auth/session \
-H 'Content-Type: application/json' \
-d '{"type":"guest","deviceId":"verify-1"}'
بکاپ و بازیابی
db.sqlite3 در گیت نیست و تنها نسخهی دادهی کاربران است. deploy.sh
پیش از migrate بکاپ نمیگیرد؛ تا وقتی این خودکار نشده، دستی بگیرید:
cd /home/administrator/mathapp-v2
mkdir -p .deploy-backups
venv/bin/python - <<'PY'
import sqlite3, datetime
dst = ".deploy-backups/db-%s.sqlite3" % datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
src, out = sqlite3.connect("db.sqlite3"), sqlite3.connect(dst)
with out:
src.backup(out) # نسخهی سازگار حتی اگر همزمان کسی بنویسد
out.close(); src.close()
print("backup:", dst, sqlite3.connect(dst).execute("PRAGMA integrity_check").fetchone()[0])
PY
sqlite3 بهعنوانِ فرمانِ خط فرمان روی آن سرور نصب نیست — از پایتونِ خودِ
پروژه استفاده کنید، نه cp؛ کپیِ ساده وسطِ یک نوشتن، فایلِ نیمهکاره میدهد.
بازیابی: سرویس را بخوابانید، فایل را برگردانید، دوباره بالا بیاورید.
sudo systemctl stop zareian-mathapp
cp .deploy-backups/db-<timestamp>.sqlite3 db.sqlite3
sudo systemctl start zareian-mathapp
curl https://api.math.tadrisino2.ir/api/health
متغیرهای محیطی
.env خودکار خوانده میشود (python-dotenv) و مقادیرش با پیشفرضهای کد
ادغام میشوند، نه جایگزینشان. پس یک .env ناقص روی سرور دیگر باعثِ بالا
نیامدن نمیشود. .env.example توضیحِ کاملِ هر کدام را دارد.
| متغیر | اگر ست نشود چه میشود |
|---|---|
DJANGO_DEBUG |
خاموش (0) — که حالتِ درستِ پروداکشن است |
DJANGO_SECRET_KEY |
یک کلیدِ تصادفیِ قوی ساخته و در .django_secret_key (مجوز 0600) ذخیره میشود |
DJANGO_ALLOWED_HOSTS |
دامنههای خودمان پیشفرضاند |
CSRF_TRUSTED_ORIGINS · CORS_ALLOWED_ORIGINS · PUBLIC_BASE_URL |
پیشفرضِ درست دارند |
CACHE_URL |
سرویس بالا میآید ولی نرخ بینِ workerها مشترک نیست؛ /api/health هشدار میدهد |
KAVENEGAR_API_KEY |
POST /api/auth/otp/request خطای صریحِ 503 میدهد، نه موفقیتِ ساختگی |
ZARINPAL_MERCHANT_ID |
مسیرِ پرداخت fail-closed میماند (503) |
OPENAI_API_KEY · QDRANT_API_KEY |
POST /api/ai/ask همان پاسخِ stub را میدهد |
TEST_LOGIN_PHONES |
هیچ حسابِ تستی باز نیست (بخشِ بعد) |
DJANGO_SECRET_KEYتوکنهای ورود را امضا میکند. بهترین حالت این است که اصلاً ست نشود تا خودِ تنظیمات یکی بسازد — آنوقت کلیدی وجود ندارد که لو برود. هرگز نگذارید سرور روی fallbackِdjango-insecure-…کار کند: آن رشته در همین ریپو کامیت شده، پس هر کسی که ریپو را بخواند میتواند بهجای هر کاربری توکن جعل کند.manage.py check --deployبا کدِsecurity.W009علامتش میزند.
DJANGO_DEBUGهرگز روی سرور1نشود.DEV_MASTER_OTP(کد0000) را روشن میکند که هر شمارهای را بهعنوانِ ورودِ معتبر میپذیرد، و صفحههای خطا کلِ تنظیمات را نشان میدهند. جایش فقط.envاست، نه یونیتِ systemd.
ورودِ توسعهدهندهها (بدونِ سرویسِ پیامک)
تا وقتی KAVENEGAR_API_KEY ست نشده، با DEBUG=0 هیچکس نمیتواند با شماره
وارد شود. برای اینکه ورودِ تیم به یک نفرِ خاص وابسته نباشد، چند شمارهی تست با
کدِ ثابت تعریف کنید:
TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947
برای این شمارهها پیامکی ارسال نمیشود و همان کد همیشه معتبر است. هر شمارهی دیگری مسیرِ عادیِ OTP را میرود، بیهیچ تغییری.
curl -X POST https://api.math.tadrisino2.ir/api/auth/otp/request \
-H 'Content-Type: application/json' -d '{"phone":"09120000001"}'
curl -X POST https://api.math.tadrisino2.ir/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"phone":"09120000001","otp":"834512"}'
این همان الگوییست که Firebase Auth (با نامِ fictional phone numbers) و Apple (حسابِ دمو برای بررسیِ App Store) برای همین مسئله دارند.
- با
DEV_MASTER_OTPیکی نیست. آن کدِ0000هر شمارهای را میپذیرد و فقط باDEBUG=1روشن میشود. این فقط همین چند شماره را، هرکدام با کدِ خودش. - کد در پاسخِ API برنمیگردد، پس دانستنِ شماره برای ورود کافی نیست. سقفِ تلاشِ ورودِ هر شماره روی اینها هم اعمال میشود، پس حدسِ کد بسته است.
- مقدارِ نامعتبر سرویس را بالا نمیآورد و پیامِ صریح میدهد — بیصدا رد
نمیشود. کد باید ۴ تا ۶ رقم باشد (
LoginRequestSerializerسقفِ ۶ کاراکتر دارد). /api/healthتا وقتی پر باشد هشدار میدهد — فقط تعداد، نه شماره و نه کد.- شماره را دقیقاً همانطور بنویسید که کلاینت میفرستد؛ این کدبیس شماره را
normalize نمیکند، پس
09121111111و+989121111111دو کاربرِ جدا هستند. - کدِ تصادفی بگذارید، نه
1234، و پیش از باز شدنِ سرویس روی کاربرانِ واقعی خالیاش کنید.
سرور
api.math.tadrisino2.ir روی یک ماشینِ مشترکِ شرکت اجرا میشود
(administrator@78.38.35.249)، نه سروری که مالِ این پروژه باشد. روی همان ماشین
Gitea، سرویسِ ایمیل، DNS، چند دیتابیس و اپهای تیمهای دیگر در حالِ کارند.
مالِ ما دقیقاً یک دایرکتوری است: /home/administrator/mathapp-v2، که با
gunicorn-mathapp.service سرو میشود. همان یونیت با نامِ zareian-mathapp هم
پاسخ میدهد و همیشه همین نام را بنویسید:
sudo systemctl restart zareian-mathapp # مالِ ما
sudo systemctl restart gunicorn # مالِ ما نیست — اپِ تیمِ دیگر
قواعدی که قابلِ مذاکره نیستند:
- هیچ فرمانِ مخربی بیرون از
~/mathapp-v2اجرا نشود. مسیر را کامل بنویسید، به دایرکتوریِ جاری تکیه نکنید. git cleanآنجا هرگز اجرا نشود.db.sqlite3،.envو.django_secret_keyهیچکدام در گیت نیستند و همانجا زندگی میکنند؛reset --hardکاری با آنها ندارد ولیclean -fdدیتابیس، پیکربندی و همهی نشستهای فعال را نابود میکند.- پروژه هرگز پاک و «از نو clone» نشود — به همان دلیل. برای برگرداندنِ
درخت به حالتِ سالم از
git reset --hard origin/mainاستفاده کنید. .envفقط از راهِ Django خوانده شود.cat .envنه، و مقادیرش در هیچ چتی paste نشود.df -h /را قبل از هر کارِ پرنوشت چک کنید. دیسک مدتیست حدودِ ۹۴٪ پر است؛ پر شدنش سرویسِ ایمیل و Gitea را هم با ما پایین میکشد.
لاگها
هر درخواستِ /api/ بهصورتِ یک شیءِ JSON در هر خط لاگ میشود
(method/path/status/duration/user — بدونِ بدنه، هدر یا سکرت) توسطِ
config.middleware.APIRequestLoggingMiddleware. خروجی روی stdout است و اگر
LOG_DIR ست باشد در app.log هم نوشته میشود.
هر خطای پیشبینینشده همیشه پاسخِ JSON برمیگرداند — نه صفحهی HTML پیشفرضِ
جنگو — و کامل با traceback لاگ میشود (config/exceptions.py).