SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-13

Python venv, pipx или uv: что выбрать для сервера

Ошибка externally-managed-environment блокирует pip install на Ubuntu 24.04. Узнайте, когда использовать 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 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, получите несовместимый релиз, и компонент, который вы не затрагивали, выйдет из строя при следующей загрузке с трассировкой, указывающей на пакет, о существовании которого в цепочке вы не знали. 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 устанавливается в это окружение благодаря расположению бинарного файла, а не из-за каких-либо переменных, экспортированных в оболочку. Проверьте это перед продолжением.

/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 является символической ссылкой на этот исполняемый файл. Обновите дистрибутив так, чтобы python3.12 исчез, и символическая ссылка останется без цели, из-за чего сервис завершится при запуске с ошибкой No such file or directory. В обоих случаях решение одно: удалите venv и создайте новый из requirements.txt. Пересборка занимает секунды. Никогда не копируйте 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 путем редактирования файла инициализации вашей оболочки. Он не может изменить оболочку, в которой вы уже работаете, поэтому 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 для создания lock-файлов

uv — это единый бинарный файл от Astral, который заменяет функциональность pip, venv и pip-tools, а также умеет загружать интерпретаторы Python. Он работает достаточно быстро, чтобы разница была заметна даже на небольшом VPS, и создает полноценный lock-файл.

Официальный установщик помещает uv и uvx в ~/.local/bin:

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

Передача скрипта через pipe в оболочку на сервере требует осторожности. Зафиксируйте версию в 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-dev

uv lock создает uv.lock — кроссплатформенный lock-файл, содержащий точные разрешенные версии зависимостей; его следует добавлять в систему контроля версий вместе с кодом. uv sync создает .venv в корне проекта в соответствии с этим файлом. На сервере важен флаг --frozen: документация определяет его как использование версий из lock-файла в качестве единственного источника истины, без проверки актуальности самого lock-файла, что и требуется при развертывании. --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 виртуального окружения в начало PATH, устанавливает VIRTUAL_ENV, сохраняет старые значения, чтобы deactivate мог их восстановить, и меняет приглашение командной строки. В нем нет ничего, что считывал бы сам интерпретатор. Активация — это удобство для человека, который вводит python в консоли.

На самом деле окружение выбирается тем файлом интерпретатора, который вы запускаете. Когда запускается /srv/myapp/.venv/bin/python, модуль site языка Python ищет файл pyvenv.cfg в директории, где находится исполняемый файл, или уровнем выше. Обнаружение /srv/myapp/.venv/pyvenv.cfg устанавливает sys.prefix на venv, что добавляет site-packages этого окружения в 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. Если там что-то другое, читайте журнал.

Строка 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 в три часа ночи, а сообщение об ошибке уходит в локальную почтовую очередь, которую никто не читает. Указывайте там абсолютный путь к 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). Этап сборки устанавливает зависимости в /opt/venv, а этап выполнения копирует только этот каталог, оставляя компиляторы за бортом. Проблема активации перемещается вместе с ним. Строка RUN source /opt/venv/bin/activate влияет только на оболочку (shell) этого слоя сборки, поэтому при запуске контейнер начинает работу с системным интерпретатором и вызывает ModuleNotFoundError. Установите ENV PATH="/opt/venv/bin:$PATH" или укажите CMD абсолютный путь /opt/venv/bin/gunicorn. Это та же ошибка, что и в systemd, просто в другом файле.

Таким образом, контейнер заменяет вопрос об интерпретаторе, поскольку образ фиксирует версию интерпретатора и всего, что находится под ним. Он не заменяет вопрос о фиксации версий (pinning). Образ, собранный из незафиксированного requirements.txt, в следующем месяце может разрешить другие версии пакетов. Это означает, что тег образа воспроизводим, а процесс сборки, который его создал, — нет. Файл блокировки, такой как uv.lock, или полностью зафиксированный файл требований (requirements file) закрывает этот пробел, независимо от использования контейнеров. А когда одно приложение работает на одном VPS под управлением systemd, контейнер по большей части переносит это решение в Dockerfile, так как systemd и без того перезапускает упавший процесс и записывает его вывод в журнал. Запуск Docker на VPS оправдывает себя, когда вы хотите развертывать именно собранный образ.

FAQ

Можно ли просто использовать pip install с флагом --break-system-packages?

Не на сервере, который должен стабильно работать. Флаг делает ровно то, что заявлено: он снимает защиту, и pip записывает файлы в /usr/local/lib/python3.12/dist-packages, который в sys.path имеет приоритет над директорией apt. Ваша версия перекрывает системную для всех скриптов, работающих от /usr/bin/python3, при этом apt считает, что установлена его версия. Конфликт не будет обнаружен, пока что-то не выйдет из строя. Внутри образа контейнера, который вы каждый раз собираете с нуля, ущерб ограничен этим образом, поэтому там это допустимо. На машине, которую вы обслуживаете, создайте venv. Это одна команда.

Где на сервере должен находиться виртуальный окружение?

Внутри директории самого приложения, как /srv/myapp/.venv, под владельцем пользователя-развертывателя, при этом сервисная учетная запись должна иметь права только на чтение и выполнение. Используйте по одному venv на каждое приложение, так как общее окружение означает, что обновление одного приложения может сломать другое. Не перемещайте и не копируйте venv после создания: в каждом скрипте в его директории bin/ прописан абсолютный путь в строке shebang, поэтому перемещенный venv завершится с ошибкой bad interpreter: No such file or directory. Удалите его и пересоберите из requirements.txt.

Почему мой systemd-сервис падает с ошибкой ModuleNotFoundError?

Потому что юнит запускает интерпретатор, который не является интерпретатором venv. Выполните systemctl cat myapp и прочитайте ExecStart. Там должен быть указан абсолютный путь к /srv/myapp/.venv/bin/python или консольному скрипту из той же директории bin/. Использование source для activate в файле юнита не сработает, так как 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, если вам нужен lock-файл, если время установки слишком велико или если вам нужна версия Python, которой нет в вашем дистрибутиве. Он создает обычный venv, поэтому юнит systemd и структура файлов не меняются, а uv sync --frozen устанавливает именно то, что зафиксировано в lock-файле. Если приложение развертывается из git с зафиксированным requirements.txt и установка занимает секунды, то python3 -m venv вполне достаточно, и это на один бинарный файл меньше, который нужно обновлять на сервере.

#python#venv#pipx#uv#deployment