SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-13

Python venv, pipx of uv op Ubuntu server gebruiken

Krijgt u de foutmelding externally-managed-environment op Ubuntu? Ontdek wanneer u kiest voor venv, pipx of uv en hoe u deze correct koppelt aan systemd voor uw server.

Waarom pip install faalt op een nieuwe Ubuntu-server

De keuze tussen een Python venv, pipx en uv op een server hangt af van één vraag: wat installeert u? Applicatie-dependencies horen thuis in een virtuele omgeving binnen de eigen directory van de applicatie. Command-line tools die u bij naam wilt aanroepen, horen thuis in pipx. uv voert beide taken uit en voegt een lockfile toe, wat relevant wordt zodra een tweede machine dezelfde omgeving moet opbouwen. Wat geen van deze tools doet, is installeren in de systeem-Python, omdat een huidige Ubuntu-server dat ronduit weigert.

Voer sudo pip install requests uit op Ubuntu 24.04 en pip stopt voordat het een enkel bestand downloadt.

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.

Dit is PEP 668 (Python enhancement proposal 668, "externally managed environments") die zijn werk doet. Debian en Ubuntu plaatsen een markeringsbestand naast de interpreter op /usr/lib/python3.12/EXTERNALLY-MANAGED, en pip weigert te schrijven naar elke interpreter die zo'n bestand bevat.

De regel bestaat vanwege de sys.path-volgorde. apt installeert bibliotheken in /usr/lib/python3/dist-packages. pip, uitgevoerd als root tegen de systeem-interpreter, schrijft naar /usr/local/lib/python3.12/dist-packages, en de packaging van Debian plaatst die directory eerder in het zoekpad. Print dit zelf met python3 -c 'import sys; print(sys.path)' en lees de volgorde. De kopie die pip schreef, overschaduwt dus de kopie die apt installeerde voor elk programma op de machine dat onder /usr/bin/python3 draait, inclusief de eigen tools van de distributie. cloud-init importeert requests, jinja2 en PyYAML vanuit die interpreter. Upgrade een van deze met pip, kom terecht bij een incompatibele release, en iets waar u nooit aan bent gekomen faalt bij de volgende boot met een traceback die een pakket noemt waarvan u niet wist dat het in de keten zat. apt registreert nog steeds zijn eigen versie als geïnstalleerd, dus niets waarschuwt u, en de reparatie is sudo apt reinstall python3-requests.

De regel die volgt is kort. De systeem-Python behoort toe aan de distributie. Installeer er niet in, upgrade de bibliotheken niet met pip, en verwijder het EXTERNALLY-MANAGED-bestand niet om de melding te laten verdwijnen. De enige taak voor /usr/bin/python3 is het bouwen van virtuele omgevingen.

venv versus pipx versus uv: de beslissingsregel

Kies op basis van wat u installeert, niet op basis van de tool waarover u onlangs het meest hebt gelezen.

  • Een applicatie die u implementeert en uitvoert als een service, zoals een Django- of Flask-project: één virtual environment (venv) binnen de map van die applicatie.
  • Een command-line tool die u op uw PATH wilt hebben, zoals ansible of httpie: pipx, dat elke tool een eigen omgeving en één koppeling op PATH geeft.
  • Een project dat een lockfile, snellere installaties of een Python-versie vereist die de distributie niet meelevert: uv, dat een standaard venv plus een uv.lock-bestand produceert.
  • Een bibliotheek die een distributietool nodig heeft in plaats van uw code: sudo apt install python3-<name>, de enige ondersteunde manier om iets toe te voegen aan de systeeminterpreter.

pipx en uv tool install doen hetzelfde werk, dus een machine die al over uv beschikt, heeft geen pipx nodig. Welk webframework u kiest, verandert hier niets aan: Django en Flask op een VPS verschillen in wat er in requirements.txt terechtkomt, niet in hoe de omgeving eromheen wordt gebouwd. Alles hieronder gebruikt Ubuntu 24.04 en de bijbehorende Python 3.12, dus pas de versie in de paden aan als de uwe afwijkt.

