SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

venv vs pipx vs uv: cual elegir en servidores Ubuntu

Evita el error externally-managed-environment al instalar paquetes en Ubuntu 24.04. Aprende a gestionar dependencias con venv, pipx o uv y cómo configurar systemd para cada caso.

Por qué falla pip install en un servidor Ubuntu recién instalado

Elegir entre un venv de Python, pipx y uv en un servidor se reduce a una pregunta: ¿qué está instalando? Las dependencias de una aplicación pertenecen a un entorno virtual dentro del propio directorio de la aplicación. Las herramientas de línea de comandos que desea ejecutar por su nombre pertenecen a pipx. uv realiza ambas tareas y añade un archivo de bloqueo, lo cual empieza a ser importante en cuanto una segunda máquina debe compilar el mismo entorno. Lo que ninguno de ellos hace es instalar en el Python del sistema, porque un servidor Ubuntu actual lo rechaza directamente.

Ejecute sudo pip install requests en Ubuntu 24.04 y pip se detendrá antes de descargar un solo archivo.

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.

Esto es PEP 668 (propuesta de mejora de Python 668, "entornos gestionados externamente") haciendo su trabajo. Debian y Ubuntu colocan un archivo marcador junto al intérprete en /usr/lib/python3.12/EXTERNALLY-MANAGED, y pip se niega a escribir en cualquier intérprete que contenga uno.

La regla existe debido al orden de sys.path. apt instala bibliotecas en /usr/lib/python3/dist-packages. pip, ejecutado como root contra el intérprete del sistema, escribe en /usr/local/lib/python3.12/dist-packages, y el empaquetado de Debian coloca ese directorio antes en la ruta de búsqueda. Imprímalo usted mismo con python3 -c 'import sys; print(sys.path)' y lea el orden. Por lo tanto, la copia que escribió pip oculta la copia que instaló apt, para cada programa en el equipo que se ejecuta bajo /usr/bin/python3, incluidas las propias herramientas de la distribución. cloud-init importa requests, jinja2 y PyYAML desde ese intérprete. Actualice uno de ellos con pip, termine en una versión incompatible, y algo que nunca tocó fallará en el siguiente arranque con un rastreo que nombra un paquete que usted no sabía que estaba en la cadena. apt sigue registrando su propia versión como instalada, por lo que nada le advierte, y la reparación es sudo apt reinstall python3-requests.

La regla que sigue es breve. El Python del sistema pertenece a la distribución. No instale en él, no actualice sus bibliotecas con pip y no elimine el archivo EXTERNALLY-MANAGED para hacer desaparecer el mensaje. La única tarea que debe asignarle a /usr/bin/python3 es la creación de entornos virtuales.

venv frente a pipx frente a uv: la regla de decisión

Elija según lo que vaya a instalar, no según la herramienta sobre la que haya leído más recientemente.

  • Una aplicación que despliega y ejecuta como servicio, como un proyecto Django o Flask: un entorno virtual (venv) dentro del directorio de esa aplicación.
  • Una herramienta de línea de comandos que desea tener en su PATH, como ansible o httpie: pipx, que proporciona a cada herramienta un entorno privado y un enlace en PATH.
  • Un proyecto que requiere un archivo de bloqueo (lockfile), instalaciones más rápidas o una versión de Python que la distribución no incluye: uv, que genera un venv estándar además de un archivo uv.lock.
  • Una biblioteca que necesita una herramienta de la distribución y no su código: sudo apt install python3-<name>, la única forma admitida de añadir cualquier elemento al intérprete del sistema.

pipx y uv tool install realizan la misma función, por lo que un equipo que ya cuenta con uv no necesita además pipx. El framework web que haya elegido no cambia nada en este aspecto: Django y Flask en un VPS difieren en lo que se aloja en requirements.txt, no en cómo se construye el entorno que los rodea. Todo lo que se detalla a continuación utiliza Ubuntu 24.04 y su Python 3.12, por lo que debe ajustar la versión en las rutas si la suya es distinta.

Construir el venv por aplicación

Ubuntu separa el módulo venv del paquete base de Python, por lo que en una imagen mínima el primer intento falla con un mensaje que indica exactamente qué falta.

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

Instálelo y luego cree el entorno como el usuario que será propietario del código.

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

Observe lo que no está presente: ni source, ni activate. /srv/myapp/.venv/bin/pip se instala en ese entorno debido a la ubicación del binario, no por algo que haya exportado al shell. Confírmelo antes de continuar.

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

Eso imprime /srv/myapp/.venv. Si imprime /usr, está ejecutando el intérprete del sistema y sus paquetes se instalaron en un lugar que no pretendía.

