SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-13

Python no servidor: venv, pipx ou uv?

No Ubuntu 24.04, pip install falha com externally-managed-environment. Saiba quando usar venv, pipx ou uv e como apontar o systemd para o ambiente.

Por que pip install falha num servidor Ubuntu novo

A escolha entre um Python venv, pipx e uv num servidor depende de uma pergunta: o que está a instalar? As dependências da aplicação devem ficar num ambiente virtual dentro do diretório da própria aplicação. As ferramentas de linha de comandos que pretende executar pelo nome devem ficar no pipx. O uv faz as duas coisas e acrescenta um lockfile, que começa a ser importante assim que uma segunda máquina precisa de construir o mesmo ambiente. Nenhuma destas opções instala no Python do sistema, porque um servidor Ubuntu atual recusa explicitamente essa operação.

Execute sudo pip install requests no Ubuntu 24.04 e o pip para antes de transferir um único ficheiro.

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.

Isto é o PEP 668 (Python enhancement proposal 668, "externally managed environments") a funcionar como previsto. Debian e Ubuntu colocam um ficheiro marcador junto do interpretador em /usr/lib/python3.12/EXTERNALLY-MANAGED, e o pip recusa escrever em qualquer interpretador que contenha esse marcador.

A regra existe por causa da ordem de sys.path. O apt instala bibliotecas em /usr/lib/python3/dist-packages. O pip, executado como root contra o interpretador do sistema, escreve em /usr/local/lib/python3.12/dist-packages, e o sistema de pacotes do Debian coloca esse diretório mais acima no caminho de pesquisa. Apresente a ordem com python3 -c 'import sys; print(sys.path)' e leia-a. Assim, a cópia escrita pelo pip sobrepõe-se à cópia instalada pelo apt, para todos os programas do servidor que executam com /usr/bin/python3, incluindo as próprias ferramentas da distribuição. O cloud-init importa requests, jinja2 e PyYAML desse interpretador. Atualize um desses pacotes com o pip, instale uma versão incompatível e algo que nunca alterou falhará no arranque seguinte com um traceback que indica um pacote cuja presença na cadeia de dependências desconhecia. O apt continua a registar a sua própria versão como instalada, por isso nada o avisa, e a correção é sudo apt reinstall python3-requests.

A regra resultante é simples. O Python do sistema pertence à distribuição. Não instale nada nele, não atualize as respetivas bibliotecas com o pip e não elimine o ficheiro EXTERNALLY-MANAGED para fazer desaparecer a mensagem. A única tarefa que deve atribuir a /usr/bin/python3 é a criação de ambientes virtuais.

venv vs pipx vs uv: regra de decisão

Escolha com base no que vai instalar, não na ferramenta sobre a qual leu mais recentemente.

  • Uma aplicação que implementa e executa como serviço, como um projeto Django ou Flask: um ambiente virtual (venv) dentro do diretório dessa aplicação.
  • Uma ferramenta de linha de comandos que quer em PATH, como ansible ou httpie: pipx, que atribui a cada ferramenta um ambiente privado e uma ligação em PATH.
  • Um projeto que precisa de um ficheiro de bloqueio, de instalações mais rápidas ou de uma versão do Python que a distribuição não disponibiliza: uv, que cria um venv normal e um ficheiro uv.lock.
  • Uma biblioteca de que uma ferramenta da distribuição precisa, e não o seu código: sudo apt install python3-<name>, a única forma suportada de adicionar algo ao interpretador do sistema.

pipx e uv tool install fazem o mesmo trabalho, por isso um sistema que já tem uv não precisa também de pipx. A framework web escolhida não altera nada neste ponto: Django e Flask num VPS diferem no que fica instalado em requirements.txt, não na forma como o ambiente envolvente é criado. Tudo abaixo usa o Ubuntu 24.04 e o respetivo Python 3.12, por isso ajuste a versão nos caminhos se a sua for diferente.

Crie o venv de cada aplicação

O Ubuntu separa o módulo venv do pacote base do Python. Por isso, numa imagem mínima, a primeira tentativa falha com uma mensagem que identifica exatamente o que está em 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

Instale-o e crie o ambiente com o utilizador que será proprietário do 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 o que não está presente: não há source nem activate. /srv/myapp/.venv/bin/pip é instalado nesse ambiente devido à localização do binário, não por causa de algo que tenha exportado para a shell. Confirme isto antes de prosseguir.

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

Isto apresenta /srv/myapp/.venv. Se apresentar /usr, está a executar o interpretador do sistema e os seus pacotes foram instalados num local não intencional.

