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, comoansibleouhttpie: pipx, que atribui a cada ferramenta um ambiente privado e uma ligação emPATH. - 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-venvInstale-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.txtObserve 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 lê #!/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/myappO 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 httpiepipx 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 --globalA 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 ansiblecommand -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 --versionExecutar 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.shpipx 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-devuv 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.txtO 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: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ã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.pyModuleNotFoundError: 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.targetsudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -n 50 --no-pagersystemctl 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.