Skip to content

إعداد الإنتاج

انشر Potato في الإنتاج باستخدام gunicorn وnginx أو Apache، مع إنهاء TLS وDocker واستمرارية الجلسات والتحكم في الوصول الذي يحتاجه خادم مشترك.

انشر Potato لمهام التعليق التوضيحي الفعلية مع مستخدمين متعددين واستمرارية البيانات والأمان المناسب.

إعداد الخادم

إعداد الإنتاج الأساسي

yaml
server:
  host: 0.0.0.0
  port: 8000
  debug: false

تقبل كتلة server المفاتيح host وport وdebug فقط. أما عدد العمال والمهلات الزمنية فتخص خادم WSGI الذي يشغّل Potato وليس ملف الإعداد. أبقِ debug معطلاً في الإنتاج، لأن وضع التصحيح يفتح صفحات المسؤول دون مفتاح للطلبات الآتية من جهاز الخادم نفسه، ومنها على المضيف المنشور طلبات وكيله العكسي.

استخدام HTTPS

لا يحمل ملف إعداد Potato أي إعدادات TLS، لذلك لا تُضبط الشهادات فيه. أنهِ TLS عند الوكيل العكسي الموضوع أمام Potato، وهو ما يُعدّه قسما nginx وApache أدناه، أو سلّم الشهادة إلى gunicorn مباشرة كما هو موضح في قسم «مع SSL».

التشغيل مع Gunicorn

شغّل Potato تحت gunicorn بعامل واحد بالضبط، وارفع عدد الخيوط عندما تحتاج إلى مزيد من التزامن:

bash
gunicorn "potato.flask_server:create_app('config.yaml')" \
  --workers 1 \
  --threads 8 \
  --bind 0.0.0.0:8000 \
  --timeout 120

العامل الواحد متطلب لصحة البيانات وليس خياراً لضبط الأداء. يحتفظ Potato بمجموعة العناصر وطابور التعيين وحالة كل معلّق في الذاكرة داخل العملية. يحصل العامل الثاني على نسخته الخاصة من الثلاثة، فيوزّع عناصر سبق أن عيّنها العامل الأول، ولأن user_state.json يُعاد كتابته بالكامل عند كل حفظ، فإن العامل الذي يحفظ أخيراً يمحو تعليقات الآخر. لا شيء يُبلغ عن هذه الخسارة، والتعليقات تختفي ببساطة. تتشارك الخيوط نسخة واحدة من تلك الحالة، لذا يرفع --threads التزامن بأمان، وهو ما لا يفعله --workers.

مع SSL

bash
gunicorn "potato.flask_server:create_app('config.yaml')" \
  --workers 1 \
  --threads 8 \
  --bind 0.0.0.0:443 \
  --certfile /path/to/cert.pem \
  --keyfile /path/to/key.pem

إعداد الوكيل العكسي

إعداد Nginx

nginx
upstream potato {
    server 127.0.0.1:8000;
}
 
server {
    listen 80;
    server_name annotation.example.com;
    return 301 https://$server_name$request_uri;
}
 
server {
    listen 443 ssl;
    server_name annotation.example.com;
 
    ssl_certificate /etc/letsencrypt/live/annotation.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/annotation.example.com/privkey.pem;
 
    location / {
        proxy_pass http://potato;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
 
        # Long annotation sessions
        proxy_read_timeout 300;
        proxy_connect_timeout 300;
        proxy_send_timeout 300;
    }
}

إعداد Apache

apache
<VirtualHost *:443>
    ServerName annotation.example.com
 
    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem
 
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8000/
    ProxyPassReverse / http://127.0.0.1:8000/
 
    <Proxy *>
        Order allow,deny
        Allow from all
    </Proxy>
</VirtualHost>

التركيب تحت مسار URL

يتطلب تقديم Potato على https://host/app1/ بدلاً من جذر النطاق خطوة إضافية واحدة. تطلب صفحة التعليق التوضيحي /static/... و/updateinstance نسبةً إلى الجذر العام، لذلك تعيد الأصول الخطأ 404 تحت بادئة المسار ويفشل حفظ التعليقات، بينما يستمر عرض هيكل الصفحة.

أخبر Potato ببادئته العامة. عندما تتحكم في الوكيل ويستطيع إرسال ترويسات التمرير (forwarded headers):

bash
export POTATO_PROXY_FIX=1
potato start config.yaml -p 8000
nginx
location /app1/ {
    proxy_pass         http://127.0.0.1:8000/;   # trailing slash strips /app1/
    proxy_set_header   Host              $host;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Forwarded-Prefix /app1;
}