Duas propriedades de um venv determinam o que pode fazer com ele depois. Um venv não é relocável, porque cada script em bin/ contém uma linha shebang absoluta: head -1 /srv/myapp/.venv/bin/pip#!/srv/myapp/.venv/bin/python. Se mudar o nome do diretório pai, esses scripts falham com bad interpreter: No such file or directory. Um venv também fixa o interpretador que o criou. Essa informação fica registada na linha home de /srv/myapp/.venv/pyvenv.cfg, e bin/python3 é um symlink para esse binário. Atualize a release para que python3.12 deixe de existir, o symlink ficará sem destino e o serviço falhará ao iniciar com No such file or directory. Ambos os casos têm a mesma correção: elimine o venv e crie um novo a partir de requirements.txt. A recriação demora segundos. Nunca copie um venv entre máquinas.

Onde fica o venv e quem é o proprietário

Coloque-o junto ao código em /srv/myapp/.venv e mantenha um venv por aplicação. O deployment passa a ser um único diretório, a unidade systemd recebe um caminho que nunca muda e duas aplicações nunca podem interferir uma com a outra através de uma atualização partilhada de dependências. Não coloque um venv num local onde o seu servidor web publique ficheiros diretamente, porque ele contém as dependências e, muitas vezes, a configuração.

A propriedade merece meio minuto de atenção. Faça com que um utilizador deploy seja o proprietário do código e do ambiente, e conceda à conta de serviço apenas permissões de leitura e execução.

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

O serviço pode agora importar as suas dependências, mas não pode reescrevê-las. Assim, uma falha de execução de código na aplicação web não pode substituir silenciosamente uma biblioteca no disco e persistir depois de um reinício. O mesmo princípio aplicado ao resto da máquina é abordado em executar serviços com utilizadores com privilégios mínimos.

pipx para ferramentas de linha de comandos

pipx instala aplicações, não bibliotecas. Cada ferramenta recebe o seu próprio ambiente em ~/.local/share/pipx/venvs/<name>, e os executáveis dessa ferramenta são ligados a ~/.local/bin. Assim, duas ferramentas que precisam de versões diferentes da mesma biblioteca nunca entram em conflito.

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

pipx ensurepath adiciona ~/.local/bin a PATH ao editar o ficheiro de arranque da shell. Não pode alterar a shell que já está em execução. Por isso, http: command not found logo depois da instalação normalmente significa que ainda não terminou a sessão e iniciou outra. O ~/.profile predefinido do Ubuntu adiciona ~/.local/bin apenas quando esse diretório já existe no início da sessão. É por isso que este problema ocorre uma vez numa conta nova e nunca mais ocorre.

Se indicar uma biblioteca ao pipx, ele recusa a operação com uma mensagem que começa por:

No apps associated with package requests or its dependencies.

Essa mensagem indica que está a utilizar a ferramenta errada. As bibliotecas devem ficar no venv de uma aplicação.

Num servidor, o local de instalação é o detalhe importante. Um pipx install simples coloca tudo no diretório pessoal de um utilizador. Uma unidade systemd executada como myapp não consegue aceder a esse conteúdo. Uma tarefa cron de root também não consegue. sudo também não o encontra, porque secure_path em /etc/sudoers substitui PATH por uma lista fixa. Para uma ferramenta que deve estar disponível em todo o sistema, faça uma instalação global.

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

A flag --global coloca os ambientes em /opt/pipx e liga os executáveis a /usr/local/bin. Esse diretório está no PATH predefinido e dentro de secure_path. Verifique primeiro a versão com pipx --version. O Ubuntu 24.04 disponibiliza o pipx 1.4.3, que é mais antigo que --global. Uma versão mais antiga do pipx responde com unrecognized arguments: --global. Nessa versão, defina manualmente os dois diretórios documentados:

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

command -v ansible deve imprimir /usr/local/bin/ansible. Se imprimir um caminho dentro de /home, a ferramenta foi instalada na conta de um único utilizador e nenhum serviço a encontrará.

uv quando quiser um ficheiro de bloqueio

uv é um binário único da Astral que reúne as funções de pip, venv e pip-tools, e também pode descarregar interpretadores. É suficientemente rápido para que a diferença seja visível numa VPS pequena, e escreve um ficheiro de bloqueio real.

O instalador oficial coloca uv e uvx em ~/.local/bin:

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

Executar um script encaminhado para uma shell num servidor exige alguma cautela. Fixe a versão no URL e leia o ficheiro antes de o executar:

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 também funciona quando pipx já está instalado. uv é um binário autónomo sem dependências próprias de Python, por isso copiá-lo para /usr/local/bin é uma forma válida de o disponibilizar a todos os utilizadores do sistema.

Para um projeto com um pyproject.toml, o fluxo de trabalho tem quatro comandos, e apenas o último é executado no servidor.

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

