SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor

الفرق بين venv وpipx وuv على خادم Ubuntu

هل يظهر لك خطأ externally-managed-environment عند استخدام pip؟ اكتشف متى تختار venv أو pipx أو uv لتثبيت حزم بايثون على خادمك وكيفية ربطها بخدمات systemd بشكل صحيح.

لماذا يفشل تثبيت الحزم عبر pip على خادم Ubuntu جديد

يعتمد الاختيار بين Python venv أو pipx أو uv على خادمك على سؤال واحد: ماذا تثبّت؟ تنتمي تبعيات التطبيق إلى بيئة افتراضية داخل دليل التطبيق نفسه. أما أدوات سطر الأوامر التي ترغب في استدعائها باسمها فتنتمي إلى pipx. يقوم uv بكلا المهمتين ويضيف ملف قفل (lockfile)، وهو أمر تزداد أهميته بمجرد أن يضطر جهاز ثانٍ إلى بناء نفس البيئة. ما لا يفعله أي منهم هو التثبيت في نسخة Python الخاصة بالنظام، لأن خادم Ubuntu الحالي يرفض ذلك تماماً.

شغّل sudo pip install requests على Ubuntu 24.04 وسيتوقف pip قبل أن يحمّل ملفاً واحداً.

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip.

    If you wish to install a non-Debian packaged Python application,
    it may be easiest to use pipx install xyz, which will manage a
    virtual environment for you.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this behaviour by passing --break-system-packages.

هذا هو PEP 668 (مقترح تحسين Python رقم 668، "البيئات المُدارة خارجياً") وهو يؤدي وظيفته. تضع توزيعتا Debian وUbuntu ملف علامة بجانب المفسّر في /usr/lib/python3.12/EXTERNALLY-MANAGED، ويرفض pip الكتابة في أي مفسّر يحمل هذا الملف.

توجد هذه القاعدة بسبب ترتيب sys.path. يقوم apt بتثبيت المكتبات في /usr/lib/python3/dist-packages. بينما يكتب pip، عند تشغيله بصلاحيات root مقابل مفسّر النظام، في /usr/local/lib/python3.12/dist-packages، وتضع حزم Debian هذا الدليل في مرتبة متقدمة في مسار البحث. اطبع المسار بنفسك باستخدام python3 -c 'import sys; print(sys.path)' واقرأ الترتيب. لذا، فإن النسخة التي كتبها pip تحجب النسخة التي ثبّتها apt، وذلك لكل برنامج على الخادم يعمل تحت /usr/bin/python3، بما في ذلك أدوات التوزيعة نفسها. يستورد cloud-init كلاً من requests وjinja2 وPyYAML من ذلك المفسّر. إذا قمت بترقية إحدى هذه الحزم عبر pip ووصلت إلى إصدار غير متوافق، سيفشل شيء لم تلمسه أبداً عند الإقلاع التالي مع ظهور تتبع خطأ (traceback) يشير إلى حزمة لم تكن تعلم بوجودها في السلسلة. لا يزال apt يسجل نسخته الخاصة على أنها مثبتة، لذا لن يحذرك أي شيء، ويكون الإصلاح هو sudo apt reinstall python3-requests.

القاعدة التي تلي ذلك قصيرة. نسخة Python الخاصة بالنظام ملك للتوزيعة. لا تثبّت فيها، ولا ترقِّ مكتباتها باستخدام pip، ولا تحذف ملف EXTERNALLY-MANAGED لجعل الرسالة تختفي. المهمة الوحيدة التي يجب أن توكلها إلى /usr/bin/python3 هي بناء البيئات الافتراضية.

venv مقابل pipx مقابل uv: قاعدة اتخاذ القرار

اختر الأداة بناءً على ما تقوم بتثبيته، وليس بناءً على الأداة التي قرأت عنها مؤخراً.

  • تطبيق تقوم بنشره وتشغيله كخدمة، مثل مشروع Django أو Flask: استخدم بيئة افتراضية واحدة (venv) داخل دليل ذلك التطبيق.
  • أداة سطر أوامر تريدها في PATH الخاص بك، مثل ansible أو httpie: استخدم pipx، الذي يمنح كل أداة بيئة خاصة ورابطاً واحداً في PATH.
  • مشروع يحتاج إلى ملف قفل (lockfile)، أو تثبيت أسرع، أو إصدار Python لا توفره التوزيعة: استخدم uv، الذي ينشئ بيئة venv عادية بالإضافة إلى ملف uv.lock.
  • مكتبة تحتاجها أداة تابعة للتوزيعة بدلاً من كودك الخاص: استخدم sudo apt install python3-<name>، وهي الطريقة الوحيدة المدعومة لإضافة أي شيء إلى مفسر النظام.