فعّل POTATO_PROXY_FIX فقط عندما يكون الوصول إلى التطبيق حصرياً عبر ذلك الوكيل. فهو يثق بترويسات التمرير، لذا يستطيع عميل يصل إلى المنفذ الداخلي مباشرة أن يزيّف البادئة ويفسد كل عنوان URL يولّده التطبيق.

عندما لا تستطيع إضافة ترويسات لكنك تعرف مسار التركيب، اضبطه مباشرة. يجب أن يظل الوكيل يحذف البادئة قبل تمرير الطلب:

bash
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000

اختبر ذلك بتسجيل الدخول من متصفح بلا جلسة، لأن نموذج تسجيل الدخول هو أول صفحة تتعطل عند غياب البادئة.

يبث عارض الوكيل المباشر وعارض البرمجة المباشرة عبر Server-Sent Events، ويحجز الوكيل الذي يخزّن الاستجابات مؤقتاً (buffering) البث حتى ينتهي. أوقف التخزين المؤقت على تلك المسارات:

nginx
location /app1/api/ {
    proxy_pass            http://127.0.0.1:8000/api/;
    proxy_set_header      Host $host;
    proxy_buffering       off;
    proxy_read_timeout    3600s;
}

نشر Docker

ينشر Potato صورة مبنية مسبقاً على GitHub Container Registry، هي ghcr.io/davidjurgens/potato. تحتوي الصورة على Potato واعتمادياته ولا شيء غيرها. يُركَّب مجلد مشروعك، بما فيه config.yaml وبياناته، في /app، فتخدم صورة واحدة كل المهام:

bash
docker run -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  -e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
  ghcr.io/davidjurgens/potato:latest

يحمل الوسم latest ووسوم الإصدارات، مثل 2.12.1، الاعتماديات الأساسية فقط. تضيف وسوم -all (latest-all و2.12.1-all) حزم SDK الخاصة بالذكاء الاصطناعي وقارئات الصيغ وإضافات التصدير والمصادقة، وتستبعد نماذج الرؤية.

تقدّم الحاوية المهمة بعامل gunicorn واحد وثمانية خيوط، للسبب المذكور في قسم gunicorn أعلاه. ضبط GUNICORN_WORKERS على قيمة أكبر من 1 يوقف الحاوية عند بدء التشغيل ما لم يُضبط POTATO_ALLOW_MULTIWORKER=1 أيضاً، وهذا الإعداد يُفقد التعليقات في دراسة حقيقية. ارفع GUNICORN_THREADS لمزيد من التزامن. يحدد POTATO_CONFIG ملف إعداد غير config.yaml، ويغيّر PORT المنفذ داخل الحاوية من 7860.

تعمل الحاوية بالمعرّف uid 1000 وتكتب مخرجات التعليق وpotato.log وقواعد بيانات SQLite الخاصة بها في /app. على Linux، إذا كان مجلد المشروع المركَّب عبر bind mount مملوكاً لمستخدم آخر، يتوقف الخادم عند الإقلاع برسالة خطأ تسمّي المشكلة. انقل ملكية المجلد إلى uid 1000 باستخدام sudo chown -R 1000:1000 myproject. يتجاهل Docker Desktop على Mac ملكية bind mount، لذلك لا يظهر هذا الخطأ إلا على Linux.

الوسائط التي تلي اسم الصورة تحل محل أمر الخادم، وهذا مفيد للفحوصات التي تُجرى لمرة واحدة:

bash
docker run -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:latest potato validate config.yaml --strict

Docker Compose

yaml
services:
  potato:
    image: ghcr.io/davidjurgens/potato:latest
    ports:
      - "8000:7860"
    volumes:
      - ./myproject:/app
    environment:
      - POTATO_SECRET_KEY=${POTATO_SECRET_KEY}
      - POTATO_ADMIN_API_KEY=${POTATO_ADMIN_API_KEY}
    restart: unless-stopped
 
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./certs:/etc/ssl/certs
    depends_on:
      - potato
    restart: unless-stopped

داخل شبكة Compose، يصل nginx إلى Potato باسم الخدمة على منفذ الحاوية، لذا تصبح كتلة upstream في قسم الوكيل العكسي أعلاه server potato:7860; بدلاً من server 127.0.0.1:8000;.

لأن مجلد المشروع مركَّب وليس منسوخاً داخل صورة، تبقى التعليقات التي يكتبها Potato فيه على المضيف عند استبدال الحاوية.

