Saltar para o conteúdo principal
Ferramentas Python2026-11-01 · 11 min de leitura

Docker para Python em 2026: o guia de construção moderno uv + multiestágio

Abra qualquer tutorial "Dockerize seu aplicativo Python" de cinco anos e você encontrará as mesmas quatro linhas: FROM python:3.11-slim, COPY requisitos.txt ., RUN pip install -r requisitos.txt, COPY . .. Funciona. Ele também fornece uma imagem de mais de 200 MB, reconstrói lentamente porque cada instalação de dependência é um cache completo perdido no momento em que uma linha do código do aplicativo é alterada e arrasta silenciosamente um compilador C e arquivos de cabeçalho que seu contêiner em execução nunca mais tocará após a conclusão da compilação.

Nada disso é mais necessário. Entre uv (o gerenciador de pacotes baseado em Rust e instalador do Astral), montagens de cache BuildKit e imagens base sem distribuição, o padrão 2026 para um contêiner Python de produção é uma construção de vários estágios que fornece apenas um interpretador Python, seu ambiente virtual e seu código, nada mais. Este guia percorre o padrão antigo, o novo padrão e um Dockerfile que você pode copiar em um projeto real hoje.

O padrão antigo e por que é caro

DOCKERFILE
FROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]

Esse padrão tem três custos que aumentam à medida que o projeto cresce. Primeiro, python:3.13-slim já vem com cerca de 150 a 200 MB compactados antes de você adicionar uma única dependência, porque "slim" ainda carrega uma área de usuário Debian completa, apt, e as extensões C da biblioteca padrão Python. Segundo, cada camada RUN pip install é armazenada em cache pelo hash de conteúdo do Docker das camadas anteriores, alterando assim uma linha em app.py antes de COPY . . não invalida nada do upstream, mas uma alteração em requirements.txt invalida toda a etapa de instalação e baixa novamente cada roda. Terceiro, se alguma dependência precisar de uma compilação nativa (uma roda Manylinux faltando, uma versão de pacote incomum), o pip puxa silenciosamente as ferramentas de compilação que permanecem na imagem final para sempre, porque há apenas um estágio para jogá-la fora.

O novo padrão: um estágio de construção uv mais um estágio final sem distribuição

A correção é uma construção de vários estágios: um estágio que possui um compilador, uv, e seu arquivo de bloqueio, e um segundo estágio, muito menor, que apenas copia o ambiente virtual finalizado e sua árvore de origem. O estágio final nunca vê pip, uv, apt ou um compilador C.

DOCKERFILE
FROM python:3.13-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev COPY . /app RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" ENTRYPOINT ["/app/.venv/bin/python", "-m", "app"]

gcr.io/distroless/python3-debian12 não tem shell, nem gerenciador de pacotes, nem pip, que é exatamente o que você deseja na produção: não há mais nada no contêiner que um invasor que obtenha a execução de código possa usar para obter mais ferramentas. A imagem final são os pacotes de sites do seu ambiente virtual mais o seu código, que para um serviço FastAPI típico chega bem abaixo de 50 MB em vez de mais de 200 MB.

Montagens de cache do BuildKit: nunca baixe novamente o mesmo wheel duas vezes

A linha --mount=type=cache,target=/root/.cache/uv está fazendo o verdadeiro trabalho aqui. Sem ele, cada docker build em um novo executor baixa novamente cada roda do PyPI, porque o cache da camada só sobrevive se as camadas anteriores, incluindo COPY uv.lock, forem idênticas em bytes. Uma montagem de cache do BuildKit é um volume persistente fora do gráfico de camada: ele sobrevive em compilações no mesmo host ou, com um back-end de cache de registro, em executores de CI, mesmo quando o arquivo de bloqueio é alterado. Combine-o com uv sync --frozen, que se recusa a tocar no arquivo de bloqueio e falha ruidosamente se pyproject.toml e uv.lock estiverem fora de sincronia, em vez de resolver silenciosamente uma árvore de dependência diferente daquela em que seus testes foram executados.

Dividir a etapa uv sync --no-install-project do uv sync final é a outra metade do truque: a primeira chamada instala apenas dependências de terceiros (armazenadas em cache desde que uv.lock não mude), e a segunda chamada, executada após COPY . /app, instala seu próprio projeto sem retocar pacotes de terceiros. Altere uma linha do código do aplicativo e a camada de dependência permanecerá em cache.

Dependências nativas: numpy, travesseiro e o problema do Manylinux

Pacotes como numpy e Pillow enviam rodas Manylinux pré-construídas para as plataformas comuns, então em linux/amd64 ou linux/arm64 uv sync normalmente apenas baixa uma roda e nunca toca em um compilador. O modo de falha aparece quando você está em uma arquitetura incomum, fixa uma versão muito nova antes que as rodas o alcancem ou depende de um pacote que envia apenas uma distribuição fonte. Nesse caso, o estágio de construção precisa de ferramentas de construção, o estágio final nunca deve ver:

DOCKERFILE
FROM python:3.13-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends build-essential \ && rm -rf /var/lib/apt/lists/* # uv + sync steps as in the stage above

build-essential (cabeçalhos gcc, make, libc) reside apenas no estágio de construção. O estágio final sem distribuição copia o .venv já compilado, nunca o compilador que o construiu. Se você quiser praticar a construção e o envio de um serviço real em contêineres de ponta a ponta, o curso FastAPI percorre exatamente esse tipo de API, desde o primeiro endpoint até uma implantação em formato de produção.

Python gerenciado por uv: pare de depender de qualquer intérprete que a imagem base enviou

python:3.13-slim fornece qualquer versão de patch que o Debian decidiu lançar naquela semana. uv pode gerenciar o próprio intérprete: uv python install 3.13.1 baixa uma compilação CPython específica e reproduzível do projeto python-build-standalone e fixa-a em .python-version, para que seu estágio de construção e seu laptop executem exatamente o mesmo intérprete até a versão do patch. Combinado com UV_PYTHON_DOWNLOADS=never no Dockerfile, de modo que uma versão mal fixada falha na construção ruidosamente em vez de baixar silenciosamente algo diferente, isso fecha a lacuna "funciona na minha máquina, falha no executor" que uma tag de imagem base flutuante deixa aberta.

Depurando um contêiner sem shell

O único custo real da distroless: não há shell, então docker exec -it container sh não funciona. Para depuração local, construa o mesmo Dockerfile com um destino que pare no estágio do construtor (docker build --target=builder -t myapp:debug .), que ainda possui a área de usuário completa do python:3.13-slim e um shell. Mantenha esse alvo de depuração fora do pipeline de implantação; existe para o seu terminal, não para produção.

O modelo de arquivo único

DOCKERFILE
FROM python:3.13-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev COPY . /app RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" ENTRYPOINT ["/app/.venv/bin/python", "-m", "app"]

Copie isso, altere o nome do módulo ENTRYPOINT para corresponder ao layout do seu projeto, e você terá uma compilação que é menor, mais rápida em compilações repetidas e mais difícil de atacar do que o padrão pip install -r requisitos.txt que a maioria dos tutoriais ainda ensina. Pronto para praticar o back-end que fica dentro deste contêiner? Comece com FastAPI.

Perguntas frequentes

Preciso que o BuildKit esteja ativado para que a montagem do cache funcione?

Sim, --mount=type=cache é um recurso exclusivo do BuildKit. Ele tem sido o construtor padrão no Docker Desktop e nas versões recentes do Docker Engine, mas em executores de CI mais antigos você pode precisar de DOCKER_BUILDKIT=1 definido explicitamente ou docker buildx build em vez de docker build simples.

Por que não usar python:3.13-alpine para uma imagem menor?

O musl libc da Alpine quebra a suposição da roda manylinux da qual o numpy, o Pillow e a maioria dos pacotes compilados dependem, então você perde as rodas pré-construídas e compila a partir do código-fonte ou encontra bugs sutis de tempo de execução. distroless-glibc evita essa armadilha.

O uv substitui completamente o pip e a Poesia?

Para a maioria dos novos projetos, sim. uv resolve e instala mais rápido, lê o mesmo pyproject.toml e gera um lockfile, mas se uma equipe já depende de plugins Poetry ou um fluxo de trabalho que uv ainda não cobre, não há urgência em migrar no meio do projeto. Experimente primeiro o uv em um novo serviço.

E se minha imagem base precisar de um patch de segurança depois de eu já ter criado a imagem?

Reconstruir. Fixe um resumo, não apenas uma tag, para python:3.13-slim no estágio de construção e reconstrua de acordo com um cronograma, semanalmente é razoável para a maioria das equipes, de modo que uma imagem base corrigida flua para uma nova construção em vez de ficar obsoleta por meses.

Posso usar esse padrão para um projeto Django em vez de FastAPI?

Sim, o Dockerfile não se importa com qual framework você executa; apenas o ENTRYPOINT final muda (por exemplo gunicorn myproject.wsgi:application). O curso FastAPI é uma maneira rápida de praticar o padrão de ponta a ponta se você estiver iniciando um novo serviço.

Está pronto para produção sem distribuição ou ainda é experimental?

É amplamente utilizado na produção; O Google o mantém e muitas equipes o enviam como base Python padrão. A desvantagem de aceitar é a falta de um shell, que é uma escolha deliberada de segurança, não um bug.

Pronto para experimentar?

15 aulas gratuitas após uma inscrição gratuita - sem cartão. Experimente a plataforma e depois decida.

Comece gratuitamente →

Or unlock every lesson

Pro — $12/mo, unlimited AI, cancel any time. Refund within 14 days worldwide.

See Pro plans →

Get one Python lesson + one career idea every Friday

No spam, no "buy our course now". Three bullets, every Friday. Unsubscribe with one click.