يقوم كل من pipx و uv tool install بنفس المهمة، لذا فإن الخادم الذي يحتوي بالفعل على uv لا يحتاج إلى pipx أيضاً. إطار عمل الويب الذي اخترته لا يغير شيئاً هنا: Django و Flask على خادم VPS يختلفان في ما يتم وضعه في requirements.txt، وليس في كيفية بناء البيئة المحيطة به. كل ما يلي يستخدم Ubuntu 24.04 وإصدار Python 3.12 الخاص بها، لذا قم بتعديل الإصدار داخل المسارات إذا كان إصدارك مختلفاً.

بناء بيئة افتراضية (venv) لكل تطبيق

يفصل نظام Ubuntu وحدة venv عن حزمة Python الأساسية، لذا ستفشل المحاولة الأولى على صورة نظام مصغرة مع ظهور رسالة تحدد بدقة ما هو مفقود.

The virtual environment was not created successfully because ensurepip is not
available.  On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.

    apt install python3.12-venv

ثبّت الوحدة، ثم أنشئ البيئة بصفتك المستخدم الذي سيمتلك الكود.

sudo apt update
sudo apt install -y python3-venv
sudo install -d -o deploy -g deploy -m 755 /srv/myapp
sudo -u deploy python3 -m venv /srv/myapp/.venv
sudo -u deploy /srv/myapp/.venv/bin/pip install -r /srv/myapp/requirements.txt

لاحظ ما هو غير موجود: لا يوجد source، ولا يوجد activate. يقوم /srv/myapp/.venv/bin/pip بالتثبيت داخل تلك البيئة بسبب موقع الملف الثنائي (binary)، وليس بسبب أي شيء قمت بتصديره إلى الصدفة (shell). تأكد من ذلك قبل المتابعة.

/srv/myapp/.venv/bin/python -c 'import sys; print(sys.prefix)'

يطبع هذا الأمر /srv/myapp/.venv. إذا طبع /usr، فأنت تستخدم مفسّر النظام (system interpreter) وقد تم تثبيت حزمك في مكان لم تقصده.

هناك خاصيتان للبيئة الافتراضية (venv) تحددان ما يمكنك فعله بها لاحقاً. البيئة الافتراضية غير قابلة للنقل، لأن كل سكربت في bin/ يحمل سطر shebang مطلقاً: يقرأ head -1 /srv/myapp/.venv/bin/pip المسار #!/srv/myapp/.venv/bin/python. إذا أعدت تسمية المجلد الأب، ستفشل تلك السكربتات مع ظهور bad interpreter: No such file or directory. كما تثبّت البيئة الافتراضية المفسّر الذي أنشأها، وهو مسجّل كسطر home في /srv/myapp/.venv/pyvenv.cfg، ويعد bin/python3 رابطاً رمزياً (symlink) لذلك الملف الثنائي. إذا قمت بترقية إصدار النظام بحيث يختفي python3.12، فلن يجد الرابط الرمزي هدفاً له، وستتوقف الخدمة عند البدء مع ظهور No such file or directory. كلتا الحالتين لهما نفس الحل: احذف البيئة الافتراضية وابنِ واحدة جديدة من requirements.txt. تستغرق عملية إعادة البناء ثوانٍ معدودة. لا تقم أبداً بنسخ بيئة افتراضية بين الأجهزة.

مكان وجود venv ومن يمتلكه

ضعه بجانب الكود في /srv/myapp/.venv، واحتفظ ببيئة افتراضية (venv) واحدة لكل تطبيق. يصبح النشر بذلك في دليل واحد، وتحصل وحدة systemd على مسار لا يتغير أبداً، ولا يمكن لتطبيقين أن يعطلا بعضهما البعض من خلال ترقية تبعية مشتركة. لا تضع venv في أي مكان ينشر فيه خادم الويب الملفات مباشرة، لأنه يحتوي على تبعياتك وغالباً على إعداداتك.