التحكم في الوصول

تقييد من يمكنه التسجيل

يسمح Potato بالتسجيل الذاتي افتراضياً. على خادم عام، حدّد من يُسمح لهم بالتسجيل:

yaml
user_config:
  allow_all_users: false
  users:
    - annotator1@example.com
    - annotator2@example.com

ينتمي كلا المفتاحين إلى user_config. إذا كُتبا في المستوى الأعلى يُتجاهلان، فتبدو قائمة الوصول مقيِّدة بينما تقبل الجميع.

وصول المسؤول

المفتاح admin_api_key مفتاح مشترك يُرسَل في الترويسة X-API-Key. عندما لا تضبطه، يولّد Potato مفتاحاً ويكتبه في {task_dir}/admin_api_key.txt، لذا اضبطه بنفسك على خادم الإنتاج وأبقِه خارج المستودع:

yaml
admin_api_key: "your-shared-key"
 
rbac:
  enabled: true
  user_role_assignments:
    lead@example.com: admin

مع إعداد rbac، يصل أشخاص محددون بأسمائهم إلى /admin بتسجيل الدخول العادي بدلاً من تداول المفتاح المشترك. راجع إدارة المستخدمين للاطلاع على مجموعة الصلاحيات الكاملة.

الجلسات

yaml
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200

يوقّع secret_key ملف تعريف الارتباط الخاص بالجلسة، ويمكن أن يأتي من POTATO_SECRET_KEY بدلاً من ذلك. من دونه يولّد Potato مفتاحاً جديداً عند كل بدء تشغيل، فتؤدي كل إعادة تشغيل إلى تسجيل خروج الجميع في منتصف المهمة. على خادم يُعاد تشغيله مع كل نشر، هذا هو الإعداد الذي يتسبب أكثر من غيره في فقدان التعليقات.

يمسح session_timeout_minutes الجلسة التي بقيت خاملة هذه المدة، ويبدأ العدّ من جديد مع كل طلب. وPotato 2.11 هو أول إصدار يطبّقه فعلياً، بقيمة افتراضية قدرها 480 دقيقة. وعند 60، كما في هذا المثال، يتعيّن على المعلّق الذي يبتعد ساعة أن يسجّل الدخول من جديد حين يعود.

مخرجات التعليق التوضيحي

yaml
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300

يكتب Potato مجلداً لكل معلّق تحت output_annotation_dir، يحتوي كل منها على ملف user_state.json الخاص بذلك الشخص. يحدد export_annotation_format الصيغ التي يكتبها التصدير الدوري، والقيمة الفارغة تعني عدم وجود تصدير تلقائي إطلاقاً. أما auto_export_interval فهو الفاصل بين عمليات التصدير بالثواني، وقيمته الافتراضية 60.

قاعدة البيانات الخلفية

تُحفظ التعليقات على القرص افتراضياً. لعمليات النشر التي تحتاج إلى قاعدة بيانات، يقبل database.type القيمة mysql أو file:

yaml
database:
  type: mysql
  host: localhost
  port: 3306
  database: potato_db
  username: potato
  password: ${DB_PASSWORD}

المفتاحان هما database وusername، وليسا name وuser. يمكن أن تأتي سلسلة الاتصال من POTATO_DB_CONNECTION بدلاً من ذلك، ما يُبقي بيانات الاعتماد خارج ملف الإعداد.

ثبّت برنامج التشغيل بالأمر pip install 'potato-annotation[mysql]'. قبل 2.10.2 لم تكن قاعدة بيانات MySQL الخلفية تعمل قط، وكان الإعداد الذي يذكرها يخزّن التعليقات على القرص دون أي تنبيه. ومنذ 2.10.2 يوقف غيابُ برنامج التشغيل أو تعذّرُ الوصول إلى الخادم بدءَ التشغيل، ويقرأ التصدير والنسخ الاحتياطي وpotato deploy pull من قاعدة البيانات.

فحوصات الصحة والسجلات

يقدّم Potato المسار GET /health في كل عملية نشر، ولا يحتاج هذا المسار إلى مصادقة، لذا يصلح فحصاً للجاهزية في موازن الحمل أو في منسِّق الحاويات:

bash
curl -sf https://annotation.example.com/health

يُضبط مستوى تفصيل السجلات في ملف الإعداد أو في سطر الأوامر، وليس في كتلة خاصة بالسجلات:

yaml
verbose: true
debug_log: server

