venv, pipx או uv בשרת Ubuntu: מה לבחור?
נתקלתם בשגיאת externally-managed-environment ב-Ubuntu? המדריך מסביר מתי להשתמש ב-venv, pipx או uv כדי לנהל חבילות Python מבלי לשבור את תלויות המערכת שלכם בשרת.
מדוע pip install נכשל בשרת 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. בחירת ה-web framework אינה משנה דבר כאן: Django ו-Flask על גבי VPS נבדלים במה שמגיע ל-requirements.txt, לא בדרך שבה נבנית הסביבה סביבם. כל מה שמתואר להלן משתמש ב-Ubuntu 24.04 וב-Python 3.12 שלה, לכן התאימו את הגרסה בתוך הנתיבים אם הגרסה שלכם שונה.
בניית venv לכל יישום
הפצת Ubuntu מפרידה את המודול venv מחבילת ה-Python הבסיסית. לכן, בתמונה (image) מינימלית, הניסיון הראשון ייכשל עם הודעה שמציינת בדיוק מה חסר.
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 מתקינה לתוך הסביבה הזו בגלל המיקום של הקובץ הבינארי, ולא בגלל משהו שייצאתם (export) ל-shell. ודאו זאת לפני שתמשיכו.
/srv/myapp/.venv/bin/python -c 'import sys; print(sys.prefix)'הפקודה תדפיס /srv/myapp/.venv. אם היא מדפיסה /usr, אתם מריצים את ה-interpreter של המערכת והחבילות שלכם הותקנו במקום שלא התכוונתם אליו.
שתי תכונות של venv קובעות מה תוכלו לעשות איתו בהמשך. venv אינו ניתן להעברה (relocatable), כיוון שכל סקריפט ב-bin/ נושא שורת shebang מוחלטת: head -1 /srv/myapp/.venv/bin/pip קורא את #!/srv/myapp/.venv/bin/python. אם תשנו את שם תיקיית האב, הסקריפטים הללו ייכשלו עם bad interpreter: No such file or directory. בנוסף, venv מקבע את ה-interpreter שיצר אותו, כפי שמתועד בשורת home בתוך /srv/myapp/.venv/pyvenv.cfg, והקובץ bin/python3 הוא קישור סימבולי (symlink) לאותו קובץ בינארי. אם תשדרגו את גרסת ההפצה כך ש-python3.12 ייעלם, הקישור הסימבולי יישאר ללא יעד, והשירות יקרוס בעת העלייה עם No such file or directory. בשני המקרים הפתרון זהה: מחקו את ה-venv ובנו אחד חדש מ-requirements.txt. הבנייה מחדש אורכת שניות. לעולם אל תעתיקו venv בין מכונות.
מיקום ה-venv והבעלות עליו
מקמו את ה-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 שלכם. היא אינה יכולה לשנות את ה-shell שבו אתם נמצאים כרגע, לכן http: command not found מיד לאחר ההתקנה בדרך כלל מעיד על כך שטרם התנתקתם ונכנסתם מחדש למערכת. ה-~/.profile המוגדר כברירת מחדל ב-Ubuntu מוסיף את ~/.local/bin רק כאשר הספרייה הזו כבר קיימת בעת הכניסה למערכת; זו הסיבה שהבעיה הזו מופיעה פעם אחת בחשבון חדש ולא חוזרת על עצמה.
אם תפנו את pipx לספרייה, הוא יסרב ויציג הודעה שמתחילה כך:
No apps associated with package requests or its dependencies.זהו הכלי המציין בפניכם כי מדובר בכלי הלא מתאים למשימה. ספריות שייכות ל-venv של יישום.
הפרט הקריטי בשרת הוא המיקום. פקודת pipx install רגילה מציבה את כל הקבצים תחת ספריית הבית של משתמש בודד. יחידת 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 קטן, והוא יוצר קובץ lockfile אמיתי.
מתקין התוכנה הרשמי מציב את uv ואת uvx ב-~/.local/bin:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --versionהזרמת סקריפט ישירות ל-shell בשרת דורשת רגע של תשומת לב. קבעו את הגרסה ב-URL וקראו את תוכן הקובץ לפני ההרצה:
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-devuv lock כותב את uv.lock, קובץ lockfile חוצה-פלטפורמות המכיל גרסאות מדויקות שנפתרו, ויש לבצע לו commit לצד הקוד. uv sync בונה את .venv בתיקיית השורש של הפרויקט בהתאם לקובץ זה. בשרת, --frozen הוא הדגל הקריטי: התיעוד מגדיר אותו כשימוש בגרסאות שב-lockfile כמקור האמת, במקום בדיקה אם ה-lockfile מעודכן – התנהגות רצויה עבור פריסה (deployment). --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 במקום להיכשל. זה נוח במחשב נייד אך מפתיע בשרת, כיוון שהשירות שלכם תלוי כעת במתורגמן שנמצא בתיקיית הבית וש-apt upgrade לעולם לא יעדכן. הגדירו את python-preference ל-only-system ב-uv.toml אם ברצונכם להשתמש במתורגמן של ההפצה. אם ברצונכם שהסביבה תהיה במיקום אחר מאשר תיקיית השורש של הפרויקט, 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:appmyapp.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/EXECExecStart אינה שורת פקודה של shell. systemd מריץ תוכנית ישירות, לכן אין פקודה מובנית של source, && מועבר כארגומנט מילולי, ושום דבר לא מתרחב.
ויחידה זו מתחילה, ואז קורסת:
[Service]
ExecStart=/usr/bin/python3 /srv/myapp/app.pyModuleNotFoundError: No module named 'flask'/usr/bin/python3 הוא מפרש המערכת, וה-sys.path שלו מעולם לא הכיל את ה-venv שלכם. אותה פקודה עובדת ב-session של ה-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.targetsudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -n 50 --no-pagersystemctl status myapp אמור לדווח על active (running) עם Main PID שהוא תהליך ה-gunicorn שלכם. לכל דבר אחר, קראו את ה-journal.
השורה Environment=PATH= אינה שם עבור ExecStart, שכבר נושא נתיב מלא. היא שם עבור התהליכים שהיישום שלכם מפעיל. שירות יורש PATH ברירת מחדל קצר מ-systemd, לכן קוד Python שקורא ל-subprocess.run(["ffmpeg", ...]), או פקודת ניהול שמפעילה סקריפט קונסולה מתוך ה-venv, לא ימצאו את מה שהם צריכים. הצבת ספריית ה-bin של ה-venv במקום הראשון היא החלק היחיד ב-activate ששירות באמת משתמש בו. בדקו מה היחידה קיבלה בפועל עם systemctl show -p Environment myapp.
אותו כלל חל על עבודה מתוזמנת. cron מריץ משימות עם PATH של /usr/bin:/bin, לכן שורת crontab שקוראת python3 /srv/myapp/cleanup.py מריצה את מפרש המערכת ונכשלת עם ModuleNotFoundError בשלוש לפנות בוקר, והשגיאה נשלחת ל-mail spool מקומי שאף אחד לא קורא. כתבו גם שם את הנתיב המוחלט ל-venv. כדי לקבל את הפלט הזה ב-journal ותיעוד של ההרצה האחרונה במקום זאת, זוג שירות וטיימר של 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 לא מקובע תפתור גרסאות שונות בחודש הבא, מה שאומר שתג התמונה ניתן לשחזור בעוד שהבנייה שיצרה אותו אינה כזו. קובץ נעילה (lockfile) כמו uv.lock, או קובץ requirements מקובע לחלוטין, הוא מה שסוגר את הפער הזה, עם מכולה או בלעדיה. וכאשר יישום אחד רץ על 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 image) שאתה בונה מחדש מאפס בכל פעם, הנזק נעצר באותה תמונה, ולכן זה קביל שם. במכונה שאתה מתחזק, צור venv. זו פקודה אחת בלבד.
היכן צריכה סביבת ה-virtual environment להישאר בשרת?
בתוך ספריית היישום עצמו, כ-/srv/myapp/.venv, בבעלות משתמש פריסה (deploy user), כאשר חשבון השירות מחזיק בהרשאות קריאה והרצה בלבד. החזק venv אחד לכל יישום, כיוון שסביבה משותפת משמעה ששדרוג עבור יישום אחד עלול לשבור את השני. אל תעביר או תעתיק venv לאחר יצירתו: כל סקריפט בספריית ה-bin/ שלו מכיל את הנתיב המוחלט הזה בשורת ה-shebang שלו, ולכן venv שהועבר ייכשל עם bad interpreter: No such file or directory. מחק אותו ובנה מחדש מתוך requirements.txt במקום זאת.
מדוע שירות ה-systemd שלי נכשל עם ModuleNotFoundError?
מכיוון שהיחידה מריצה מפרש (interpreter) שאינו זה של ה-venv. הרץ את systemctl cat myapp וקרא את ExecStart. עליו לציין את /srv/myapp/.venv/bin/python, או סקריפט קונסולה מאותה ספריית bin/, באמצעות נתיב מוחלט. הרצת 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 שההפצה שלך אינה מספקת. הוא יוצר venv רגיל, כך שיחידת ה-systemd ומבנה הקבצים אינם משתנים, ו-uv sync --frozen מתקין בדיוק את מה שקובץ הנעילה מתעד. אם יישום בודד נפרס מ-git עם requirements.txt נעול וההתקנה מסתיימת בשניות, python3 -m venv מספיק בהחלט, וזהו קובץ בינארי אחד פחות שצריך לעדכן בשרת.