تستحق الملكية نصف دقيقة من وقتك. اجعل مستخدماً بـ deploy يمتلك الكود والبيئة، وامنح حساب الخدمة صلاحيات القراءة والتنفيذ فقط.

sudo adduser --system --group --no-create-home myapp
sudo chown -R deploy:myapp /srv/myapp
sudo chmod -R o-rwx /srv/myapp

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

استخدام pipx لأدوات سطر الأوامر

تُثبّت pipx التطبيقات، لا المكتبات. تحصل كل أداة على بيئتها الخاصة ضمن ~/.local/share/pipx/venvs/<name>، وتُربط ملفات الأداة التنفيذية في ~/.local/bin، لذا لا يحدث تعارض أبداً بين أداتين تحتاجان إلى إصدارات مختلفة من نفس المكتبة.

sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install httpie

يضيف pipx ensurepath المسار ~/.local/bin إلى PATH عبر تعديل ملف بدء تشغيل الصدفة (shell). لا يمكنه تغيير الصدفة التي تعمل بها حالياً، لذا فإن ظهور http: command not found مباشرة بعد التثبيت يعني عادةً أنك لم تسجّل الخروج وتعُد للدخول بعد. يضيف ملف ~/.profile الافتراضي في Ubuntu المسار ~/.local/bin فقط عندما يكون هذا المجلد موجوداً بالفعل عند تسجيل الدخول، وهذا هو سبب حدوث هذه المشكلة مرة واحدة عند إنشاء حساب جديد وعدم تكرارها.

إذا وجّهت pipx نحو مكتبة، فسيتم رفض الطلب برسالة تبدأ بـ:

No apps associated with package requests or its dependencies.

هذه هي الأداة تخبرك بأنك تستخدم الأداة الخاطئة. تنتمي المكتبات إلى بيئة افتراضية (venv) خاصة بالتطبيق.

التفصيل المهم على الخادم هو الموقع. يضع أمر pipx install العادي كل شيء تحت المجلد الرئيسي (home) لمستخدم واحد. لا يمكن لوحدة systemd تعمل بصلاحيات myapp رؤية ذلك، ولا يمكن لمهمة cron تعمل بصلاحيات root رؤيتها، ولن يجدها sudo أيضاً، لأن secure_path في /etc/sudoers يستبدل PATH بقائمة ثابتة. بالنسبة لأداة يجب أن تتوفر على مستوى الجهاز بالكامل، ثبّتها بشكل عام.

sudo pipx install --global ansible
sudo pipx ensurepath --global

يضع العلم --global البيئات في /opt/pipx ويربط الملفات التنفيذية في /usr/local/bin، وهو مسار موجود ضمن PATH الافتراضي وداخل secure_path. تحقق من إصدارك أولاً باستخدام pipx --version، لأن Ubuntu 24.04 يوفّر pipx 1.4.3، وهو إصدار أقدم من --global، ويستجيب إصدار pipx الأقدم بـ unrecognized arguments: --global. في ذلك الإصدار، حدد المجلدين الموثقين بنفسك:

sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible
command -v ansible

يجب أن يطبع command -v ansible المسار /usr/local/bin/ansible. إذا طبع مساراً تحت /home، فهذا يعني أن الأداة تم تثبيتها في حساب مستخدم واحد ولن تجدها أي خدمة.

uv عندما تحتاج إلى ملف قفل (lockfile)

أداة uv هي ملف تنفيذي واحد من Astral تغطي وظائف pip وvenv وpip-tools، كما يمكنها تحميل مترجمات Python. سرعتها ملحوظة حتى على خادم VPS صغير، وتنشئ ملف قفل حقيقي.

يضع المثبّت الرسمي uv وuvx في ~/.local/bin:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

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

curl -LsSf https://astral.sh/uv/0.12.3/install.sh -o uv-install.sh
less uv-install.sh
sh uv-install.sh

يعمل pipx install uv أيضاً إذا كان pipx مثبتاً مسبقاً. أداة uv عبارة عن ملف تنفيذي مستقل لا يعتمد على Python، لذا فإن نسخه إلى /usr/local/bin طريقة صالحة لمشاركته مع جميع المستخدمين على الخادم.

بالنسبة لمشروع يحتوي على pyproject.toml، يتكون سير العمل من أربعة أوامر، الأخير منها فقط هو ما يتم تشغيله على الخادم.

uv init myapp
uv add flask gunicorn
uv lock
uv sync --frozen --no-dev