يقبل debug_log القيم all أو ui أو server أو none. ويحصر متغير البيئة POTATO_LOGGERS المخرجات في مسجِّلات محددة بالاسم عندما تتتبع نظاماً فرعياً واحداً.

التوسع

يعمل Potato كعملية واحدة تحتفظ بمجموعة العناصر كاملة في الذاكرة، لذا وسّع الجهاز بدلاً من زيادة عدد الخوادم. تشغيل عدة نسخ متماثلة خلف موازن حمل لا يقسّم العمل. تحتفظ كل نسخة بنسختها الخاصة من طابور التعيين وحالة المعلّقين، وهذا يعيد إنتاج فقدان البيانات الموصوف في قسم gunicorn أعلاه بالضبط.

الذاكرة هي المورد الذي ينمو مع حجم مجموعة البيانات. عند نحو مليون عنصر، يمكن نقل الحمولات إلى ذاكرة تخزين مؤقت في SQLite:

yaml
item_store:
  backend: paged      # default: memory
  cache_size: 2048

قِس قبل تفعيل هذا الخيار. يوفّر التقسيم إلى صفحات نحو 28% في عناصر الرؤية و51% في النصوص، وليس عشرة أضعاف، ويجعل تحميل مجموعة البيانات أبطأ بنحو 2.5 مرة. الملف ذاكرة تخزين مؤقت يُعاد بناؤها من ملفات البيانات عند كل إقلاع، لذا يمكن حذفه بأمان.

في المشاريع الكبيرة، الأدوات الفعالة هي تحديد سقف لما يُعيَّن لأي معلّق واحد باستخدام per_annotator_quota، وتقسيم مجموعة البيانات بين الأفواج باستخدام batch_assignment، وإضافة ملفات البيانات بمرور الوقت باستخدام data_directory بدلاً من تحميل كل شيء عند بدء التشغيل.

استراتيجية النسخ الاحتياطي

يجب أن يشمل النسخ الاحتياطي أكثر من مجلد مخرجات التعليقات. فمجلد المهمة يحتوي أيضاً على ما يصل إلى ثلاث قواعد بيانات SQLite، ولا يمكن إعادة بناء أي منها من ملفات البيانات: project.sqlite (المذكرات، ودليل الترميز، وسير عمل المراجعة)، وdatasets.sqlite، وengine.sqlite عند استخدام محرّك الوسم. ويخزّن Potato الحسابات التي أُنشئت عبر نموذج التسجيل في user_config.json. تعمل قواعد البيانات في وضع WAL أثناء كتابة الخادم فيها، لذا قد ينتج tar أو cp لملف قاعدة بيانات نسخة تالفة أو ينقصها عمل حديث، ولا يحذّرك أي من الأمرين.

النسخ الاحتياطي المدمج

منذ الإصدار 2.10، تنسخ كتلة backup في Potato مخرجات التعليقات ولقطات .backup من قواعد البيانات إلى خارج الخادم، وتستعيدها عندما يبدأ الخادم بقرص فارغ. وتعيد الاستعادة قائمة الحسابات مع التعليقات، ولا تكتب أبداً فوق تعليقات موجودة على القرص. ترسل الكتلة أدناه النسخة إلى حاوية S3 كل خمس دقائق، وهو الفاصل الافتراضي، فلا يكلّف فقدان القرص المعلّقين أكثر من خمس دقائق من العمل:

yaml
backup:
  schedule_minutes: 5
  restore_on_boot: true
  sinks:
    - type: s3
      bucket: lab-potato-backups
      prefix: pilot-study
      endpoint_url: https://s3.example.edu   # leave out for AWS S3

يوجّه endpoint_url الوجهة إلى أي خدمة تتحدث واجهة S3 البرمجية، مثل مخزن كائنات جامعي أو MinIO أو Cloudflare R2 أو Backblaze B2. تأتي بيانات الاعتماد من POTATO_S3_ACCESS_KEY_ID وPOTATO_S3_SECRET_ACCESS_KEY في بيئة الخادم، أو من سلسلة boto3 المعتادة. والمفتاح المقصور على تلك الحاوية وحدها يحدّ مما يستطيع الخادم الوصول إليه. أما الوجهة type: huggingface مع repo_id فترسل النسخة إلى مجموعة بيانات خاصة على Hugging Face بدلاً من ذلك، باستخدام HF_TOKEN. وتحتاج الوجهتان كلتاهما إلى الإضافة hosting، pip install 'potato-annotation[hosting]'.

نسخة تبقى على تخزينك الخاص

