Por que usar venv no Ubuntu 24.04 antes de pensar em Docker?
Se você quer um fluxo profissional em Python, o ponto de partida é isolar dependências no seu sistema local antes de levar qualquer coisa para o container. O venv faz exatamente isso: cria um ambiente por projeto, separando os pacotes instalados e o interpretador do ambiente do sistema. A forma padrão de criar esse ambiente é com o módulo integrado python3 -m venv <diretório>, e a documentação oficial do Python recomenda esse isolamento justamente para evitar conflito com pacotes do sistema.
Na prática, isso evita um erro clássico: você instala uma lib no Ubuntu, ela funciona no projeto A, quebra o projeto B e depois você tenta compensar isso dentro do Docker. A ordem certa é esta: primeiro o projeto fica previsível localmente com venv; depois o container replica esse comportamento de forma controlada.
Outro motivo importante é o diagnóstico. Quando o ambiente local usa a mesma versão de Python, as mesmas dependências e a mesma estrutura de projeto, fica muito mais fácil distinguir problema de código, problema de dependência ou problema de containerização. Sem esse passo, você acaba depurando três camadas ao mesmo tempo.
Quando python3 -m venv funciona localmente e o python --version muda dentro da .venv, você já eliminou uma grande classe de problemas de “funciona na minha máquina” antes mesmo de escrever o Dockerfile.
Quando python3 -m venv funciona localmente e o python --version muda dentro da .venv, você já eliminou uma grande classe de problemas de “funciona na minha máquina” antes mesmo de escrever o Dockerfile.
Como instalar o suporte a venv no Ubuntu 24.04?
No Ubuntu, normalmente você precisa instalar o pacote python3-venv para habilitar o suporte ao módulo venv. Em Ubuntu 24.04 LTS, o pacote existe no repositório oficial da distribuição; a documentação de pacotes do Noble Numbat confirma isso.
Um passo a passo real e direto para começar:
sudo apt update
sudo apt install python3 python3-venv python3-pip
Depois valide a instalação:
python3 --version
python3 -m venv --help
Se o pacote não estiver instalado, o comando de criação do ambiente virtual costuma falhar com erro de módulo indisponível. Esse é o primeiro ponto de checagem em máquina nova ou instalação limpa do Ubuntu. Não pule essa etapa, porque ela economiza tempo quando você começa a automatizar o fluxo em scripts e Dockerfiles.
Se python3 -m venv .venv falhar com erro de módulo ausente, o primeiro suspeito não é o seu projeto — é a falta do pacote python3-venv no sistema.
Se python3 -m venv .venv falhar com erro de módulo ausente, o primeiro suspeito não é o seu projeto — é a falta do pacote python3-venv no sistema.
Como criar, ativar e remover um ambiente virtual com venv?
Depois que o suporte está instalado, crie o ambiente com o comando padrão:
python3 -m venv .venv
Esse comando cria a pasta .venv no diretório atual e prepara um Python isolado para o projeto. Em seguida, ative o ambiente no shell:
source .venv/bin/activate
Ao ativar, o prompt geralmente muda e o executável python passa a apontar para o interpretador dentro de .venv. Valide isso com:
python --version
pip --version
O comportamento esperado é ver caminhos associados ao ambiente virtual, não ao sistema global. A desativação é simples:
deactivate
Se você quiser remover o ambiente, basta apagar a pasta:
rm -rf .venv
Esse fluxo é propositalmente simples porque o ambiente virtual deve ser descartável. O código do projeto fica no repositório; o ambiente pode ser recriado sempre que necessário. Essa é uma das melhores formas de manter reprodutibilidade e limpar a casa quando algo sai do lugar.
Valide a ativação com which python e which pip: ambos devem apontar para caminhos dentro de .venv. Se ainda aparecer /usr/bin/python ou um pip global, o ambiente não foi ativado corretamente.
Valide a ativação com which python e which pip: ambos devem apontar para caminhos dentro de .venv. Se ainda aparecer /usr/bin/python ou um pip global, o ambiente não foi ativado corretamente.
Qual estrutura de projeto Python moderna funciona melhor com venv?
Projetos Python modernos se beneficiam de uma estrutura previsível, especialmente com layout src/. Em vez de deixar módulos soltos na raiz, organize o projeto de forma que o código de aplicação viva em src/ e a configuração do projeto fique no nível superior.
Uma estrutura prática para este caso fica assim:
meu-projeto/
├── pyproject.toml
├── README.md
├── src/
│ └── app/
│ ├── __init__.py
│ └── main.py
└── tests/
Esse layout reduz ambiguidades de importação e deixa explícito o que é código fonte, o que é configuração e o que são testes. Em projetos reais, a diferença aparece logo quando você roda testes, monta wheel ou empacota a aplicação para container. Estrutura solta costuma funcionar no começo, mas vira uma fonte de importações acidentais, paths implícitos e comportamento diferente entre local e Docker.
Um módulo mínimo executável pode ser assim:
# src/app/main.py
def main() -> None:
print("Aplicação Python funcionando!")
if __name__ == "__main__":
main()
Teste localmente com o ambiente ativo:
python src/app/main.py
Se a saída aparece como esperado, você já tem uma base limpa para levar a mesma aplicação para o container.
Com layout src/, o teste rápido de importação deve ser executado a partir da raiz do projeto, com a .venv ativa, usando algo como python -c "from app.main import main" para validar que os imports estão corretos.
Com layout src/, o teste rápido de importação deve ser executado a partir da raiz do projeto, com a .venv ativa, usando algo como python -c "from app.main import main" para validar que os imports estão corretos.
Como organizar dependências e configuração com pyproject.toml?
O pyproject.toml é o arquivo recomendado pelo ecossistema Python moderno para declarar metadados e o build-system do projeto. Em vez de espalhar configuração em múltiplos arquivos sem padrão claro, você centraliza informações essenciais num arquivo declarativo e legível.
Um exemplo mínimo e funcional para este fluxo:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "meu-projeto"
version = "0.1.0"
description = "Exemplo de projeto Python com venv e Docker"
requires-python = ">=3.12"
dependencies = []
Se você quiser instalar o projeto em modo editável durante o desenvolvimento, pode manter a dependência de empacotamento clara e instalar o próprio projeto dentro do ambiente virtual. Isso ajuda a replicar a lógica do container depois, porque o empacotamento já foi pensado desde o começo.
Na prática, o valor do pyproject.toml é reduzir a distância entre desenvolvimento e distribuição. Você não depende de convenção informal ou de instruções na cabeça de alguém do time; a configuração fica versionada, visível e reproduzível.
Nesse arquivo, concentre nome, versão e backend de build de forma explícita; deixe as dependências declaradas de modo previsível para que o Docker copie exatamente o necessário sem arrastar configuração supérflua.
Nesse arquivo, concentre nome, versão e backend de build de forma explícita; deixe as dependências declaradas de modo previsível para que o Docker copie exatamente o necessário sem arrastar configuração supérflua.
Como transformar esse projeto em uma imagem Docker reproduzível?
Para obter uma imagem reproduzível, comece por uma imagem oficial do Python no Docker Hub com versão fixa. A documentação do Docker e a página oficial da imagem Python reforçam a importância de pinar a base para evitar mudanças implícitas. Isso significa não usar uma tag genérica demais quando a estabilidade importa.
Exemplo de base fixa:
FROM python:3.12.7-slim-bookworm
Ao fixar a versão, você controla melhor a origem do runtime. Em seguida, use cache com consciência: copie primeiro apenas os arquivos de dependência e metadados, instale o que for necessário, e só depois copie o código-fonte. Essa ordem permite que mudanças pequenas no código não invalidem todo o cache de dependências a cada build.
Também é nesse momento que faz sentido separar ambiente de build e ambiente de execução. Em vez de carregar tudo para a imagem final, você constrói em um estágio e entrega só o resultado mínimo no estágio final. Essa abordagem é a base de um Dockerfile realmente profissional para Python.
Pinar a imagem base por versão evita que um rebuild “igual” produza resultados diferentes semanas depois, especialmente em pipelines CI que usam cache.
Pinar a imagem base por versão evita que um rebuild “igual” produza resultados diferentes semanas depois, especialmente em pipelines CI que usam cache.
Como escrever um Dockerfile profissional para Python com multi-stage build?
Multi-stage builds permitem manter artefatos de build fora da imagem final, reduzindo tamanho e superfície de ataque. Para um projeto Python com venv, uma forma prática é criar o ambiente virtual no estágio de build, instalar dependências ali e copiar apenas o necessário para o runtime.
Exemplo completo:
# syntax=docker/dockerfile:1
FROM python:3.12.7-slim-bookworm AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
VIRTUAL_ENV=/opt/venv
WORKDIR /app
RUN python -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY pyproject.toml /app/
COPY src /app/src
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir .
FROM python:3.12.7-slim-bookworm AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:$PATH"
WORKDIR /app
RUN useradd --create-home --shell /bin/bash appuser
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /app /app
USER appuser
CMD ["python", "-m", "app.main"]
Esse desenho traz três benefícios concretos. Primeiro, a instalação de dependências acontece no estágio de build. Segundo, o runtime recebe apenas o ambiente pronto, sem precisar de ferramentas de compilação. Terceiro, você executa a aplicação como usuário não root no container final.
O uso de pip install --no-cache-dir ajuda a não deixar resíduos de cache desnecessários na imagem. Além disso, a cópia do código e do ambiente fica explícita, o que facilita revisão de segurança e debugging.
O estágio de build pode incluir gcc, headers e ferramentas de compilação quando forem necessários; o estágio final não deve carregá-los, porque isso aumenta tamanho, tempo de pull e superfície de ataque.
O estágio de build pode incluir gcc, headers e ferramentas de compilação quando forem necessários; o estágio final não deve carregá-los, porque isso aumenta tamanho, tempo de pull e superfície de ataque.
Como deixar a imagem final menor e mais segura?
A imagem final de produção deve conter só o necessário para executar a aplicação, sem ferramentas de build desnecessárias. Na prática, isso significa evitar compilers, headers, caches temporários e dependências de desenvolvimento no estágio final.
Compare os dois cenários:
Imagem inchada: contém compiladores, bibliotecas de build, cache do pip, arquivos temporários, ferramentas que só foram úteis durante a instalação e até dependências que não serão usadas em produção.
Imagem limpa: contém o interpretador, a aplicação, dependências de runtime e um usuário sem privilégios. Tudo o que serviu para montar o artefato ficou no estágio anterior.
O ganho real não é só tamanho. Uma imagem menor tende a baixar mais rápido, escanear mais rápido e expor menos superfície de ataque. Executar o container com um usuário não root é uma boa prática de segurança para produção porque reduz o impacto de uma eventual exploração dentro do processo da aplicação. Em muitos ambientes, esse detalhe é o que separa um container “funciona” de um container minimamente aceitável em produção.
Criar um usuário só para executar o app no container não impede bugs do código, mas reduz muito o impacto caso a aplicação seja comprometida.
Criar um usuário só para executar o app no container não impede bugs do código, mas reduz muito o impacto caso a aplicação seja comprometida.
Como validar que o fluxo local e o container estão funcionando do mesmo jeito?
O melhor teste é comparar o mesmo app respondendo no host e no container. Primeiro, valide localmente com o ambiente ativo:
source .venv/bin/activate
python --version
pip --version
python src/app/main.py
Depois, construa a imagem:
docker build -t meu-projeto:latest .
E execute o container:
docker run --rm meu-projeto:latest
Se a saída for a mesma, você tem uma forte evidência de que o empacotamento está consistente. Para projetos web, o teste equivalente é subir a aplicação na porta esperada no host e no container e comparar resposta HTTP, headers e comportamento básico. O ponto é o mesmo: o container não deve mudar a lógica da aplicação, apenas o empacotamento e o isolamento.
Um checklist curto para evitar dor de cabeça:
- Instale
python3-venvantes de criar o ambiente no Ubuntu 24.04. - Crie o ambiente com
python3 -m venv .venv. - Ative com
source .venv/bin/activatee desative comdeactivate. - Use
src/epyproject.tomlpara organizar o projeto. - Fixe a imagem base do Python em vez de depender de uma tag vaga.
- Use multi-stage build para não levar ferramentas de build ao runtime.
- Não copie um
venvcriado em outro contexto sem revisar compatibilidade de caminhos e binários. - Execute o container como usuário não root sempre que possível.
Quando esse fluxo está montado direito, você ganha previsibilidade: o que roda no Ubuntu com venv é o que roda no Docker, com menos surpresa e menos retrabalho. Esse é o tipo de base que aguenta um projeto profissional sem virar improviso a cada mudança de dependência.
Antes disso, rode a aplicação local com a .venv, suba o container com a mesma porta e a mesma variável de ambiente e compare a resposta HTTP; se houver divergência, o problema está no empacotamento, não no código.
Copiar uma .venv criada no host para dentro da imagem costuma quebrar por diferenças de caminhos e binários, então o build deve recriar dependências no ambiente alvo.
Se o shell não reconhecer a ativação, validar which pip é mais confiável do que assumir que o pip já está dentro da .venv.
No fim, o fluxo profissional não é “usar venv ou Docker”, e sim fazer o venv resolver o isolamento no desenvolvimento e o Docker resolver a reprodução no deploy.
Antes disso, rode a aplicação local com a .venv, suba o container com a mesma porta e a mesma variável de ambiente e compare a resposta HTTP; se houver divergência, o problema está no empacotamento, não no código.
Copiar uma .venv criada no host para dentro da imagem costuma quebrar por diferenças de caminhos e binários, então o build deve recriar dependências no ambiente alvo.
Se o shell não reconhecer a ativação, validar which pip é mais confiável do que assumir que o pip já está dentro da .venv.
No fim, o fluxo profissional não é “usar venv ou Docker”, e sim fazer o venv resolver o isolamento no desenvolvimento e o Docker resolver a reprodução no deploy.