ينشئ uv lock ملف uv.lock، وهو ملف قفل متوافق مع مختلف المنصات يحتوي على إصدارات دقيقة ومحلولة، ويجب عليك دمجه (commit) مع الكود الخاص بك. يقوم uv sync ببناء .venv في جذر المشروع ليتطابق معه. على الخادم، --frozen هو العلم (flag) المهم: توثيق الأداة يعرّفه على أنه استخدام الإصدارات الموجودة في ملف القفل كمصدر وحيد للحقيقة بدلاً من التحقق مما إذا كان ملف القفل محدثاً، وهو السلوك المطلوب عند النشر. يستبعد --no-dev مجموعة تبعيات التطوير.

لا يحتاج مشروع requirements.txt الحالي إلى تحويل، لأن uv يفهم لغة pip:

uv venv /srv/myapp/.venv
uv pip install --python /srv/myapp/.venv/bin/python -r /srv/myapp/requirements.txt

النتيجة هي بيئة افتراضية عادية. يتصرف .venv/bin/python تماماً كما لو كان python3 -m venv هو من بناها، لذا لا يتغير أي شيء لاحق في هذا الدليل.

هناك إعداد افتراضي في uv يستحق المعرفة قبل استخدامه على الخادم. إعداد python-preference مضبوط افتراضياً على managed، وهو ما يعني اختيار المترجمات "التي يتم تحميلها وتثبيتها بواسطة uv" بدلاً من المترجمات الموجودة مسبقاً على النظام. لذا، فإن uv venv --python 3.13 على خادم يحتوي فقط على إصدار 3.12 سيقوم بهدوء بجلب إصدار 3.13 إلى ~/.local/share/uv/python بدلاً من الفشل. هذا مريح على الحاسوب المحمول ولكنه مفاجئ على الخادم، لأن خدمتك ستعتمد الآن على مترجم موجود في مجلد المستخدم (home directory) ولن يقوم apt upgrade بتحديثه. اضبط python-preference على only-system في uv.toml إذا كنت تريد استخدام مترجم التوزيعة (distribution). إذا كنت تريد البيئة في مكان آخر غير جذر المشروع، يحدد UV_PROJECT_ENVIRONMENT المجلد المراد استخدامه للبيئة الافتراضية للمشروع.

وجّه systemd إلى مفسّر venv، لا إلى activate

هذا هو الموضع الذي تفشل فيه معظم عمليات نشر Python، والسبب هو سوء فهم لما يفعله activate.

bin/activate هو سكربت shell. يقوم بإضافة دليل bin الخاص بـ venv إلى بداية PATH، ويضبط VIRTUAL_ENV، ويحفظ القيم القديمة ليتمكن deactivate من استعادتها، كما يغيّر محث الأوامر (prompt) الخاص بك. لا يحتوي هذا السكربت على أي شيء يقرؤه المفسّر نفسه. التفعيل (activation) هو مجرد وسيلة مريحة للإنسان الذي يكتب python في محث الأوامر.

ما يحدد البيئة فعلياً هو ملف المفسّر الذي تقوم بتنفيذه. عندما يبدأ /srv/myapp/.venv/bin/python، تبحث وحدة site في Python عن ملف pyvenv.cfg في الدليل الذي يحتوي على الملف التنفيذي وفي المستوى الذي يعلوه مباشرة. العثور على /srv/myapp/.venv/pyvenv.cfg يضبط sys.prefix على venv، مما يضع site-packages الخاص بـ venv في sys.path. هذه هي الآلية بالكامل. لا تحتاج إلى أي متغير بيئة ولا إلى shell.

لذا، هذه الوحدة لا تبدأ أبداً:

[Service]
ExecStart=source /srv/myapp/.venv/bin/activate && gunicorn app:app
myapp.service: Failed to locate executable source: No such file or directory
myapp.service: Failed at step EXEC spawning source: No such file or directory
myapp.service: Main process exited, code=exited, status=203/EXEC

ExecStart ليس سطر أوامر shell. يقوم systemd بتنفيذ البرنامج مباشرة، لذا لا يوجد أمر مدمج source، ويتم تمرير && كوسيط حرفي، ولا يتم توسيع أي شيء.

وهذه الوحدة تبدأ ثم تتوقف:

[Service]
ExecStart=/usr/bin/python3 /srv/myapp/app.py
ModuleNotFoundError: No module named 'flask'

