Python venv, pipx чи uv на сервері Ubuntu
Ubuntu 24.04 повертає externally-managed-environment для pip install. Дізнайтеся, коли обрати venv, pipx або uv і як підключити середовище до systemd.
Чому 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 enhancement proposal 668, «externally managed environments») працює за призначенням. 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 встановлює пакети саме в це середовище через розташування бінарного файла, а не через змінні, експортовані в shell. Перевірте це, перш ніж продовжувати.
/srv/myapp/.venv/bin/python -c 'import sys; print(sys.prefix)'Команда виведе /srv/myapp/.venv. Якщо вона виведе /usr, ви використовуєте системний інтерпретатор, а пакети встановлено не туди, куди потрібно.
Подальше використання venv визначають дві його властивості. Venv не можна переносити, оскільки кожен скрипт у bin/ містить абсолютний рядок shebang: head -1 /srv/myapp/.venv/bin/pip читає #!/srv/myapp/.venv/bin/python. Якщо перейменувати батьківський каталог, ці скрипти завершаться помилкою bad interpreter: No such file or directory. Venv також фіксує інтерпретатор, за допомогою якого його створено. Цей шлях записано в рядку home файла /srv/myapp/.venv/pyvenv.cfg, а bin/python3 є symlink на цей бінарний файл. Якщо оновити реліз і python3.12 зникне, symlink не матиме цілі, а сервіс завершиться під час запуску з помилкою No such file or directory. В обох випадках рішення однакове: видаліть venv і створіть його заново з requirements.txt. Повторне створення триває кілька секунд. Ніколи не копіюйте venv між машинами.
Де розмістити venv і кому його призначити
Розмістіть його поруч із кодом у /srv/myapp/.venv і використовуйте окремий venv для кожного застосунку. У результаті розгортання міститься в одному каталозі, systemd unit отримує незмінний шлях, а два застосунки не зможуть вплинути один на одного через спільне оновлення залежності. Не розміщуйте 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 httpiepipx ensurepath додає ~/.local/bin до PATH, редагуючи файл запуску оболонки. Він не може змінити оболонку, у якій ви вже працюєте, тому http: command not found одразу після встановлення зазвичай означає, що ви ще не вийшли із системи та не ввійшли знову. Стандартний ~/.profile в Ubuntu додає ~/.local/bin лише тоді, коли цей каталог уже існує під час входу в систему. Тому на новому обліковому записі ця проблема виникає один раз і більше не повторюється.
Якщо передати бібліотеку до pipx, він відмовиться та виведе повідомлення, яке починається так:
No apps associated with package requests or its dependencies.Це означає, що для цього завдання вибрано неправильний інструмент. Бібліотеки потрібно встановлювати у venv застосунку.
На сервері важливим є розташування. Звичайна команда pipx install розміщує все в домашньому каталозі одного користувача. Unit 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 ansiblecommand -v ansible має вивести /usr/local/bin/ansible. Якщо виведено шлях у /home, інструмент встановлено в обліковому записі одного користувача, і жоден сервіс його не знайде.
uv, якщо вам потрібен lockfile
uv — це один бінарний файл від Astral, який охоплює можливості pip, venv і pip-tools, а також уміє завантажувати інтерпретатори. Він достатньо швидкий, щоб різниця була помітною на невеликому VPS, і створює справжній lockfile.
Офіційний інсталятор розміщує uv і uvx у ~/.local/bin:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --versionПередавання скрипту через pipe до shell на сервері потребує обережності. Зафіксуйте версію в URL і перегляньте файл перед запуском:
curl -LsSf https://astral.sh/uv/0.12.3/install.sh -o uv-install.sh
less uv-install.sh
sh uv-install.shpipx 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 з точними розв’язаними версіями, який ви комітіть поруч із кодом. uv sync створює .venv у корені проєкту відповідно до нього. На сервері важливим є прапорець --frozen: документація визначає його як використання версій із lockfile як джерела істини замість перевірки актуальності lockfile. Саме така поведінка потрібна під час розгортання. --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У результаті створюється звичайне virtual environment. .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 визначає каталог для virtual environment проєкту.
Спрямуйте systemd до інтерпретатора venv, а не до activate
Саме тут найчастіше виникають проблеми з розгортанням Python. Причина — неправильне розуміння того, що робить activate.
bin/activate — це shell-скрипт. Він додає каталог bin venv на початок PATH, установлює VIRTUAL_ENV, зберігає попередні значення, щоб deactivate міг їх відновити, і змінює prompt. Інтерпретатор не читає цей скрипт. Активація лише спрощує роботу людини, яка вводить python у prompt.
Середовище фактично вибирає файл інтерпретатора, який ви запускаєте. Коли запускається /srv/myapp/.venv/bin/python, модуль Python site шукає файл pyvenv.cfg у каталозі, що містить виконуваний файл, і на один рівень вище. Якщо знайдено /srv/myapp/.venv/pyvenv.cfg, значення sys.prefix встановлюється як шлях до venv. У результаті каталог site-packages цього venv додається до sys.path. Це весь механізм. Змінні середовища та shell не потрібні.
Тому цей unit не запускається:
[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 запускає програму безпосередньо, тому builtin source не існує, && передається як буквальний аргумент, і нічого не розгортається.
А цей unit запускається, а потім завершується:
[Service]
ExecStart=/usr/bin/python3 /srv/myapp/app.pyModuleNotFoundError: No module named 'flask'/usr/bin/python3 — це системний інтерпретатор, а в його sys.path ніколи не було вашого venv. У SSH-сеансі та сама команда працює лише тому, що там ви активували venv. Отже, shell через PATH знайшов python3 і фактично запустив .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. Якщо результат інший, прочитайте журнал.
Рядок Environment=PATH= потрібен не для ExecStart, оскільки він уже містить повний шлях. Він потрібен для процесів, які запускає ваш застосунок. Сервіс успадковує від systemd скорочений типовий PATH. Тому код Python, який викликає subprocess.run(["ffmpeg", ...]), або команда керування, що запускає console script із venv, не знайде потрібний файл. Додавання каталогу bin venv на початок — єдина частина activate, яку сервіс справді використовує. Перевірте, що саме отримав unit, за допомогою systemctl show -p Environment myapp.
Це саме правило стосується запланованих завдань. cron запускає завдання з PATH, що має значення /usr/bin:/bin. Тому рядок crontab із python3 /srv/myapp/cleanup.py запускає системний інтерпретатор і о третій годині ночі завершується помилкою ModuleNotFoundError. Помилка потрапляє до локальної поштової черги, яку ніхто не переглядає. Укажіть там абсолютний шлях до venv. Щоб натомість отримувати цей вивід у журналі та зберігати відомості про останній запуск, пара systemd service і timer використовує той самий рядок 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. Етап builder встановлює пакети в /opt/venv, а етап runtime копіює лише цей каталог і не переносить компілятори. Проблема з activate переходить разом із ним. Рядок RUN source /opt/venv/bin/activate впливає лише на shell цього build layer, тому під час запуску контейнер використовує системний інтерпретатор і видає помилку ModuleNotFoundError. Установіть ENV PATH="/opt/venv/bin:$PATH" або передайте CMD абсолютний шлях /opt/venv/bin/gunicorn. Це та сама помилка, що й у systemd, але в іншому файлі.
Отже, контейнер замінює питання вибору інтерпретатора, оскільки образ фіксує інтерпретатор і все, що розташоване під ним. Але він не замінює питання фіксації залежностей. Образ, зібраний із непофіксованого requirements.txt, наступного місяця може отримати інші версії. Це означає, що тег образу є відтворюваним, а збірка, яка його створила, — ні. Lockfile на кшталт uv.lock або файл requirements із повністю зафіксованими версіями усуває цю різницю — незалежно від використання контейнера. Якщо один застосунок працює на одному VPS під systemd, контейнер здебільшого лише переносить це саме рішення у Dockerfile, оскільки systemd і так перезапускає процес після помилки та записує його вивід у журнал. Запуск Docker на VPS виправданий, коли ви хочете розгортати саме зібраний образ.
FAQ
Чи можна просто виконати pip install з --break-system-packages?
Ні, якщо сервер має працювати безперервно. Прапорець робить саме те, що випливає з його назви: вимикає захист, і pip записує пакети до /usr/local/lib/python3.12/dist-packages, який має вищий пріоритет, ніж каталог apt у sys.path. Тоді ваша версія затіняє версію дистрибутива для кожного системного скрипту, що запускається під /usr/bin/python3. Водночас apt і далі вважає встановленою власну версію. Тому конфлікт не виявляється, доки щось не перестане працювати. В образі контейнера, який щоразу збирається з нуля, наслідки обмежені цим образом, тому такий підхід там допустимий. На сервері, який ви обслуговуєте, створіть venv. Для цього достатньо однієї команди.
Де має зберігатися віртуальне середовище на сервері?
У власному каталозі застосунку, наприклад у /srv/myapp/.venv. Власником має бути користувач deploy, а обліковий запис сервісу повинен мати лише права на читання та виконання. Створюйте окремий venv для кожного застосунку. У спільному середовищі оновлення першого застосунку може зламати другий. Не переміщуйте й не копіюйте venv після створення. У кожному скрипті в його каталозі bin/ абсолютний шлях записано в рядку shebang, тому після переміщення venv виникає помилка bad interpreter: No such file or directory. Видаліть середовище та створіть його заново з requirements.txt.
Чому мій сервіс systemd завершується з помилкою ModuleNotFoundError?
Тому що unit запускає інтерпретатор, який не належить venv. Виконайте systemctl cat myapp і перегляньте ExecStart. Там має бути вказано /srv/myapp/.venv/bin/python або console script із того самого каталогу bin/, причому абсолютним шляхом. Додавання activate до файла unit не допоможе, оскільки ExecStart не є оболонкою, а 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, тому unit systemd та структура файлів не змінюються, а uv sync --frozen встановлює саме те, що записано в lockfile. Якщо один застосунок розгортається з git із зафіксованим requirements.txt, а встановлення триває кілька секунд, python3 -m venv уже достатньо. Це також один додатковий бінарний файл, який не потрібно оновлювати на сервері.