Bouw de per-applicatie venv

Ubuntu splitst de venv-module af van het basis-Python-pakket. Op een minimale image mislukt de eerste poging daarom met een melding die precies aangeeft wat er ontbreekt.

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

Installeer deze module en maak vervolgens de omgeving aan als de gebruiker die eigenaar van de code wordt.

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

Let op wat er niet aanwezig is: geen source, geen activate. /srv/myapp/.venv/bin/pip installeert in die omgeving vanwege de locatie van het binaire bestand, niet door iets dat u naar de shell heeft geëxporteerd. Controleer dit voordat u verdergaat.

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

Dit print /srv/myapp/.venv. Als het /usr print, gebruikt u de systeem-interpreter en zijn uw pakketten op een locatie terechtgekomen die niet de bedoeling was.

Twee eigenschappen van een venv bepalen wat u er daarna mee kunt doen. Een venv is niet verplaatsbaar, omdat elk script in bin/ een absoluut shebang-pad bevat: head -1 /srv/myapp/.venv/bin/pip leest #!/srv/myapp/.venv/bin/python. Hernoem de bovenliggende map en deze scripts falen met bad interpreter: No such file or directory. Een venv koppelt ook de interpreter die deze heeft aangemaakt, vastgelegd als de home-regel in /srv/myapp/.venv/pyvenv.cfg, en bin/python3 is een symbolische link naar dat binaire bestand. Voer een release-upgrade uit waardoor python3.12 verdwijnt; de symbolische link heeft dan geen doel meer en de service stopt bij het opstarten met No such file or directory. Beide gevallen hebben dezelfde oplossing: verwijder de venv en bouw een nieuwe vanaf requirements.txt. Het opnieuw opbouwen duurt enkele seconden. Kopieer nooit een venv tussen machines.

Waar de venv zich bevindt en wie de eigenaar is

Plaats deze naast de code op /srv/myapp/.venv en houd één venv per applicatie aan. De implementatie is dan één enkele map, de systemd-unit krijgt een pad dat nooit verandert en twee applicaties kunnen elkaar nooit verstoren door een gedeelde update van dependencies. Plaats een venv nooit op een locatie waar uw webserver direct bestanden publiceert, aangezien deze uw dependencies en vaak ook uw configuratie bevat.

Eigenaarschap verdient enige aandacht. Laat een deploy-gebruiker de eigenaar zijn van de code en de omgeving, en geef het service-account alleen lees- en uitvoerrechten.

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

De service kan nu zijn dependencies importeren, maar kan deze niet overschrijven. Dit betekent dat een bug voor code-executie in de webapplicatie niet stilletjes een bibliotheek op de schijf kan vervangen en een herstart kan overleven. Dezelfde redenering toegepast op de rest van de machine wordt behandeld in services draaien als gebruikers met minimale rechten.

pipx voor command-line tools

pipx installeert applicaties, geen libraries. Elke tool krijgt een eigen omgeving onder ~/.local/share/pipx/venvs/<name>, en de executables van die tool worden gelinkt naar ~/.local/bin. Hierdoor conflicteren twee tools die verschillende versies van dezelfde library vereisen nooit.

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

pipx ensurepath voegt ~/.local/bin toe aan PATH door uw shell-opstartbestand te bewerken. Het kan de shell waarin u zich al bevindt niet wijzigen, dus http: command not found direct na de installatie betekent meestal dat u nog niet opnieuw bent ingelogd. De standaard ~/.profile van Ubuntu voegt ~/.local/bin alleen toe wanneer die map bij het inloggen al bestaat. Daarom treedt dit probleem eenmalig op bij een nieuw account en daarna niet meer.

Wijs pipx naar een library en het zal weigeren, met een melding die begint met:

No apps associated with package requests or its dependencies.

Dat is de tool die aangeeft dat het het verkeerde instrument is. Libraries horen thuis in de venv van een applicatie.

