Python su server: venv, pipx o uv?
Su Ubuntu 24.04 pip install restituisce "externally-managed-environment": ecco quando scegliere venv, pipx o uv e come collegarlo a systemd.
Perché pip install non funziona su un server Ubuntu appena installato
La scelta tra un Python venv, pipx e uv su un server dipende da una sola domanda: che cosa si sta installando? Le dipendenze di un'applicazione appartengono a un ambiente virtuale nella directory dell'applicazione. Gli strumenti da riga di comando che si vogliono eseguire per nome appartengono a pipx. uv svolge entrambi i compiti e aggiunge un lockfile, che diventa utile non appena una seconda macchina deve creare lo stesso ambiente. Nessuno di questi strumenti installa pacchetti nel Python di sistema, perché un server Ubuntu aggiornato rifiuta esplicitamente questa operazione.
Eseguire sudo pip install requests su Ubuntu 24.04 fa sì che pip si interrompa prima di scaricare anche un solo file.
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.Questo è il comportamento previsto da PEP 668 (Python enhancement proposal 668, "externally managed environments"). Debian e Ubuntu inseriscono un file indicatore accanto all'interprete in /usr/lib/python3.12/EXTERNALLY-MANAGED e pip rifiuta di scrivere in qualsiasi interprete che lo contiene.
La regola esiste a causa dell'ordine di sys.path. apt installa le librerie in /usr/lib/python3/dist-packages. pip, eseguito come root sull'interprete di sistema, scrive in /usr/local/lib/python3.12/dist-packages e il sistema di pacchettizzazione Debian inserisce questa directory prima nel percorso di ricerca. È possibile visualizzare direttamente l'ordine con python3 -c 'import sys; print(sys.path)'. In questo modo, la copia scritta da pip nasconde quella installata da apt per ogni programma del server eseguito con /usr/bin/python3, inclusi gli strumenti della distribuzione. cloud-init importa requests, jinja2 e PyYAML da quell'interprete. Se si aggiorna uno di questi pacchetti con pip e si installa una release incompatibile, al riavvio successivo può non funzionare un componente che non era stato modificato, mostrando un traceback con il nome di un pacchetto di cui non si sapeva l'esistenza nella catena delle dipendenze. apt continua a registrare come installata la propria versione, quindi non viene visualizzato alcun avviso e la correzione consiste in sudo apt reinstall python3-requests.
La regola da seguire è semplice. Il Python di sistema appartiene alla distribuzione. Non installare pacchetti al suo interno, non aggiornare le relative librerie con pip e non eliminare il file EXTERNALLY-MANAGED per nascondere il messaggio. L'unico compito da assegnare a /usr/bin/python3 è la creazione di ambienti virtuali.
venv, pipx o uv: criterio per la scelta
Scegli in base a ciò che devi installare, non in base allo strumento di cui hai letto più recentemente.
- Un'applicazione che distribuisci ed esegui come servizio, ad esempio un progetto Django o Flask: un ambiente virtuale (venv) nella directory dell'applicazione.
- Uno strumento da riga di comando che vuoi su
PATH, ad esempioansibleohttpie: pipx, che assegna a ogni strumento un ambiente privato e crea un unico collegamento inPATH. - Un progetto che richiede un file di lock, installazioni più rapide o una versione di Python che la distribuzione non fornisce: uv, che crea un normale venv oltre a un file
uv.lock. - Una libreria richiesta da uno strumento della distribuzione e non dal tuo codice:
sudo apt install python3-<name>, l'unico metodo supportato per aggiungere elementi all'interprete di sistema.
pipx e uv tool install svolgono lo stesso compito, quindi un server che dispone già di uv non ha bisogno anche di pipx. Il framework web scelto non cambia nulla in questo caso: Django e Flask su un VPS differiscono per ciò che viene installato in requirements.txt, non per il modo in cui viene creato l'ambiente circostante. Tutti gli esempi seguenti usano Ubuntu 24.04 e Python 3.12 incluso nella distribuzione; se la tua versione è diversa, modifica la versione nei percorsi.
Creare il venv per l'applicazione
Ubuntu separa il modulo venv dal pacchetto Python di base. In un'immagine minimale, quindi, il primo tentativo non riesce e il messaggio indica esattamente ciò che manca.
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-venvInstallalo, quindi crea l'ambiente con l'utente proprietario del codice.
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.txtNota ciò che non è presente: nessun source e nessun activate. /srv/myapp/.venv/bin/pip viene installato in quell'ambiente in base alla posizione del binario, non in base a variabili esportate nella shell. Verificalo prima di procedere.
/srv/myapp/.venv/bin/python -c 'import sys; print(sys.prefix)'Il comando restituisce /srv/myapp/.venv. Se restituisce /usr, stai usando l'interprete di sistema e i pacchetti sono stati installati in una posizione diversa da quella prevista.
Due proprietà di un venv determinano le operazioni che puoi eseguire in seguito. Un venv non è rilocabile, perché ogni script in bin/ contiene una riga shebang con un percorso assoluto: head -1 /srv/myapp/.venv/bin/pip legge #!/srv/myapp/.venv/bin/python. Se rinomini la directory padre, gli script non funzionano più e restituiscono bad interpreter: No such file or directory. Un venv mantiene inoltre l'interprete che lo ha creato. Questo è registrato nella riga home di /srv/myapp/.venv/pyvenv.cfg, mentre bin/python3 è un symlink a quel binario. Aggiorna la release in modo che python3.12 non esista più: il symlink non avrà alcuna destinazione e il servizio non si avvierà, restituendo No such file or directory. In entrambi i casi la soluzione è la stessa: elimina il venv e creane uno nuovo usando requirements.txt. La ricreazione richiede pochi secondi. Non copiare mai un venv tra macchine.
Dove si trova il venv e chi ne è il proprietario
Posizionalo accanto al codice in /srv/myapp/.venv e mantieni un solo venv per applicazione. In questo modo il deployment consiste in un'unica directory, l'unità systemd utilizza un percorso invariabile e due applicazioni non possono mai interferire tra loro a causa dell'aggiornamento condiviso di una dipendenza. Non posizionare un venv in una directory che il server Web pubblica direttamente, perché contiene le dipendenze e spesso anche la configurazione.
La proprietà richiede una verifica di pochi secondi. Assegna a un utente deploy la proprietà del codice e dell'ambiente, quindi concedi all'account di servizio soltanto i permessi di lettura ed esecuzione.
sudo adduser --system --group --no-create-home myapp
sudo chown -R deploy:myapp /srv/myapp
sudo chmod -R o-rwx /srv/myappIl servizio può ora importare le proprie dipendenze, ma non può sovrascriverle. Di conseguenza, una vulnerabilità di esecuzione del codice nell'applicazione Web non può sostituire di nascosto una libreria sul disco e mantenerla dopo un riavvio. Lo stesso principio, applicato al resto della macchina, è descritto in eseguire i servizi con utenti dotati del minimo privilegio.
pipx per gli strumenti a riga di comando
pipx installa applicazioni, non librerie. Ogni strumento riceve un ambiente dedicato in ~/.local/share/pipx/venvs/<name> e gli eseguibili dello strumento vengono collegati in ~/.local/bin. In questo modo, due strumenti che richiedono versioni diverse della stessa libreria non entrano in conflitto.
sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install httpiepipx ensurepath aggiunge ~/.local/bin a PATH modificando il file di avvio della shell. Non può modificare la shell già in uso. Per questo, se http: command not found viene visualizzato subito dopo l'installazione, in genere significa che non è ancora stato eseguito il logout e un nuovo login. Il valore predefinito di ~/.profile in Ubuntu aggiunge ~/.local/bin soltanto se quella directory esiste già al momento del login. Per questo il problema si verifica una sola volta per un nuovo account.
Se si indica a pipx una libreria, pipx rifiuta l'operazione con un messaggio che inizia con:
No apps associated with package requests or its dependencies.Il messaggio indica che si sta usando lo strumento sbagliato. Le librerie devono essere installate nel venv dell'applicazione.
Su un server, il percorso di installazione è l'aspetto rilevante. Un semplice pipx install installa tutto nella home directory di un singolo utente. Un'unità systemd eseguita come myapp non può accedervi. Anche un job cron eseguito da root non può accedervi. Inoltre, sudo non lo troverà, perché secure_path in /etc/sudoers sostituisce PATH con un elenco fisso. Se uno strumento deve essere disponibile per l'intero sistema, installarlo globalmente.
sudo pipx install --global ansible
sudo pipx ensurepath --globalIl flag --global inserisce gli ambienti in /opt/pipx e collega gli eseguibili in /usr/local/bin. Questa directory è inclusa nel valore predefinito di PATH e in secure_path. Controllare prima la versione con pipx --version. Ubuntu 24.04 include pipx 1.4.3, che è precedente a --global, e una versione precedente di pipx restituisce unrecognized arguments: --global. Con quella versione, impostare manualmente le due directory documentate:
sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible
command -v ansiblecommand -v ansible dovrebbe stampare /usr/local/bin/ansible. Se stampa un percorso sotto /home, lo strumento è stato installato nell'account di un singolo utente e nessun servizio potrà trovarlo.
uv quando serve un lockfile
uv è un singolo binario di Astral che riunisce le funzionalità di pip, venv e pip-tools e che può anche scaricare gli interpreti. È abbastanza veloce da rendere evidente la differenza su un VPS di piccole dimensioni e scrive un vero lockfile.
L'installer ufficiale installa uv e uvx in ~/.local/bin:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --versionPrima di eseguire su un server uno script passato a una shell, è opportuno prestare attenzione. Fissa la versione nell'URL e leggi il file prima di eseguirlo:
curl -LsSf https://astral.sh/uv/0.12.3/install.sh -o uv-install.sh
less uv-install.sh
sh uv-install.shFunziona anche pipx install uv, se pipx è già installato. uv è un singolo binario autonomo che non ha dipendenze Python proprie, quindi copiarlo in /usr/local/bin è un modo valido per condividerlo con tutti gli utenti del server.
Per un progetto con un pyproject.toml, il flusso di lavoro richiede quattro comandi e solo l'ultimo viene eseguito sul server.
uv init myapp
uv add flask gunicorn
uv lock
uv sync --frozen --no-devuv lock scrive uv.lock, un lockfile multipiattaforma che contiene le versioni esatte risolte, e va inserito nel commit insieme al codice. uv sync crea .venv nella root del progetto per allinearlo al lockfile. Sul server, --frozen è il flag importante: la documentazione lo definisce come l'uso delle versioni nel lockfile come fonte di verità, senza verificare se il lockfile è aggiornato. Questo è il comportamento richiesto da un deployment. --no-dev esclude il gruppo delle dipendenze di sviluppo.
Un progetto requirements.txt esistente non richiede conversioni, perché uv usa la sintassi di pip:
uv venv /srv/myapp/.venv
uv pip install --python /srv/myapp/.venv/bin/python -r /srv/myapp/requirements.txtIl risultato è un normale ambiente virtuale. .venv/bin/python si comporta esattamente come se python3 -m venv lo avesse creato, quindi il resto di questa guida non cambia.
Prima di usare uv su un server, è utile conoscere una delle sue impostazioni predefinite. L'impostazione python-preference ha come valore predefinito managed. La documentazione indica che questo valore dà la preferenza agli interpreti "scaricati e installati da uv" rispetto a quelli già presenti nel sistema. Di conseguenza, uv venv --python 3.13 su un server che dispone soltanto di 3.12 scarica silenziosamente 3.13 in ~/.local/share/uv/python invece di terminare con un errore. Su un laptop è una scelta comoda, ma su un server è sorprendente, perché il servizio dipende da un interprete presente nella home directory e che apt upgrade non aggiornerà mai. Imposta python-preference su only-system in uv.toml se vuoi usare l'interprete della distribuzione. Se vuoi collocare l'ambiente in una directory diversa dalla root del progetto, UV_PROJECT_ENVIRONMENT specifica la directory da usare per l'ambiente virtuale del progetto.
Indicare a systemd l'interprete del venv, non activate
È qui che la maggior parte dei deployment Python si interrompe. La causa è un'interpretazione errata di ciò che fa activate.
bin/activate è uno script shell. Anteppone la directory bin del venv a PATH, imposta VIRTUAL_ENV, salva i valori precedenti affinché deactivate possa ripristinarli e modifica il prompt. Non contiene nulla che l'interprete legga direttamente. L'attivazione è solo una comodità per chi digita python in una shell.
A selezionare realmente l'ambiente è il file dell'interprete che viene eseguito. Quando si avvia /srv/myapp/.venv/bin/python, il modulo Python site cerca un file pyvenv.cfg nella directory che contiene l'eseguibile e nella directory immediatamente superiore. Se trova /srv/myapp/.venv/pyvenv.cfg, imposta sys.prefix sul venv e inserisce il relativo site-packages in sys.path. Questo è l'intero meccanismo. Non servono variabili d'ambiente né una shell.
Quindi questa unità non viene mai avviata:
[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 non è una riga di comando della shell. systemd esegue direttamente un programma, quindi non esiste alcun builtin source, && viene passato come argomento letterale e non viene eseguita alcuna espansione.
Questa unità invece si avvia e poi termina:
[Service]
ExecStart=/usr/bin/python3 /srv/myapp/app.pyModuleNotFoundError: No module named 'flask'/usr/bin/python3 è l'interprete di sistema e il suo sys.path non ha mai incluso il venv. Lo stesso comando funziona nella sessione SSH solo perché il venv era stato attivato e la shell aveva quindi risolto python3 tramite PATH in .venv/bin/python3.
Racchiudere il comando in /bin/bash -c 'source ... && gunicorn ...' funziona. Tuttavia inserisce inutilmente una shell tra systemd e il processo. È sufficiente un percorso assoluto:
[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 dovrebbe restituire active (running) con un Main PID corrispondente al processo gunicorn. In caso contrario, consultare il journal.
La riga Environment=PATH= non serve a ExecStart, che contiene già un percorso completo. Serve ai processi avviati dall'applicazione. Un servizio eredita da systemd un PATH predefinito e ridotto. Di conseguenza, il codice Python che invoca subprocess.run(["ffmpeg", ...]), oppure un comando di gestione che esegue uno script console dal venv, non trova ciò che gli serve. Inserire per prima la directory bin del venv è l'unica parte di activate che il servizio utilizza realmente. Verificare ciò che l'unità ha ricevuto con systemctl show -p Environment myapp.
La stessa regola vale per le attività pianificate. cron esegue i job con un PATH pari a /usr/bin:/bin. Una riga crontab che contiene python3 /srv/myapp/cleanup.py esegue quindi l'interprete di sistema e alle tre del mattino termina con ModuleNotFoundError. L'errore viene inviato a una casella di posta locale che nessuno controlla. Inserire anche qui il percorso assoluto del venv. Per inviare quell'output al journal e conservare invece un record dell'ultima esecuzione, una coppia composta da un servizio e un timer systemd usa la stessa riga ExecStart.
Docker sostituisce questa decisione?
Un container ha un filesystem proprio, quindi la questione cambia forma invece di scomparire. In un'immagine ufficiale come python:3.12-slim, Python è integrato in /usr/local e non contiene alcun indicatore EXTERNALLY-MANAGED. Per questo, pip install come root è il metodo previsto per aggiungere pacchetti e un venv offre pochi vantaggi. Eseguite invece il build di FROM ubuntu:24.04 e incontrerete di nuovo externally-managed-environment all'interno dell'immagine, per lo stesso motivo che vale sull'host: è l'interprete della distribuzione che contiene il file indicatore della distribuzione.
Molte immagini usano comunque un venv, perché semplifica una build a più stage. Lo stage builder installa i pacchetti in /opt/venv e lo stage runtime copia quella directory, lasciando indietro i compilatori. Anche il problema dell'attivazione viene trasferito. Una riga RUN source /opt/venv/bin/activate modifica soltanto la shell del layer di build, quindi, all'avvio, il container usa l'interprete di sistema e genera ModuleNotFoundError. Impostate ENV PATH="/opt/venv/bin:$PATH" oppure fornite a CMD il percorso assoluto /opt/venv/bin/gunicorn. È lo stesso errore visto con systemd, ma in un file diverso.
Un container sostituisce quindi la questione dell'interprete, perché l'immagine fissa l'interprete e tutto ciò che dipende da esso. Non sostituisce però la questione del pinning. Un'immagine compilata da un requirements.txt senza versioni fissate risolve versioni diverse il mese prossimo. Il tag dell'immagine è quindi riproducibile, ma la build che l'ha prodotto non lo è. Un lockfile come uv.lock, oppure un file requirements con tutte le versioni fissate, elimina questa differenza, dentro o fuori da un container. Quando una sola applicazione viene eseguita su un VPS tramite systemd, un container trasferisce soprattutto questa stessa decisione in un Dockerfile, perché systemd riavvia già un processo che termina con errore e ne acquisisce l'output nel journal. Eseguire Docker su un VPS è utile quando volete che sia l'immagine compilata stessa l'elemento da distribuire.
FAQ
Posso usare semplicemente pip install con --break-system-packages?
Non su un server che deve rimanere operativo. Il flag fa esattamente ciò che indica: rimuove la protezione e pip scrive in /usr/local/lib/python3.12/dist-packages, che viene prima della directory apt in sys.path. La versione installata sovrascrive quindi quella della distribuzione per ogni script di sistema eseguito con /usr/bin/python3. apt continua a considerare installata la propria versione, quindi il conflitto non viene rilevato finché qualcosa non smette di funzionare. In un'immagine container che ricostruisci da zero ogni volta, il problema resta confinato a quell'immagine, quindi questa scelta può essere giustificata. Su una macchina che gestisci, crea un venv. Basta un comando.
Dove deve trovarsi l'ambiente virtuale su un server?
Nella directory dell'applicazione, ad esempio /srv/myapp/.venv, con il proprietario impostato sull'utente deploy e l'account del servizio autorizzato soltanto alla lettura e all'esecuzione. Usa un venv per ogni applicazione, perché un ambiente condiviso consente a un aggiornamento della prima applicazione di compromettere la seconda. Non spostare né copiare un venv dopo averlo creato: ogni script nella relativa directory bin/ contiene quel percorso assoluto nella riga shebang, quindi un venv spostato non funziona e restituisce bad interpreter: No such file or directory. Eliminalo e ricrealo invece da requirements.txt.
Perché il mio servizio systemd termina con ModuleNotFoundError?
Perché l'unità esegue un interprete diverso da quello del venv. Esegui systemctl cat myapp e leggi ExecStart. Deve indicare /srv/myapp/.venv/bin/python oppure, tramite percorso assoluto, uno script console presente nella stessa directory bin/. Il caricamento di activate in un file dell'unità non può funzionare, perché ExecStart non è una shell e systemd restituisce Failed to locate executable source con status=203/EXEC. Aggiungi Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin, così anche ogni sottoprocesso avviato dal codice troverà gli strumenti del venv.
Devo usare uv invece di venv e pip?
Usa uv quando vuoi un lockfile, quando i tempi di installazione sono abbastanza lunghi da creare un problema oppure quando ti serve una versione di Python che la distribuzione non fornisce. uv crea un venv ordinario, quindi l'unità systemd e la struttura delle directory non cambiano, mentre uv sync --frozen installa esattamente ciò che registra il lockfile. Se una singola applicazione viene distribuita da git con un requirements.txt bloccato e l'installazione termina in pochi secondi, python3 -m venv è già sufficiente e hai un binario in meno da mantenere aggiornato sul server.