این سند برای تیم DevOps و فروش تهیه شده است: نیازمندیهای سرور برای هر مشتری، خطلولهی استقرار، پایش زندهی مصرف منابع، استراتژی احراز هویت میان PHP و Django، و وضعیت ORM. همهی اعداد از اندازهگیری واقعی روی استک در حال اجرا بهدست آمدهاند.
| سرویس | ایمیج/بیلد | نقش |
|---|---|---|
| app | php:8.4-fpm-bullseye (build docker/Dockerfile) | PHP-FPM |
| web | nginx:alpine | reverse proxy / FastCGI |
| db | mariadb:10.5 | پایگاهداده |
| cron | همان app | اجرای ow_cron/run.php هر ۶۰ ثانیه (تعریفشده، فعلاً غیرفعال) |
pdo_mysql, mysqli, mbstring, exif, pcntl, bcmath, gd, sockets, zip, soap, intl, ftp + OPcache (فعال، ۱۲۸MB).client_max_body_size 20M, fastcgi_read_timeout 300.illuminate/database|events|support|container|pagination (اجزای Eloquent لاراول)، Symfony (cache/console/http-foundation)، Guzzle، Monolog، PhpSpreadsheet، Shieldon (WAF)، swagger-php، firebase/php-jwt، php-di، wideimage، morilog/jalali. درخت vendor ≈ ۲۰۵MB.| سرویس | وضعیت |
|---|---|
| RabbitMQ/AMQP | در کد هست ولی log-over-RabbitMQ غیرفعال شده؛ broker در compose نیست |
| WebSocket (Ratchet) | سرویس socket در compose کامنت شده |
Elasticsearch (ruflin/elastica) | backend جستجو؛ container ندارد |
درگاه پیامک (iistpnetsms) | فعال؛ API خارجی صدا میزند |
| SMTP | نیازمند relay خارجی |
| S3/CloudFiles | OW_USE_CLOUDFILES=false (غیرفعال؛ فایلها محلی) |
Nexus/Debian mirror (nexus.danafakher.ir, debian-main.devneeds.ir) | وابستگی زمان build؛ بدون دسترسی به این میرورها ایمیج build نمیشود |
| Redis | ندارد |
docker stats --no-stream (استک بیبار)| کانتینر | RAM |
|---|---|
| app (php-fpm) | ~۵۲ MiB بیبار → ~۶۹ زیر بار |
| db (MariaDB) | ~۱۶۶ MiB (با دیتابیس ۱۷MB و buffer pool پیشفرض ۱۲۸MB) |
| web (nginx) | ~۲۶ MiB |
init.php هرکدام در هر درخواست اجرا میشود (Oxwall پلاگینهای فعال را eager-load میکند؛ lazy loading ندارد) + بارگذاری Smarty/Twig/Illuminate/Symfony/Guzzle/Shieldon.RAM app = master + pm.max_children × RSS هر worker
31 + 5 × 57 ≈ 316 MB سقف پایدار.memory_limit):31 + 5 × 256 MB = 1.31 GB
pm.max_children ضریب است. مقدار shipped برابر ۵ است (سقف ~۳۱۶MB)؛ اما اپراتورها معمولاً برای همزمانی آن را بالا میبرند: در max_children=20 تنها PHP = 20×57 ≈ 1.14GB، در ۴۰ → 2.3GB. این مکانیزم گمشدن RAM است.docker/php/www.conf (که pm.max_children=10 + status page + slowlog دارد) هرگز داخل ایمیج کپی نمیشود — Dockerfile فقط docker/php-fpm.conf را کپی میکند. پس کانتینر روی www.conf استاندارد (max_children=5) اجرا میشود و هر tuning در docker/php/www.conf بیاثر است. باید این کپی در Dockerfile اضافه شود.memory_limit=256MB هر درخواست یعنی رگبار درخواستهای سنگین میتواند هرکدام ۴–۵ برابر worker بیبار متورم شود پیش از reap.فرضها: بدون swap (سقف سخت RAM)، ~۲۰٪ headroom، هر worker ۷۰MB (بدبینانه برای جذب spike تصویر/اکسپورت)، OS+Docker ~۵۰۰MB.
| پروفایل | worker های PHP | buffer pool | ریاضی سقف | RAM (بدبینانه) | vCPU | دیسک |
|---|---|---|---|---|---|---|
| بار تست فعلی | ۵ | ۱۲۸MB | ۳۱۶ + ۱۶۰ + ۳۰ + ۵۰۰ ≈ ۱GB | ۲GB | ۱–۲ | ۲۰GB |
| کوچک (<۵۰۰ کاربر) | ۱۰ | ۲۵۶MB | ۷۳۰ + ۳۲۰ + ۴۰ + ۵۰۰ ≈ ۱.۶GB | ۲–۳GB | ۲ | ۲۰–۳۰GB |
| متوسط (<۵۰۰۰ کاربر) | ۲۵–۳۰ | ۱–۲GB | ۲.۱GB + ۲GB + ۵۰ + ۵۰۰ ≈ ۴.۷GB | ۸GB | ۴ | ۶۰–۱۰۰GB |
هشدارهای بدبینانه:
max_children را بدون RAM متناسب بالا ببرد، OOM-kill تقریباً حتمی است: شعاع انفجار واقعی max_children × 256MB است (۲۰ worker × ۲۵۶MB = ۵.۱GB).ow_log بدون rotation رشد نامحدود دارد — روی استوریج پایششده نگه دارید.جملهی نهایی: این اپ memory-bound است، نه CPU-bound. هرگز روی <۲GB اجرا نکنید. RAM را اینطور اندازه بگیرید:
500MB (OS) + buffer_pool + (max_children × 70MB) + 20% headroom
# ۱) کد
git clone <repo> motoshub && cd motoshub
git checkout motonext # شاخهی زنده
# ۲) تنظیمات محیط
cp .env.example .env
# ویرایش .env: DB_*، SITE_URL، PROJECT_NAME، OW_PASSWORD_PEPPER (کلید امضای JWT)
# ۳) بالا آوردن استک
docker compose up -d --build # app + db + web (+ cron در prod)
# ۴) نصب/آپدیت اکسوال و پلاگینها
# نصب تازه از طریق ماژول init یا صفحهی نصب؛
# سایت موجود: از پنل مدیریت، آپدیتهای در انتظار پلاگینها را اجرا کنید
# (اکسوال با دیدن بامپ build در plugin.xml، update.php ها را خودکار اجرا میکند).
# ۵) پاکسازی کش
rm -rf ow_smarty/template_c/* ow_static/tmp/*
# ۶) راستیآزمایی
curl -i https://<domain>/ # 200
curl -i https://<domain>/api/v1/docs/json # 200، خروجی OpenAPI
docker/php/www.conf را اضافه کنید تا tuning استخر (max_children، status page، slowlog) واقعاً اعمال شود.docker/nexus.pem روی هیچ شاخهای نیست و مرحلهی build را میشکند — یا گواهی Nexus را به مخزن/CI بیفزایید یا آن خط Dockerfile را مشروط کنید.ow_log تنظیم کنید.docker stats # زنده CPU/MEM هر کانتینر
docker stats --no-stream # snapshot یکباره برای اسکریپت
docker compose exec app top # اگر procps نصب باشد
# fallback بدون top/ps (این ایمیج ندارد):
docker compose exec app sh -c 'for p in $(ls /proc|grep -E "^[0-9]+$"); do \
grep -qa "pool www" /proc/$p/cmdline 2>/dev/null && \
echo "$p $(grep VmRSS /proc/$p/status|awk "{print \$2}")kB"; done'
curl -s "https://<domain>/status?full" # worker های active/idle/total، صف، peak
curl -s https://<domain>/ping # → pong
docker compose exec db mysql -uroot -p<pass> -e "SHOW FULL PROCESSLIST;"
docker compose exec db mysql -uroot -p<pass> -e "SHOW ENGINE INNODB STATUS\G"
docker compose exec db mysql -uroot -p<pass> -e \
"SET GLOBAL slow_query_log=ON; SET GLOBAL long_query_time=1; \
SET GLOBAL slow_query_log_file='/var/lib/mysql/slow.log';" # فعالسازی بدون restart
# روند رشد دیتابیس:
docker compose exec db mysql -uroot -p<pass> -e \
"SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024,1) MB \
FROM information_schema.tables GROUP BY table_schema;"
| ابزار | overhead | توضیح |
|---|---|---|
| ctop | ~۱۰–۲۰MB | باینری Go تکفایل، نمای top مثل هر کانتینر. بهترین انتخاب کمزحمت |
| glances --docker | ~۵۰–۸۰MB | داشبورد یکصفحهای host+container |
| cAdvisor | ~۵۰–۱۰۰MB | صادرکنندهی متریک کانتینر، خوراک Prometheus |
| node_exporter + Prometheus + Grafana | ~۵۰۰MB مجموع | استک کامل متریک/هشدار؛ برای ناوگان، نه یک هاست کوچک |
وضعیت فعلی: API (لایهی جدید) با JWT (الگوریتم HS256) کار میکند. توکن با hash('sha256', OW_PASSWORD_PEPPER) امضا میشود و claims دارد: { iat, exp, sub: userId }. ورود از POST /api/v1/auth/login توکن یکساعته میدهد.
معماری هدف شما: api.shub.ir (Gateway) جلوی api1 (PHP/Oxwall) و api2 (Django).
OW_PASSWORD_PEPPER در PHP همان رازی باشد که Django برای امضا/اعتبارسنجی JWT استفاده میکند. کلید نهایی = sha256(pepper).exp، خواندن sub بهعنوان userId). کتابخانه: PyJWT — jwt.decode(token, key, algorithms=['HS256']) که key = sha256(pepper).hexdigest() (باید دقیقاً همان hex که PHP میسازد باشد).Authorization: Bearer <token> را دستنخورده به api1 و api2 بفرستد. هر دو سرویس مستقل همان توکن را میپذیرند.sub همان userId جدول ow_base_user است. Django باید به همان دیتابیس (یا یک نگاشت) دسترسی داشته باشد تا userId را به کاربرش وصل کند.مزیت: هیچ تغییری در کد PHP لازم نیست؛ فقط راز مشترک + پیادهسازی decode در Django.
محدودیتها (صادقانه):
همه (فرانت، Gateway، PHP، Django) توکن را از یک IdP میگیرند و با کلید عمومی verify میکنند. تکنقطهی حقیقت برای هویت، پشتیبانی refresh/revoke استاندارد. برای مقیاس چندسرویسی درست است، ولی راهاندازی جدا میخواهد.
پاسخ دقیق: بله و نه — بستگی به لایه دارد. دو لایهی دسترسی به داده روی یک MariaDB مشترک (shub_db، پیشوند ow_):
ow_plugins/*/bol/*_dao.php که از OW_BaseDao ارث میبرند. Eloquent نیست — خودش SQL میسازد (SELECT * FROM ow_photo WHERE id = ?) و از OW_Example فیلتر میگیرد. نمونه: PHOTO_BOL_PhotoDao::getInstance()->findById($id).ow_plugins/*/src/Models/*.php و src/Repositories/*.php. واقعاً Eloquent است — مدل پایه Bridge\Models\BaseModel از Illuminate\Database\Eloquent\Model ارث میبرد؛ ۷۴ مدل در ۲۶ پلاگین. نمونه: Photo::query()->with('album')->find($id) و Photo::create([...]).نکتهی صادقانه: این Laravel نیست. در composer.json هیچ laravel/framework نیست؛ فقط اجزای مستقل Illuminate (illuminate/database v13.16.1 و چند بسته). Eloquent دستی و از طریق یک «Bridge» سفارشی راهاندازی شده (bridge/src/Kernel.php با Capsule::bootEloquent()). پس عبارت درست: «API جدید از کامپوننت ORM لاراول (Eloquent) استفاده میکند، اما اپ یک اپ لاراولی نیست؛ یک اپ Oxwall است که Eloquent به آن اضافه شده.»
ORM یعنی چه؟ ابزاری که ردیفهای جدول را به شیء PHP تبدیل میکند تا بهجای SQL با متدهای شیء کار کنید. Eloquent ORM لاراول با الگوی Active Record است: هر کلاس مدل = یک جدول، هر شیء = یک ردیف که خودش میداند چطور ذخیره/بهروزرسانی/حذف شود.
ریسک مهم — نوشتن دوگانه: هر دو لایه به همان جدولها مینویسند. نوشتن با Eloquent، رویدادها و پاکسازی کشِ لایهی قدیمی را دور میزند:
OW_BaseDao::save() رویداد امنیتی/کش/اعلان trigger میکند؛ Photo::create()/update() اینها را اجرا نمیکند.BaseModel::$timestamps=false است، پس Eloquent created_at/updated_at نمینویسد (که اسکیمای اکسوال ندارد) — از رایجترین crash جلوگیری میکند.جمعبندی برای مستندات: لایهی API یک بازنویسی مشروع مبتنی بر Eloquent روی لایهی DAO قدیمی است، هر دو روی یک MariaDB. Eloquent است، نه Laravel. خطر اصلی، مسیر نوشتن دوگانه است؛ هر جدولی که هر دو لایه مینویسند نیاز به دقت (یا یک نویسندهی معتبر واحد) دارد.
تهیهشده پس از مطالعهی کامل سورس و اندازهگیری زنده روی استک Docker.