Het detail dat op een server van belang is, is de locatie. Een standaard pipx install plaatst alles onder de home-directory van één gebruiker. Een systemd-unit die draait als myapp kan dit niet zien, een root-cronjob kan dit niet zien, en sudo zal het ook niet vinden, omdat secure_path in /etc/sudoers de PATH vervangt door een vaste lijst. Installeer een tool die de hele machine moet kunnen gebruiken daarom globaal.

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

De --global-vlag plaatst omgevingen in /opt/pipx en linkt executables naar /usr/local/bin, wat in de standaard PATH staat en binnen secure_path valt. Controleer eerst uw versie met pipx --version, omdat Ubuntu 24.04 pipx 1.4.3 verpakt, wat ouder is dan --global, en een oudere pipx reageert met unrecognized arguments: --global. Stel op die versie de twee gedocumenteerde mappen zelf in:

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

command -v ansible hoort /usr/local/bin/ansible te printen. Als het een pad onder /home print, is de tool in het account van een enkele gebruiker terechtgekomen en zal geen enkele service deze vinden.

uv wanneer u een lockfile wilt

uv is een enkel binair bestand van Astral dat de functies van pip, venv en pip-tools combineert, en het kan bovendien interpreters downloaden. Het is snel genoeg om het verschil merkbaar te maken op een kleine VPS, en het schrijft een echte lockfile.

De officiële installer plaatst uv en uvx in ~/.local/bin:

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

Het via een pipe naar een shell sturen van een script op een server vereist enige voorzichtigheid. Pin de versie in de URL en lees het bestand voordat u het uitvoert:

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 werkt ook, wanneer pipx al aanwezig is. uv is één op zichzelf staand binair bestand zonder eigen Python-afhankelijkheid, dus het kopiëren naar /usr/local/bin is een geldige manier om het beschikbaar te maken voor elke gebruiker op de server.

Voor een project met een pyproject.toml bestaat de workflow uit vier commando's, waarvan alleen de laatste op de server wordt uitgevoerd.

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

uv lock schrijft uv.lock, een platformonafhankelijke lockfile met exact opgeloste versies, die u samen met uw code commit. uv sync bouwt .venv in de projectmap om hiermee overeen te komen. Op de server is --frozen de relevante vlag: de documentatie definieert dit als het gebruik van de versies in de lockfile als de bron van waarheid, in plaats van te controleren of de lockfile up-to-date is; dit is het gewenste gedrag voor een deployment. --no-dev laat de groep met ontwikkelingsafhankelijkheden weg.

Een bestaand requirements.txt-project vereist geen conversie, omdat uv de taal van pip spreekt:

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

Het resultaat is een gewone virtuele omgeving. .venv/bin/python gedraagt zich precies zoals het zou doen als python3 -m venv deze had gebouwd, dus niets in het verdere verloop van deze handleiding verandert.

Eén standaardinstelling van uv is belangrijk om te kennen voordat u het op een server gebruikt. De instelling python-preference staat standaard op managed, wat gedocumenteerd is als het verkiezen van "interpreters die door uv worden gedownload en geïnstalleerd" boven interpreters die al op het systeem aanwezig zijn. Dus uv venv --python 3.13 op een systeem dat alleen 3.12 bevat, haalt stilletjes 3.13 op naar ~/.local/share/uv/python in plaats van te falen. Dat is handig op een laptop, maar verrassend op een server, omdat uw service nu afhankelijk is van een interpreter die in een homedirectory staat en die apt upgrade nooit zal patchen. Stel python-preference in op only-system in uv.toml als u de interpreter van de distributie wilt gebruiken. Als u de omgeving ergens anders wilt hebben dan in de projectmap, specificeert UV_PROJECT_ENVIRONMENT de directory die gebruikt moet worden voor een virtuele projectomgeving.

Verwijs systemd naar de venv-interpreter, niet naar activate

Dit is waar de meeste Python-implementaties misgaan. De oorzaak is een misverstand over wat activate doet.