uv lock escreve uv.lock, um ficheiro de bloqueio multiplataforma que contém as versões exatas resolvidas, e deve ser submetido juntamente com o código. uv sync cria .venv na raiz do projeto para corresponder ao ficheiro de bloqueio. No servidor, --frozen é a opção relevante: a documentação define-a como a utilização das versões no ficheiro de bloqueio como fonte de verdade, em vez de verificar se o ficheiro de bloqueio está atualizado. Esse é o comportamento pretendido numa implementação. --no-dev exclui o grupo de dependências de desenvolvimento.

Um projeto requirements.txt existente não precisa de conversão, porque uv é compatível com a sintaxe do pip:

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

O resultado é um ambiente virtual normal. .venv/bin/python comporta-se exatamente como se python3 -m venv o tivesse criado, por isso nada muda no restante deste guia.

Antes de usar uv num servidor, é importante conhecer uma das suas predefinições. A definição python-preference usa managed por predefinição. A documentação indica que isto dá prioridade aos interpretadores "descarregados e instalados pelo uv" em vez dos interpretadores já presentes no sistema. Assim, uv venv --python 3.13 num sistema que tenha apenas 3.12 descarrega silenciosamente o 3.13 para ~/.local/share/uv/python em vez de falhar. Isto é conveniente num computador portátil, mas pode ser surpreendente num servidor, porque o serviço passa a depender de um interpretador guardado num diretório pessoal que apt upgrade nunca irá atualizar. Defina python-preference como only-system em uv.toml se quiser usar o interpretador da distribuição. Se quiser o ambiente noutro local que não a raiz do projeto, UV_PROJECT_ENVIRONMENT especifica o diretório a usar para o ambiente virtual do projeto.

Aponte o systemd para o interpretador do venv, não para o activate

É aqui que a maioria das implementações Python falha. A causa é uma interpretação incorreta do que activate faz.

bin/activate é um script de shell. Ele acrescenta o diretório bin do venv ao início de PATH, define VIRTUAL_ENV, guarda os valores antigos para que deactivate os possa restaurar e altera o prompt. Não contém nada que o próprio interpretador leia. A ativação é apenas uma conveniência para uma pessoa que escreve python num prompt.

O que seleciona efetivamente o ambiente é o ficheiro do interpretador que executa. Quando /srv/myapp/.venv/bin/python arranca, o módulo Python site procura um ficheiro pyvenv.cfg no diretório que contém o executável e no diretório imediatamente acima. Encontrar /srv/myapp/.venv/pyvenv.cfg define sys.prefix como o venv, o que coloca o site-packages desse venv em sys.path. Esse é todo o mecanismo. Não precisa de uma variável de ambiente nem de um shell.

Por isso, esta unidade 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 não é uma linha de comando de shell. O systemd executa diretamente um programa, por isso não existe nenhum builtin source, && é passado como um argumento literal e nada é expandido.

E esta unidade arranca e termina logo depois:

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

/usr/bin/python3 é o interpretador do sistema, e o seu sys.path nunca incluiu o seu venv. O mesmo comando funciona na sua sessão SSH apenas porque tinha ativado o venv nessa sessão. Assim, o shell resolveu python3 através de PATH para .venv/bin/python3.

Envolver o comando em /bin/bash -c 'source ... && gunicorn ...' funciona. Mas isso também coloca um shell entre o systemd e o seu processo sem qualquer benefício, quando um único caminho absoluto resolve o problema:

[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 deve apresentar active (running) com um Main PID que corresponda ao seu processo gunicorn. Se apresentar outra coisa, consulte o journal.

A linha Environment=PATH= não existe por causa de ExecStart, que já contém um caminho completo. Ela existe para os processos iniciados pela sua aplicação. Um serviço herda um PATH curto e predefinido do systemd. Por isso, o código Python que chama subprocess.run(["ffmpeg", ...]), ou um comando de gestão que executa um console script do venv, não encontra o que precisa. Colocar primeiro o diretório bin do venv é a única parte de activate que um serviço utiliza efetivamente. Verifique o que a unidade recebeu de facto com systemctl show -p Environment myapp.

A mesma regra aplica-se às tarefas agendadas. O cron executa os trabalhos com um PATH de /usr/bin:/bin. Assim, uma linha do crontab que contenha python3 /srv/myapp/cleanup.py executa o interpretador do sistema e falha com ModuleNotFoundError às três da manhã. O erro é enviado para uma fila de correio local que ninguém consulta. Escreva também aí o caminho absoluto do venv. Para enviar essa saída para o journal e obter um registo da última execução, um par de serviço e temporizador systemd usa a mesma linha ExecStart.

O Docker substitui esta decisão?

Um contentor tem o seu próprio sistema de ficheiros, por isso a questão muda de forma em vez de desaparecer. Numa imagem oficial como python:3.12-slim, o Python está incorporado em /usr/local e não contém nenhum marcador EXTERNALLY-MANAGED, por isso pip install como root é a forma prevista de adicionar pacotes, e um venv acrescenta pouco. Em vez disso, crie FROM ubuntu:24.04 e encontrará novamente externally-managed-environment dentro da imagem, pelo mesmo motivo que no host: é o interpretador da distribuição que contém o ficheiro marcador da distribuição.

Muitas imagens continuam a usar um venv, porque isso simplifica uma compilação em várias etapas. A etapa de compilação instala em /opt/venv, e a etapa de runtime copia esse diretório e deixa os compiladores para trás. O problema da ativação também é transportado. Uma linha RUN source /opt/venv/bin/activate afeta apenas a shell dessa camada de compilação, por isso, em runtime, o contentor inicia com o interpretador do sistema e gera ModuleNotFoundError. Defina ENV PATH="/opt/venv/bin:$PATH" ou forneça a CMD o caminho absoluto /opt/venv/bin/gunicorn. É o mesmo erro que ocorre com o systemd, mas noutro ficheiro.

Assim, um contentor substitui a questão do interpretador, porque a imagem fixa o interpretador e tudo o que está abaixo dele. Não substitui a questão da fixação de versões. Uma imagem criada a partir de um requirements.txt sem versão fixada resolve versões diferentes no próximo mês. Isso significa que a tag da imagem é reproduzível, mas a compilação que a produziu não é. Um ficheiro de bloqueio como uv.lock, ou um ficheiro de requisitos com todas as versões fixadas, elimina essa diferença, com ou sem contentor. E quando uma aplicação é executada num VPS sob systemd, um contentor limita-se sobretudo a transferir esta mesma decisão para um Dockerfile, uma vez que o systemd já reinicia um processo que falhou e captura o respetivo output no journal. Executar Docker num VPS é útil quando pretende que a própria imagem criada seja o artefacto que implementa.

FAQ

Posso simplesmente usar pip install com --break-system-packages?

Não num servidor que precisa de continuar em execução. A flag faz exatamente o que indica: remove a proteção e o pip escreve em /usr/local/lib/python3.12/dist-packages, que aparece antes do diretório do apt em sys.path. A sua versão passa então a ocultar a versão da distribuição para todos os scripts do sistema executados com /usr/bin/python3, enquanto o apt continua a considerar instalada a sua própria versão. Assim, nada deteta o conflito até ocorrer uma falha. Numa imagem de contentor que reconstrói de raiz a cada execução, o problema fica limitado a essa imagem, pelo que a utilização é defensável. Numa máquina que mantém, crie um venv. Isso requer um comando.

Onde deve ficar o ambiente virtual num servidor?

Dentro do diretório da própria aplicação, em /srv/myapp/.venv, pertencente a um utilizador de deployment, com a conta de serviço a ter apenas permissões de leitura e execução. Mantenha um venv por aplicação, porque um ambiente partilhado permite que uma atualização da primeira aplicação interrompa a segunda. Não mova nem copie um venv depois de o criar: todos os scripts no diretório bin/ contêm esse caminho absoluto na linha shebang, pelo que um venv movido falha com bad interpreter: No such file or directory. Apague-o e reconstrua-o a partir de requirements.txt.

Por que motivo o meu serviço systemd falha com ModuleNotFoundError?

Porque a unidade está a executar um interpretador que não pertence ao venv. Execute systemctl cat myapp e leia ExecStart. Este deve indicar /srv/myapp/.venv/bin/python ou um console script do mesmo diretório bin/, usando o caminho absoluto. O carregamento de activate num ficheiro de unidade não pode funcionar, porque ExecStart não é uma shell, e o systemd comunica Failed to locate executable source com status=203/EXEC. Adicione Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin para que qualquer subprocesso iniciado pelo seu código também encontre as ferramentas do venv.

Devo usar uv em vez de venv e pip?

Use uv quando precisar de um lockfile, quando o tempo de instalação for suficientemente lento para ser um problema ou quando precisar de uma versão do Python que a sua distribuição não disponibiliza. O uv cria um venv normal, pelo que a unidade systemd e a disposição dos ficheiros não mudam, e uv sync --frozen instala exatamente o que o lockfile regista. Se uma única aplicação for distribuída a partir do git com um requirements.txt fixado e a instalação terminar em segundos, python3 -m venv já é suficiente, e terá menos um binário para manter atualizado no servidor.

#python#venv#pipx#uv#deployment