Dos propiedades de un venv determinan lo que puede hacer con él después. Un venv no es reubicable, porque cada script en bin/ contiene una línea shebang absoluta: head -1 /srv/myapp/.venv/bin/pip lee #!/srv/myapp/.venv/bin/python. Cambie el nombre del directorio principal y esos scripts fallarán con bad interpreter: No such file or directory. Un venv también fija el intérprete que lo creó, registrado como la línea home en /srv/myapp/.venv/pyvenv.cfg, y bin/python3 es un enlace simbólico a ese binario. Si actualiza la versión de modo que python3.12 desaparezca, el enlace simbólico se queda sin destino y el servicio muere al iniciar con No such file or directory. Ambos casos tienen la misma solución: elimine el venv y cree uno nuevo desde requirements.txt. Reconstruirlo toma segundos. Nunca copie un venv entre máquinas.

Dónde reside el venv y quién es su propietario

Colóquelo junto al código en /srv/myapp/.venv y mantenga un venv por cada aplicación. El despliegue se convierte así en un único directorio, la unidad de systemd obtiene una ruta que nunca cambia y dos aplicaciones nunca podrán interferir entre sí mediante la actualización de una dependencia compartida. No coloque un venv en ningún lugar donde su servidor web publique archivos directamente, ya que contiene sus dependencias y, a menudo, su configuración.

La propiedad merece medio minuto de atención. Permita que un usuario deploy sea el propietario del código y del entorno, y otorgue a la cuenta de servicio únicamente acceso de lectura y ejecución.

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

El servicio ahora puede importar sus dependencias pero no puede sobrescribirlas, lo que significa que un error de ejecución de código en la aplicación web no puede reemplazar silenciosamente una biblioteca en el disco y sobrevivir a un reinicio. El mismo razonamiento aplicado al resto de la máquina se trata en ejecución de servicios con usuarios de privilegios mínimos.

pipx para herramientas de línea de comandos

pipx instala aplicaciones, no bibliotecas. Cada herramienta obtiene su propio entorno bajo ~/.local/share/pipx/venvs/<name>, y los ejecutables de dicha herramienta se enlazan en ~/.local/bin, por lo que dos herramientas que requieran versiones distintas de la misma biblioteca nunca entran en conflicto.

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

pipx ensurepath añade ~/.local/bin a PATH editando el archivo de inicio de su shell. No puede modificar el shell en el que ya se encuentra, por lo que http: command not found justo después de la instalación suele significar que aún no ha cerrado y vuelto a abrir la sesión. El ~/.profile predeterminado de Ubuntu añade ~/.local/bin solo cuando ese directorio ya existe al iniciar sesión, razón por la cual esto ocurre una vez en una cuenta nueva y nunca más.

Si apunta pipx a una biblioteca, este se negará con un mensaje que comienza así:

No apps associated with package requests or its dependencies.

Es la herramienta indicándole que es el instrumento equivocado. Las bibliotecas pertenecen al venv de una aplicación.

El detalle que importa en un servidor es la ubicación. Un pipx install simple coloca todo bajo el directorio home de un usuario. Una unidad de systemd ejecutándose como myapp no puede verlo, un trabajo de cron de root no puede verlo, y sudo tampoco lo encontrará, porque secure_path en /etc/sudoers reemplaza PATH con una lista fija. Para una herramienta que toda la máquina deba tener, instálela globalmente.

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

El flag --global coloca los entornos en /opt/pipx y enlaza los ejecutables en /usr/local/bin, el cual está en el PATH predeterminado y dentro de secure_path. Compruebe primero su versión con pipx --version, ya que Ubuntu 24.04 empaqueta pipx 1.4.3, que es más antiguo que --global, y un pipx antiguo responde con unrecognized arguments: --global. En esa versión, configure usted mismo los dos directorios documentados:

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

command -v ansible debería imprimir /usr/local/bin/ansible. Si imprime una ruta bajo /home, la herramienta se instaló en la cuenta de un solo usuario y ningún servicio la encontrará.

uv cuando necesita un archivo de bloqueo

uv es un binario único de Astral que cubre las funciones de pip, venv y pip-tools, además de poder descargar intérpretes. Es lo suficientemente rápido como para que la diferencia sea notable en un VPS pequeño y genera un archivo de bloqueo real.

El instalador oficial coloca uv y uvx en ~/.local/bin:

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

Enviar un script a una shell en un servidor merece un momento de precaución. Fije la versión en la URL y lea el archivo antes de ejecutarlo:

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 también funciona si ya tiene instalado pipx. uv es un binario autónomo sin dependencias de Python, por lo que copiarlo en /usr/local/bin es una forma válida de compartirlo con todos los usuarios del sistema.

Para un proyecto con un pyproject.toml, el flujo de trabajo consta de cuatro comandos, y solo el último se ejecuta en el servidor.

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