/usr/bin/python3 هو مفسّر النظام، وsys.path الخاص به لم يحتوِ قط على venv الخاص بك. يعمل الأمر نفسه في جلسة SSH الخاصة بك فقط لأنك قمت بتفعيل venv هناك، مما جعل الـ shell يحل python3 عبر PATH إلى .venv/bin/python3 بدلاً من ذلك.

تغليف الأمر بـ /bin/bash -c 'source ... && gunicorn ...' يعمل بالفعل. لكنه يضع shell بين systemd وعمليتك دون أي فائدة، بينما يحل مسار مطلق واحد المشكلة:

[Unit]
Description=myapp web service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp
Environment=PYTHONUNBUFFERED=1
Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/srv/myapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -n 50 --no-pager

يجب أن يبلغ systemctl status myapp عن active (running) مع Main PID يمثل عملية gunicorn الخاصة بك. إذا ظهر أي شيء آخر، اقرأ السجلات (journal).

سطر Environment=PATH= ليس موجوداً من أجل ExecStart، الذي يحمل بالفعل مساراً كاملاً. إنه موجود من أجل العمليات التي يبدأها تطبيقك. ترث الخدمة PATH افتراضياً قصيراً من systemd، لذا فإن كود Python الذي يستدعي subprocess.run(["ffmpeg", ...])، أو أمر إدارة يستدعي سكربت console من venv، لن يجد ما يحتاجه. وضع دليل bin الخاص بـ venv في البداية هو الجزء الوحيد من activate الذي تستخدمه الخدمة فعلياً. تحقق مما استقبلته الوحدة حقاً باستخدام systemctl show -p Environment myapp.

تغطي القاعدة نفسها المهام المجدولة. يقوم cron بتشغيل المهام بـ PATH قيمته /usr/bin:/bin، لذا فإن سطر crontab الذي يقرأ python3 /srv/myapp/cleanup.py يشغّل مفسّر النظام ويفشل مع ModuleNotFoundError في الثالثة صباحاً، ويذهب الخطأ إلى بريد محلي لا يقرؤه أحد. اكتب مسار venv المطلق هناك أيضاً. للحصول على هذا المخرج في السجلات وسجل لآخر تشغيل، زوج خدمة ومؤقت systemd يستخدم نفس سطر ExecStart.

هل يستبدل Docker هذا القرار؟

تمتلك الحاوية نظام ملفات خاصاً بها، لذا يتغير شكل السؤال بدلاً من أن يختفي. في صورة رسمية مثل python:3.12-slim، يكون Python مدمجاً في /usr/local ولا يحمل أي علامة EXTERNALLY-MANAGED، لذا فإن استخدام pip install بصلاحيات root هو الطريقة المقصودة لإضافة الحزم، ولا يضيف venv الكثير في هذه الحالة. بدلاً من ذلك، عند بناء FROM ubuntu:24.04 ستواجه externally-managed-environment مجدداً داخل الصورة، ولنفس السبب الموجود على المضيف: فهو مفسّر التوزيعة الذي يحمل ملف العلامة الخاص بها.

لا تزال العديد من الصور تستخدم venv، لأنّه يجعل عملية البناء متعددة المراحل (multi-stage build) بسيطة. تُثبّت مرحلة البناء الحزم في /opt/venv، وتقوم مرحلة التشغيل بنسخ ذلك المجلد فقط وترك المترجمات خلفها. تنتقل مشكلة التفعيل (activate) معها. يؤثر سطر RUN source /opt/venv/bin/activate على shell تلك الطبقة من البناء فقط، لذا عند التشغيل تبدأ الحاوية باستخدام مفسّر النظام وتُظهر خطأ ModuleNotFoundError. اضبط ENV PATH="/opt/venv/bin:$PATH"، أو امنح CMD المسار المطلق /opt/venv/bin/gunicorn. إنه نفس الخطأ الموجود في systemd، ولكن في ملف مختلف.