bin/activate is een shell-script. Het voegt de bin-directory van de venv toe aan het begin van PATH, stelt VIRTUAL_ENV in, slaat de oude waarden op zodat deactivate ze kan herstellen, en wijzigt uw prompt. Het bevat niets wat de interpreter zelf leest. Activering is een gemak voor een mens die python typt bij een prompt.

Wat de omgeving daadwerkelijk selecteert, is welk interpreter-bestand u uitvoert. Wanneer /srv/myapp/.venv/bin/python start, zoekt de site-module van Python naar een pyvenv.cfg-bestand in de directory met het uitvoerbare bestand en één niveau daarboven. Het vinden van /srv/myapp/.venv/pyvenv.cfg stelt sys.prefix in op de venv, waardoor de site-packages van die venv op sys.path komt te staan. Dat is het volledige mechanisme. Het heeft geen omgevingsvariabele en geen shell nodig.

Deze unit start dus nooit:

[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 is geen shell-opdrachtregel. systemd voert een programma rechtstreeks uit, dus er is geen source-builtin, && wordt doorgegeven als een letterlijk argument en er vindt geen expansie plaats.

En deze unit start, maar stopt direct weer:

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

/usr/bin/python3 is de systeem-interpreter en de sys.path daarvan heeft uw venv nooit bevat. Hetzelfde commando werkt alleen in uw SSH-sessie omdat u daar de venv had geactiveerd, waardoor de shell python3 via PATH naar .venv/bin/python3 had omgezet.

Het commando verpakken in /bin/bash -c 'source ... && gunicorn ...' werkt wel. Het plaatst echter ook een shell tussen systemd en uw proces zonder enig voordeel, terwijl één absoluut pad het probleem oplost:

[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 hoort active (running) te rapporteren met een Main PID die uw gunicorn-proces is. Als er iets anders staat, lees dan het journal.

De regel Environment=PATH= is er niet voor ExecStart, die al een volledig pad bevat. Deze is er voor de processen die uw applicatie start. Een service erft een kort standaard PATH van systemd, dus Python-code die subprocess.run(["ffmpeg", ...]) aanroept, of een beheercommando dat een console-script uit de venv aanroept, zal niet vinden wat het nodig heeft. De bin-directory van de venv vooraan plaatsen is het enige onderdeel van activate dat een service daadwerkelijk gebruikt. Controleer wat de unit daadwerkelijk heeft ontvangen met systemctl show -p Environment myapp.

Dezelfde regel geldt voor geplande taken. cron voert taken uit met een PATH van /usr/bin:/bin, dus een crontab-regel die python3 /srv/myapp/cleanup.py leest, voert de systeem-interpreter uit en faalt om drie uur 's ochtends met ModuleNotFoundError, waarna de fout naar een lokale mail-spool gaat die niemand leest. Schrijf ook daar het absolute venv-pad. Om die output in het journal te krijgen en een overzicht van de laatste uitvoering, gebruikt u een combinatie van een systemd-service en timer met dezelfde ExecStart-regel.

Vervangt Docker deze beslissing?

Een container heeft een eigen bestandssysteem, waardoor de vraag van vorm verandert in plaats van verdwijnt. In een officiële image zoals python:3.12-slim is Python ingebouwd in /usr/local en bevat deze geen EXTERNALLY-MANAGED-markering. Daarom is pip install als root de beoogde manier om pakketten toe te voegen en voegt een venv weinig toe. Bouw in plaats daarvan FROM ubuntu:24.04 en u komt externally-managed-environment opnieuw tegen in de image, om dezelfde reden als op de host: het is de interpreter van de distributie die het markeringsbestand van de distributie bevat.

Veel images gebruiken nog steeds een venv, omdat dit een multi-stage build eenvoudig maakt. De builder-fase installeert in /opt/venv, en de runtime-fase kopieert die ene map en laat de compilers achter. Het activate-probleem reist mee. Een RUN source /opt/venv/bin/activate-regel heeft alleen invloed op de shell van die build-laag, dus bij runtime start de container op de systeem-interpreter en genereert ModuleNotFoundError. Stel ENV PATH="/opt/venv/bin:$PATH" in, of geef CMD het absolute pad /opt/venv/bin/gunicorn. Het is dezelfde bug als die in systemd, maar dan in een ander bestand.

Een container vervangt dus de vraag over de interpreter, omdat de image de interpreter en alles daaronder vastzet. Het vervangt niet de vraag over het vastzetten van versies. Een image die is gebouwd op basis van een niet-vastgezet requirements.txt zal volgende maand andere versies resolven. Dit betekent dat de image-tag reproduceerbaar is, terwijl de build die deze heeft geproduceerd dat niet is. Een lockfile zoals uv.lock, of een volledig vastgezet requirements-bestand, dicht dat gat, ongeacht of u containers gebruikt of niet. En wanneer één applicatie op één VPS onder systemd draait, verplaatst een container deze beslissing grotendeels naar een Dockerfile, aangezien systemd een gefaald proces al opnieuw opstart en de uitvoer ervan vastlegt in de journal. Docker draaien op een VPS is zinvol wanneer u wilt dat de gebouwde image zelf het object is dat u uitrolt.

FAQ

Kan ik gewoon pip install gebruiken met --break-system-packages?

Niet op een server die u draaiende moet houden. De flag doet precies wat de naam zegt: het verwijdert de beveiliging en pip schrijft naar /usr/local/lib/python3.12/dist-packages, dat in de zoekvolgorde vóór de apt-directory op sys.path staat. Uw versie overschrijft vervolgens de versie van de distributie voor elk systeemscript dat onder /usr/bin/python3 draait. Omdat apt nog steeds gelooft dat zijn eigen versie is geïnstalleerd, wordt het conflict niet gedetecteerd totdat er iets uitvalt. Binnen een container-image die u telkens vanaf nul opbouwt, blijft de schade beperkt tot die image; daar is het verdedigbaar. Op een machine die u onderhoudt, maakt u een venv aan. Dat is slechts één commando.

Waar moet de virtual environment op een server staan?

Binnen de eigen directory van de applicatie, als /srv/myapp/.venv, eigendom van een deploy-gebruiker, waarbij het service-account alleen lees- en uitvoerrechten heeft. Gebruik één venv per applicatie, omdat een gedeelde venv betekent dat een upgrade voor de eerste applicatie de tweede kan breken. Verplaats of kopieer een venv niet nadat deze is aangemaakt: elk script in de bin/-directory bevat het absolute pad in de shebang-regel, waardoor een verplaatste venv faalt met bad interpreter: No such file or directory. Verwijder deze in plaats daarvan en bouw hem opnieuw op vanuit requirements.txt.

Waarom faalt mijn systemd-service met ModuleNotFoundError?

Omdat de unit een interpreter uitvoert die niet die van de venv is. Voer systemctl cat myapp uit en lees ExecStart. Het moet verwijzen naar /srv/myapp/.venv/bin/python, of een console-script uit diezelfde bin/-directory, via een absoluut pad. Het sourcen van activate in een unit-bestand werkt niet, omdat ExecStart geen shell is en systemd Failed to locate executable source rapporteert met status=203/EXEC. Voeg Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin toe zodat elk subproces dat uw code start ook de tools van de venv vindt.

Moet ik uv gebruiken in plaats van venv en pip?

Gebruik uv wanneer u een lockfile wilt, wanneer de installatietijd traag genoeg is om hinderlijk te zijn, of wanneer u een Python-versie nodig heeft die uw distributie niet aanbiedt. Het maakt een standaard venv aan, dus de systemd-unit en de bestandsstructuur veranderen niet, en uv sync --frozen installeert exact wat in de lockfile is vastgelegd. Als een enkele applicatie wordt uitgerold vanuit git met een vastgepinde requirements.txt en de installatie is binnen enkele seconden klaar, dan is python3 -m venv al voldoende en heeft u één binary minder om op de server bij te houden.

#python#venv#pipx#uv#deployment