uv lock escribe uv.lock, un archivo de bloqueo multiplataforma que contiene las versiones exactas resueltas, el cual debe incluir en su repositorio junto al código. uv sync construye .venv en la raíz del proyecto para que coincida. En el servidor, --frozen es el flag relevante: la documentación lo define como el uso de las versiones del archivo de bloqueo como fuente de verdad, en lugar de verificar si el archivo está actualizado, que es el comportamiento esperado en un despliegue. --no-dev excluye el grupo de dependencias de desarrollo.

Un proyecto requirements.txt existente no requiere conversión, ya que uv utiliza el lenguaje de pip:

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

El resultado es un entorno virtual convencional. .venv/bin/python se comporta exactamente igual que si lo hubiera creado python3 -m venv, por lo que nada de lo expuesto más adelante en esta guía cambia.

Conviene conocer un valor predeterminado de uv antes de usarlo en un servidor. Su configuración python-preference tiene como valor por defecto managed, documentado como la elección de "aquellos descargados e instalados por uv" por encima de los intérpretes ya presentes en el sistema. Por lo tanto, uv venv --python 3.13 en una máquina que solo incluye la versión 3.12 descargará silenciosamente la 3.13 en ~/.local/share/uv/python en lugar de fallar. Esto es práctico en un equipo portátil, pero sorprendente en un servidor, ya que su servicio dependerá ahora de un intérprete alojado en un directorio personal que apt upgrade nunca actualizará. Establezca python-preference en only-system en uv.toml si desea utilizar el intérprete de la distribución. Si desea que el entorno se ubique en un lugar distinto a la raíz del proyecto, UV_PROJECT_ENVIRONMENT especifica el directorio que se debe usar para el entorno virtual del proyecto.

Apunte systemd al intérprete del venv, no a activate

Aquí es donde fallan la mayoría de los despliegues de Python, debido a una interpretación errónea de lo que hace activate.

bin/activate es un script de shell. Antepone el directorio bin del venv a PATH, establece VIRTUAL_ENV, guarda los valores antiguos para que deactivate pueda restaurarlos y cambia el prompt. No contiene nada que el propio intérprete lea. La activación es una comodidad para un humano que escribe python en un prompt.

Lo que realmente selecciona el entorno es qué archivo de intérprete se ejecuta. Cuando /srv/myapp/.venv/bin/python arranca, el módulo site de Python busca un archivo pyvenv.cfg en el directorio que contiene el ejecutable y en el nivel inmediatamente superior. Al encontrar /srv/myapp/.venv/pyvenv.cfg, se establece sys.prefix al venv, lo que coloca el directorio site-packages de ese venv en sys.path. Ese es todo el mecanismo. No necesita ninguna variable de entorno ni shell.

Por tanto, esta unidad nunca arranca:

