venv, pipx ou uv sur Ubuntu : quel outil choisir ?
L'erreur externally-managed-environment bloque pip sur Ubuntu 24.04. Apprenez à choisir entre venv, pipx et uv selon vos besoins et configurez correctement vos services systemd.
Pourquoi pip install échoue sur un serveur Ubuntu vierge
Le choix entre un venv Python, pipx et uv sur un serveur se résume à une question : qu'installez-vous ? Les dépendances d'une application doivent résider dans un environnement virtuel au sein du répertoire de l'application. Les outils en ligne de commande que vous souhaitez appeler par leur nom doivent être gérés par pipx. uv effectue ces deux tâches et ajoute un fichier de verrouillage (lockfile), ce qui devient nécessaire dès qu'une seconde machine doit construire le même environnement. Aucun de ces outils n'installe de paquets dans le Python système, car un serveur Ubuntu actuel le refuse systématiquement.
Exécutez sudo pip install requests sur Ubuntu 24.04 et pip s'arrête avant même de télécharger le moindre fichier.
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.Il s'agit de la PEP 668 (Python enhancement proposal 668, « environnements gérés de manière externe ») qui remplit son rôle. Debian et Ubuntu déposent un fichier marqueur à côté de l'interpréteur dans /usr/lib/python3.12/EXTERNALLY-MANAGED, et pip refuse d'écrire dans tout interpréteur qui en possède un.
Cette règle existe pour des raisons d'ordre de sys.path. apt installe les bibliothèques dans /usr/lib/python3/dist-packages. pip, exécuté en root sur l'interpréteur système, écrit dans /usr/local/lib/python3.12/dist-packages, et le système de paquets de Debian place ce répertoire en priorité dans le chemin de recherche. Affichez-le vous-même avec python3 -c 'import sys; print(sys.path)' pour vérifier l'ordre. Ainsi, la copie écrite par pip masque la copie installée par apt pour tous les programmes de la machine s'exécutant sous /usr/bin/python3, y compris les outils de la distribution. cloud-init importe requests, jinja2 et PyYAML depuis cet interpréteur. Mettez à jour l'un d'eux avec pip, tombez sur une version incompatible, et un composant que vous n'avez jamais touché échouera au prochain démarrage avec une trace d'erreur mentionnant un paquet dont vous ignoriez la présence dans la chaîne. apt enregistre toujours sa propre version comme étant installée, donc aucune alerte ne vous prévient, et la réparation est sudo apt reinstall python3-requests.
La règle qui en découle est simple. Le Python système appartient à la distribution. N'installez rien dedans, ne mettez pas à jour ses bibliothèques avec pip, et ne supprimez pas le fichier EXTERNALLY-MANAGED pour faire disparaître le message. La seule tâche à confier à /usr/bin/python3 est la création d'environnements virtuels.
venv, pipx et uv : la règle de décision
Choisissez en fonction de ce que vous installez, et non de l'outil dont vous avez entendu parler le plus récemment.
- Une application que vous déployez et exécutez en tant que service, comme un projet Django ou Flask : un environnement virtuel (venv) à l'intérieur du répertoire de cette application.
- Un outil en ligne de commande que vous voulez sur votre
PATH, commeansibleouhttpie: pipx, qui attribue à chaque outil un environnement privé et un lien surPATH. - Un projet qui nécessite un fichier de verrouillage (lockfile), des installations plus rapides ou une version de Python que la distribution ne fournit pas : uv, qui génère un venv classique accompagné d'un fichier
uv.lock. - Une bibliothèque dont un outil de la distribution a besoin, plutôt que votre code :
sudo apt install python3-<name>, la seule méthode supportée pour ajouter quoi que ce soit à l'interpréteur système.
pipx et uv tool install effectuent la même tâche ; une machine qui possède déjà uv n'a donc pas besoin de pipx en plus. Le framework web choisi ne change rien ici : Django et Flask sur un VPS diffèrent par ce qui se trouve dans requirements.txt, mais pas par la manière dont l'environnement est construit. Tout ce qui suit utilise Ubuntu 24.04 et son Python 3.12 ; ajustez donc la version dans les chemins si la vôtre est différente.
Créer le venv par application
Ubuntu sépare le module venv du paquet Python de base. Sur une image minimale, la première tentative échoue avec un message indiquant précisément ce qui manque.
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-venvInstallez-le, puis créez l'environnement en tant qu'utilisateur propriétaire du code.
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.txtNotez ce qui est absent : pas de source, pas de activate. /srv/myapp/.venv/bin/pip s'installe dans cet environnement grâce à l'emplacement du binaire, et non grâce à une variable exportée dans le shell. Confirmez cela avant de poursuivre.
/srv/myapp/.venv/bin/python -c 'import sys; print(sys.prefix)'Cela affiche /srv/myapp/.venv. Si cela affiche /usr, vous utilisez l'interpréteur système et vos paquets ont été installés à un emplacement non souhaité.
Deux propriétés d'un venv déterminent ce que vous pouvez en faire par la suite. Un venv n'est pas déplaçable, car chaque script dans bin/ contient une ligne shebang absolue : head -1 /srv/myapp/.venv/bin/pip lit #!/srv/myapp/.venv/bin/python. Renommez le répertoire parent et ces scripts échoueront avec bad interpreter: No such file or directory. Un venv verrouille également l'interpréteur qui l'a créé, enregistré via la ligne home dans /srv/myapp/.venv/pyvenv.cfg, et bin/python3 est un lien symbolique vers ce binaire. Si vous mettez à jour la version de l'OS et que python3.12 disparaît, le lien symbolique n'a plus de cible et le service échoue au démarrage avec No such file or directory. Dans les deux cas, la solution est identique : supprimez le venv et reconstruisez-en un nouveau à partir de requirements.txt. La reconstruction ne prend que quelques secondes. Ne copiez jamais un venv entre deux machines.
Emplacement et droits du venv
Placez-le à côté du code dans /srv/myapp/.venv et conservez un venv par application. Le déploiement constitue alors un répertoire unique, l'unité systemd utilise un chemin fixe, et deux applications ne peuvent jamais se corrompre mutuellement lors d'une mise à jour de dépendance. Ne placez jamais un venv dans un répertoire exposé directement par votre serveur web, car il contient vos dépendances et souvent votre configuration.
La gestion des droits mérite une attention particulière. Laissez un utilisateur deploy posséder le code et l'environnement, et accordez au compte de service uniquement des droits de lecture et d'exécution.
sudo adduser --system --group --no-create-home myapp
sudo chown -R deploy:myapp /srv/myapp
sudo chmod -R o-rwx /srv/myappLe service peut désormais importer ses dépendances sans pouvoir les modifier. Ainsi, une faille d'exécution de code dans l'application web ne permet pas de remplacer discrètement une bibliothèque sur le disque pour persister après un redémarrage. Le même raisonnement appliqué au reste de la machine est détaillé dans exécuter des services avec des privilèges restreints.
pipx pour les outils en ligne de commande
pipx installe des applications, pas des bibliothèques. Chaque outil dispose de son propre environnement sous ~/.local/share/pipx/venvs/<name>, et les exécutables de cet outil sont liés dans ~/.local/bin. Ainsi, deux outils nécessitant des versions différentes d'une même bibliothèque ne sont jamais en conflit.
sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install httpiepipx ensurepath ajoute ~/.local/bin à PATH en modifiant le fichier de configuration de votre shell. Il ne peut pas modifier le shell dans lequel vous travaillez actuellement. Par conséquent, http: command not found juste après l'installation signifie généralement que vous ne vous êtes pas encore déconnecté puis reconnecté. Le ~/.profile par défaut d'Ubuntu n'ajoute ~/.local/bin que si ce répertoire existe déjà à la connexion. C'est pourquoi ce problème survient une fois sur un nouveau compte et ne se reproduit plus.
Si vous pointez pipx vers une bibliothèque, il refuse l'opération avec un message commençant par :
No apps associated with package requests or its dependencies.C'est l'outil qui vous indique que vous utilisez le mauvais instrument. Les bibliothèques doivent être placées dans le venv d'une application.
Le détail qui compte sur un serveur est l'emplacement. Un pipx install standard place tout dans le répertoire personnel d'un utilisateur. Une unité systemd s'exécutant en tant que myapp ne peut pas le voir, une tâche cron root ne peut pas le voir, et sudo ne le trouvera pas non plus, car secure_path dans /etc/sudoers remplace PATH par une liste fixe. Pour un outil destiné à toute la machine, installez-le globalement.
sudo pipx install --global ansible
sudo pipx ensurepath --globalLe flag --global place les environnements dans /opt/pipx et lie les exécutables dans /usr/local/bin, qui se trouve dans le PATH par défaut et à l'intérieur de secure_path. Vérifiez d'abord votre version avec pipx --version, car Ubuntu 24.04 propose pipx 1.4.3, qui est plus ancien que --global, et un pipx plus ancien répond avec unrecognized arguments: --global. Sur cette version, définissez vous-même les deux répertoires documentés :
sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible
command -v ansiblecommand -v ansible doit afficher /usr/local/bin/ansible. S'il affiche un chemin sous /home, l'outil a été installé dans le compte d'un utilisateur unique et aucun service ne le trouvera.
uv pour gérer un lockfile
uv est un binaire unique fourni par Astral qui remplace pip, venv et pip-tools, tout en pouvant télécharger des interpréteurs. Il est suffisamment rapide pour que la différence soit notable sur un petit VPS, et il génère un véritable lockfile.
L'installateur officiel place uv et uvx dans ~/.local/bin :
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --versionTransmettre un script via un pipe à un shell sur un serveur mérite un instant d'attention. Fixez la version dans l'URL et lisez le fichier avant de l'exécuter :
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 fonctionne également si pipx est déjà installé. uv est un binaire autonome sans dépendance Python propre ; le copier dans /usr/local/bin est donc une méthode valide pour le rendre disponible à tous les utilisateurs de la machine.
Pour un projet avec un pyproject.toml, le flux de travail se résume à quatre commandes, dont seule la dernière est exécutée sur le serveur.
uv init myapp
uv add flask gunicorn
uv lock
uv sync --frozen --no-devuv lock génère uv.lock, un lockfile multiplateforme contenant les versions résolues exactes, que vous devez versionner avec votre code. uv sync crée .venv à la racine du projet pour correspondre à ce fichier. Sur le serveur, --frozen est l'option déterminante : la documentation précise qu'elle utilise les versions du lockfile comme source de vérité, sans vérifier si le lockfile est à jour, ce qui est le comportement attendu pour un déploiement. --no-dev exclut le groupe de dépendances de développement.
Un projet requirements.txt existant ne nécessite aucune conversion, car uv utilise le langage de pip :
uv venv /srv/myapp/.venv
uv pip install --python /srv/myapp/.venv/bin/python -r /srv/myapp/requirements.txtLe résultat est un environnement virtuel classique. .venv/bin/python se comporte exactement comme si python3 -m venv l'avait créé, donc rien ne change pour la suite de ce guide.
Un comportement par défaut de uv mérite d'être connu avant toute utilisation sur un serveur. Son paramètre python-preference est réglé par défaut sur managed, ce qui signifie qu'il privilégie les interpréteurs téléchargés par uv plutôt que ceux déjà présents sur le système. Ainsi, uv venv --python 3.13 sur une machine qui ne propose que la version 3.12 téléchargera silencieusement la 3.13 dans ~/.local/share/uv/python au lieu de renvoyer une erreur. C'est pratique sur un ordinateur portable, mais surprenant sur un serveur, car votre service dépend alors d'un interpréteur situé dans un répertoire utilisateur que apt upgrade ne mettra jamais à jour. Réglez python-preference sur only-system dans uv.toml si vous souhaitez utiliser l'interpréteur de la distribution. Si vous préférez que l'environnement soit situé ailleurs qu'à la racine du projet, UV_PROJECT_ENVIRONMENT permet de spécifier le répertoire à utiliser pour l'environnement virtuel du projet.
Pointer l'interpréteur du venv dans systemd, pas le script activate
C'est ici que la plupart des déploiements Python échouent, à cause d'une mauvaise compréhension du rôle de activate.
bin/activate est un script shell. Il ajoute le répertoire bin du venv au début de PATH, définit VIRTUAL_ENV, sauvegarde les anciennes valeurs pour que deactivate puisse les restaurer, et modifie votre invite de commande. Il ne contient rien que l'interpréteur lui-même lise. L'activation est une commodité pour un humain qui tape python dans un terminal.
Ce qui sélectionne réellement l'environnement, c'est le fichier d'interpréteur que vous exécutez. Lorsque /srv/myapp/.venv/bin/python démarre, le module site de Python cherche un fichier pyvenv.cfg dans le répertoire contenant l'exécutable et dans le répertoire parent. Trouver /srv/myapp/.venv/pyvenv.cfg définit sys.prefix sur le venv, ce qui place le répertoire site-packages de ce venv dans sys.path. C'est tout le mécanisme. Il ne nécessite aucune variable d'environnement ni aucun shell.
Par conséquent, cette unité ne démarre jamais :
[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 n'est pas une ligne de commande shell. systemd exécute un programme directement, il n'y a donc pas de builtin source, && est transmis comme un argument littéral, et rien n'est développé.
Et cette unité démarre, puis s'arrête :
[Service]
ExecStart=/usr/bin/python3 /srv/myapp/app.pyModuleNotFoundError: No module named 'flask'/usr/bin/python3 est l'interpréteur système, et son sys.path n'a jamais contenu votre venv. La même commande fonctionne dans votre session SSH uniquement parce que vous y aviez activé le venv, ce qui a permis au shell de résoudre python3 via PATH vers .venv/bin/python3.
Envelopper la commande dans /bin/bash -c 'source ... && gunicorn ...' fonctionne. Cela ajoute toutefois un shell entre systemd et votre processus sans aucun gain, alors qu'un chemin absolu règle le problème :
[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 doit renvoyer active (running) avec un Main PID correspondant à votre processus gunicorn. Pour tout autre résultat, consultez le journal.
La ligne Environment=PATH= n'est pas là pour ExecStart, qui utilise déjà un chemin complet. Elle est là pour les processus que votre application lance. Un service hérite d'un PATH par défaut restreint de la part de systemd, donc le code Python appelant subprocess.run(["ffmpeg", ...]), ou une commande de gestion qui exécute un script console du venv, ne trouvera pas ce dont il a besoin. Placer le répertoire bin du venv en premier est la seule partie de activate qu'un service utilise réellement. Vérifiez ce que l'unité a réellement reçu avec systemctl show -p Environment myapp.
La même règle s'applique aux tâches planifiées. cron exécute les tâches avec un PATH valant /usr/bin:/bin, donc une ligne crontab lisant python3 /srv/myapp/cleanup.py exécute l'interpréteur système et échoue avec ModuleNotFoundError à trois heures du matin, et l'erreur est envoyée vers une boîte mail locale que personne ne consulte. Indiquez également le chemin absolu du venv à cet endroit. Pour obtenir cette sortie dans le journal et un historique de la dernière exécution, un couple service et timer systemd utilise la même ligne ExecStart.
Docker remplace-t-il cette décision ?
Un conteneur possède son propre système de fichiers, la question change donc de forme plutôt que de disparaître. Dans une image officielle comme python:3.12-slim, Python est intégré à /usr/local et ne comporte aucun marqueur EXTERNALLY-MANAGED. Par conséquent, pip install en tant que root est la méthode prévue pour ajouter des paquets, et un venv n'apporte que peu d'intérêt. Si vous construisez FROM ubuntu:24.04 à la place, vous rencontrerez à nouveau externally-managed-environment à l'intérieur de l'image, pour la même raison que sur l'hôte : il s'agit de l'interpréteur de la distribution, qui contient le fichier marqueur de la distribution.
De nombreuses images utilisent encore un venv, car cela simplifie les builds multi-étapes. L'étape de construction installe les dépendances dans /opt/venv, et l'étape d'exécution copie uniquement ce répertoire, laissant les compilateurs de côté. Le problème de l'activation persiste. Une ligne RUN source /opt/venv/bin/activate n'affecte que le shell de cette couche de build ; au moment de l'exécution, le conteneur démarre donc sur l'interpréteur système et déclenche ModuleNotFoundError. Définissez ENV PATH="/opt/venv/bin:$PATH", ou indiquez à CMD le chemin absolu /opt/venv/bin/gunicorn. Il s'agit du même bug que celui rencontré avec systemd, mais dans un fichier différent.
Ainsi, un conteneur remplace la question de l'interpréteur, car l'image fige l'interpréteur et tout ce qu'il contient. Il ne remplace pas la question du verrouillage des versions. Une image construite à partir d'un requirements.txt non verrouillé résoudra des versions différentes le mois prochain ; cela signifie que le tag de l'image est reproductible, mais que le build qui l'a produit ne l'est pas. Un fichier de verrouillage comme uv.lock, ou un fichier requirements entièrement figé, est ce qui comble cette lacune, avec ou sans conteneur. Et lorsqu'une application tourne sur un VPS sous systemd, un conteneur déplace principalement cette décision dans un Dockerfile, puisque systemd redémarre déjà un processus défaillant et capture sa sortie dans le journal. Exécuter Docker sur un VPS se justifie pleinement lorsque vous souhaitez que l'image construite soit l'élément que vous déployez.
FAQ
Puis-je simplement utiliser pip install avec --break-system-packages ?
Pas sur un serveur que vous devez maintenir en production. Ce flag fait exactement ce qu'il annonce : il supprime la protection et pip écrit dans /usr/local/lib/python3.12/dist-packages, qui est prioritaire sur le répertoire apt dans sys.path. Votre version masque alors celle de la distribution pour tous les scripts système s'exécutant sous /usr/bin/python3. Comme apt croit toujours que sa propre version est installée, aucun mécanisme ne détecte le conflit avant qu'une erreur ne survienne. À l'intérieur d'une image de conteneur reconstruite à partir de zéro à chaque fois, les dégâts sont limités à cette image, ce qui rend la pratique acceptable dans ce contexte. Sur une machine que vous maintenez, créez un venv. C'est une seule commande.
Où le virtual environment doit-il se situer sur un serveur ?
À l'intérieur du répertoire de l'application, sous le nom /srv/myapp/.venv, détenu par un utilisateur de déploiement, avec le compte de service disposant uniquement des droits de lecture et d'exécution. Maintenez un venv par application, car un venv partagé signifie qu'une mise à jour pour la première application peut casser la seconde. Ne déplacez pas et ne copiez pas un venv après sa création : chaque script dans son répertoire bin/ contient ce chemin absolu écrit dans sa ligne shebang. Un venv déplacé échouera donc avec bad interpreter: No such file or directory. Supprimez-le et reconstruisez-le à partir de requirements.txt à la place.
Pourquoi mon service systemd échoue-t-il avec ModuleNotFoundError ?
Parce que l'unité exécute un interpréteur qui n'est pas celui du venv. Exécutez systemctl cat myapp et lisez ExecStart. Il doit pointer vers /srv/myapp/.venv/bin/python, ou vers un script de console situé dans ce même répertoire bin/, via un chemin absolu. Sourcer activate dans un fichier d'unité ne peut pas fonctionner, car ExecStart n'est pas un shell, et systemd renvoie Failed to locate executable source avec status=203/EXEC. Ajoutez Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin afin que tout sous-processus lancé par votre code trouve également les outils du venv.
Dois-je utiliser uv au lieu de venv et pip ?
Utilisez uv si vous souhaitez un fichier de verrouillage (lockfile), si le temps d'installation est trop long, ou si vous avez besoin d'une version de Python que votre distribution ne fournit pas. Il crée un venv standard, donc l'unité systemd et l'arborescence des fichiers ne changent pas, et uv sync --frozen installe exactement ce que le fichier de verrouillage enregistre. Si une application unique est déployée depuis git avec un requirements.txt figé et que l'installation se termine en quelques secondes, python3 -m venv est déjà suffisant, et cela fait un binaire de moins à maintenir à jour sur le serveur.