عندما تُلزم اتفاقية استخدام البيانات بإبقاء البيانات على الخادم، يمكن لمهمة ليلية أن تنسخها إلى تخزين محلي أو مركَّب بدلاً من ذلك. يؤرشف السكربت أدناه مجلد المخرجات، ويأخذ لقطة من كل قاعدة بيانات باستخدام أمر .backup في SQLite بدلاً من نسخ الملف، ويحتفظ بنسخ سبعة أيام. ويحتاج إلى أداة سطر الأوامر sqlite3:

bash
#!/bin/bash
# backup.sh
set -e
TASK_DIR=/var/potato
DEST=/var/backups/potato/$(date +%Y%m%d_%H%M%S)
mkdir -p "$DEST"
 
tar -czf "$DEST/annotations.tar.gz" -C "$TASK_DIR" annotations
for db in project.sqlite datasets.sqlite engine.sqlite; do
  if [ -f "$TASK_DIR/$db" ]; then
    sqlite3 "$TASK_DIR/$db" ".backup '$DEST/$db'"
  fi
done
cp "$TASK_DIR/config.yaml" "$DEST/"
if [ -f "$TASK_DIR/user_config.json" ]; then cp "$TASK_DIR/user_config.json" "$DEST/"; fi
 
find /var/backups/potato -mindepth 1 -maxdepth 1 -mtime +7 -exec rm -rf {} +

شغّله من cron في الساعة 2 صباحاً كل ليلة:

bash
0 2 * * * /var/potato/backup.sh

قد تفقد النسخة الليلية ما يصل إلى يوم من العمل، لذا شغّلها أكثر من ذلك أثناء دراسة فيها معلّقون كثيرون. وقبل أن تبدأ الدراسة، استعد نسخة واحدة على خادم اختبار وتحقق من وجود آخر تعليق فيها.

قائمة التحقق الأمنية

  • أنهِ TLS عند الوكيل بشهادة صالحة
  • اضبط secret_key أو POTATO_SECRET_KEY لكي تبقى الجلسات بعد إعادة التشغيل
  • اضبط admin_api_key بدلاً من الاعتماد على الملف المولَّد
  • قيّد التسجيل باستخدام user_config.users
  • أبقِ debug معطلاً، لأن وضع التصحيح يتيح لطلبات الوكيل العكسي الوصول إلى صفحات المسؤول دون مفتاح
  • شغّل gunicorn بعامل واحد
  • احفظ بيانات الاعتماد في متغيرات البيئة، لا في ملف الإعداد
  • فعّل POTATO_PROXY_FIX فقط خلف وكيل موثوق
  • انسخ مجلد المخرجات وملف الحسابات وقواعد البيانات احتياطياً، واختبر الاستعادة
  • راجع من يملك دور المسؤول

متغيرات البيئة

bash
# .env file
POTATO_SECRET_KEY=your-super-secret-key
POTATO_ADMIN_API_KEY=your-admin-key
POTATO_DB_CONNECTION=mysql://potato:password@localhost:3306/potato_db

ولّد مفتاح توقيع باستخدام:

bash
python3 -c 'import secrets; print(secrets.token_hex(32))'

التحميل في Docker Compose:

yaml
services:
  potato:
    env_file:
      - .env

استكشاف الأخطاء وإصلاحها

فحص السجلات

bash
# Gunicorn logs
journalctl -u potato -f
 
# Docker logs
docker-compose logs -f potato

المشاكل الشائعة

502 Bad Gateway: تحقق من أن Potato قيد التشغيل ومرتبط بالمنفذ الذي يمرر إليه الوكيل الطلبات.

تسجيل خروج الجميع بعد النشر: لم يُضبط secret_key، لذا وُلِّد مفتاح توقيع جديد عند بدء التشغيل.

تعليقات مفقودة: تحقق من عدد العمال. تشغيل أكثر من عامل gunicorn واحد يمحو التعليقات دون الإبلاغ عن ذلك.

أخطاء 404 للأصول تحت بادئة مسار: اضبط POTATO_PROXY_FIX أو POTATO_URL_PREFIX، وتأكد من أن الوكيل يحذف البادئة قبل تمرير الطلب.

بطء مع مجموعة بيانات كبيرة: أضف ذاكرة قبل إضافة خوادم، وفكّر في item_store.backend: paged بعد نحو مليون عنصر.

الخطوات التالية

للاطلاع على تفاصيل التنفيذ، راجع دليل الوكيل العكسي ودليل التوسع.