إذن، تستبدل الحاوية سؤال المفسّر، لأن الصورة تثبّت نسخة المفسّر وكل ما يتبعه. لكنها لا تستبدل سؤال التثبيت (pinning). فالصورة التي تُبنى من ملف requirements.txt غير مُثبّت الإصدارات ستحصل على إصدارات مختلفة الشهر القادم، مما يعني أن وسم الصورة (tag) قابل للتكرار بينما عملية البناء التي أنتجته ليست كذلك. إن ملف القفل (lockfile) مثل uv.lock، أو ملف المتطلبات المُثبّت بالكامل، هو ما يسد هذه الفجوة، سواء استخدمت حاوية أم لا. وعندما يعمل تطبيق واحد على خادم VPS تحت إدارة systemd، فإن الحاوية تنقل هذا القرار في الغالب إلى ملف Dockerfile، حيث أن systemd يقوم بالفعل بإعادة تشغيل العملية الفاشلة ويلتقط مخرجاتها في السجل (journal). إن تشغيل Docker على خادم VPS يثبت جدواه عندما تريد أن تكون الصورة المبنية نفسها هي الشيء الذي تنشره.

FAQ

هل يمكنني استخدام pip install مع الخيار --break-system-packages؟

لا تفعل ذلك على خادم يجب أن يظل قيد التشغيل. هذا الخيار يقوم بالضبط بما يشير إليه اسمه: فهو يزيل حماية النظام، ويكتب pip الملفات مباشرة في /usr/local/lib/python3.12/dist-packages، وهو مسار يسبق مسار apt في sys.path. ستؤدي نسختك حينها إلى حجب نسخة التوزيعة لكل سكربت نظام يعمل تحت /usr/bin/python3، بينما سيظل apt يعتقد أن نسخته الأصلية هي المثبتة، لذا لن يكتشف أحد هذا التعارض حتى ينهار شيء ما. داخل صورة حاوية (container) تعيد بناءها من الصفر في كل مرة، يقتصر الضرر على تلك الصورة فقط، لذا يمكن تبرير استخدامه هناك. أما على جهاز تديره أنت، فأنشئ venv. الأمر لا يتطلب سوى أمر واحد.

أين يجب أن يوضع البيئة الافتراضية (virtual environment) على الخادم؟

داخل دليل التطبيق نفسه، باسم /srv/myapp/.venv، بحيث يكون مملوكاً لمستخدم النشر (deploy user)، مع منح حساب الخدمة صلاحيات القراءة والتنفيذ فقط. احتفظ ببيئة venv واحدة لكل تطبيق، لأن مشاركة البيئة تعني أن ترقية التطبيق الأول قد تؤدي إلى تعطل التطبيق الثاني. لا تنقل أو تنسخ venv بعد إنشائها: فكل سكربت في دليل bin/ الخاص بها يحتوي على المسار المطلق مكتوباً في سطر الـ shebang، لذا ستفشل venv المنقولة مع خطأ bad interpreter: No such file or directory. احذفها وأعد بناءها من requirements.txt بدلاً من ذلك.

لماذا تفشل خدمة systemd الخاصة بي مع خطأ ModuleNotFoundError؟

لأن وحدة الخدمة (unit) تنفذ مفسراً (interpreter) ليس هو المفسر الخاص بـ venv. نفذ systemctl cat myapp واقرأ ExecStart. يجب أن يشير المسار إلى /srv/myapp/.venv/bin/python، أو إلى سكربت وحدة تحكم من نفس دليل bin/، باستخدام المسار المطلق. لا يمكن تنفيذ source لملف activate داخل ملف الوحدة، لأن ExecStart ليس صدفة (shell)، وسيقوم systemd بالإبلاغ عن Failed to locate executable source مع status=203/EXEC. أضف Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin لضمان أن أي عملية فرعية يبدأها كودك ستجد أدوات venv أيضاً.

هل يجب أن أستخدم uv بدلاً من venv و pip؟

استخدم uv عندما تحتاج إلى ملف قفل (lockfile)، أو عندما يكون وقت التثبيت بطيئاً بما يكفي لإزعاجك، أو عندما تحتاج إلى إصدار Python لا توفره توزيعة نظامك. يقوم uv بإنشاء venv عادية، لذا لن تتغير وحدة systemd ولا هيكلية الملفات، وسيقوم uv sync --frozen بتثبيت ما يسجله ملف القفل بدقة. إذا كان التطبيق يُنشر من git مع ملف requirements.txt مثبت الإصدارات، وكان التثبيت ينتهي في ثوانٍ، فإن python3 -m venv كافٍ تماماً، وهو ملف تنفيذي أقل ستحتاج لتحديثه على الخادم.

#python#venv#pipx#uv#deployment