[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 no es una línea de comandos de shell. systemd ejecuta un programa directamente, por lo que no existe el builtin source, && se pasa como un argumento literal y no se expande nada.

Y esta unidad arranca, pero muere inmediatamente:

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

/usr/bin/python3 es el intérprete del sistema y su sys.path nunca ha contenido su venv. El mismo comando funciona en su sesión SSH solo porque usted había activado el venv allí, por lo que el shell resolvió python3 a través de PATH hacia .venv/bin/python3.

Envolver el comando en /bin/bash -c 'source ... && gunicorn ...' funciona. Sin embargo, interpone un shell entre systemd y su proceso sin obtener beneficio alguno, cuando una ruta absoluta lo resuelve:

[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 debería informar active (running) con un Main PID que sea su proceso gunicorn. Si muestra otra cosa, consulte el journal.

La línea Environment=PATH= no está ahí para ExecStart, que ya incluye una ruta completa. Está ahí para los procesos que inicia su aplicación. Un servicio hereda un PATH predeterminado corto de systemd, por lo que el código Python que llama a subprocess.run(["ffmpeg", ...]), o un comando de gestión que invoca un script de consola desde el venv, no encontrará lo que necesita. Colocar el directorio bin del venv al principio es la única parte de activate que un servicio utiliza realmente. Compruebe lo que recibió la unidad con systemctl show -p Environment myapp.

La misma regla se aplica al trabajo programado. cron ejecuta tareas con un PATH de /usr/bin:/bin, por lo que una línea de crontab que diga python3 /srv/myapp/cleanup.py ejecuta el intérprete del sistema y falla con ModuleNotFoundError a las tres de la mañana, enviando el error a un spool de correo local que nadie revisa. Escriba también ahí la ruta absoluta del venv. Para obtener esa salida en el journal y un registro de la última ejecución, un par de servicio y temporizador de systemd utiliza la misma línea ExecStart.

¿Reemplaza Docker esta decisión?

Un contenedor tiene su propio sistema de archivos, por lo que la pregunta cambia de forma en lugar de desaparecer. En una imagen oficial como python:3.12-slim, Python está integrado en /usr/local y no contiene ningún marcador EXTERNALLY-MANAGED, por lo que pip install como root es la forma prevista para añadir paquetes y un venv aporta poco. Si construye FROM ubuntu:24.04, se encontrará con externally-managed-environment de nuevo dentro de la imagen, por la misma razón que en el host: es el intérprete de la distribución que contiene el archivo marcador de la distribución.

Muchas imágenes siguen utilizando un venv porque simplifica la compilación multietapa. La etapa de compilación instala en /opt/venv y la etapa de ejecución copia ese único directorio y deja atrás los compiladores. El problema de la activación viaja con él. Una línea RUN source /opt/venv/bin/activate afecta solo al shell de esa capa de compilación, por lo que, en tiempo de ejecución, el contenedor se inicia con el intérprete del sistema y genera ModuleNotFoundError. Establezca ENV PATH="/opt/venv/bin:$PATH" o proporcione a CMD la ruta absoluta /opt/venv/bin/gunicorn. Es el mismo error que el de systemd, pero en un archivo diferente.

Por lo tanto, un contenedor reemplaza la pregunta sobre el intérprete, ya que la imagen fija el intérprete y todo lo que contiene. No reemplaza la pregunta sobre la fijación de versiones. Una imagen construida a partir de un requirements.txt sin fijar resuelve versiones diferentes el próximo mes, lo que significa que la etiqueta de la imagen es reproducible, pero la compilación que la produjo no lo es. Un archivo de bloqueo como uv.lock, o un archivo de requisitos totalmente fijado, es lo que cierra esa brecha, con o sin contenedor. Y cuando una aplicación se ejecuta en un VPS bajo systemd, un contenedor traslada principalmente esta misma decisión a un Dockerfile, ya que systemd ya reinicia un proceso fallido y captura su salida en el journal. Ejecutar Docker en un VPS vale la pena cuando desea que la propia imagen compilada sea lo que usted despliega.

FAQ

¿Puedo usar simplemente pip install con --break-system-packages?

No en un servidor que debe mantenerse en funcionamiento. El flag hace exactamente lo que indica: elimina la protección y pip escribe en /usr/local/lib/python3.12/dist-packages, que tiene prioridad sobre el directorio de apt en sys.path. Su versión entonces oculta la de la distribución para cualquier script del sistema que se ejecute bajo /usr/bin/python3, y apt sigue creyendo que su propia versión está instalada, por lo que nadie detecta el conflicto hasta que algo falla. Dentro de una imagen de contenedor que se reconstruye desde cero cada vez, el daño se limita a esa imagen, por lo que es justificable en ese caso. En una máquina que usted mantiene, cree un venv. Es solo un comando.

¿Dónde debería residir el entorno virtual en un servidor?

Dentro del propio directorio de la aplicación, como /srv/myapp/.venv, propiedad de un usuario de despliegue, con la cuenta de servicio teniendo solo acceso de lectura y ejecución. Mantenga un venv por aplicación, ya que uno compartido implica que una actualización para la primera aplicación puede romper la segunda. No mueva ni copie un venv después de crearlo: cada script en su directorio bin/ tiene esa ruta absoluta escrita en su línea shebang, por lo que un venv movido fallará con bad interpreter: No such file or directory. Elimínelo y reconstruya desde requirements.txt en su lugar.

¿Por qué mi servicio systemd falla con ModuleNotFoundError?

Porque la unidad está ejecutando un intérprete que no es el del venv. Ejecute systemctl cat myapp y lea ExecStart. Debe indicar /srv/myapp/.venv/bin/python, o un script de consola de ese mismo directorio bin/, mediante una ruta absoluta. Ejecutar activate en un archivo de unidad no funciona, porque ExecStart no es un shell y systemd informa Failed to locate executable source con status=203/EXEC. Añada Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin para que cualquier subproceso que inicie su código encuentre también las herramientas del venv.

¿Debería usar uv en lugar de venv y pip?

Use uv cuando necesite un archivo de bloqueo (lockfile), cuando el tiempo de instalación sea lo suficientemente lento como para molestarle, o cuando necesite una versión de Python que su distribución no ofrece. Crea un venv estándar, por lo que la unidad de systemd y la estructura de archivos no cambian, y uv sync --frozen instala exactamente lo que registra el archivo de bloqueo. Si una sola aplicación se despliega desde git con un requirements.txt fijado y la instalación termina en segundos, python3 -m venv ya es suficiente, y es un binario menos que mantener actualizado en el servidor.

#python#venv#pipx#uv#deployment