Vai al contenuto principale
Strumenti Python2026-11-01 · 11 min di lettura

Docker per Python nel 2026: The Modern uv + Multi-Stage Build Guide

Apri qualsiasi tutorial "Dockerize your Python app" vecchio di cinque anni e troverai le stesse quattro righe: FROM python:3.11-slim, COPY require.txt ., RUN pip install -r require.txt, COPY . .. Funziona. Fornisce inoltre un'immagine da oltre 200 MB, si ricostruisce lentamente perché ogni installazione di dipendenza è una cache piena, manca il momento in cui una riga di codice dell'applicazione cambia e trascina silenziosamente un compilatore C e file di intestazione che il contenitore in esecuzione non toccherà mai più al termine della compilazione.

Niente di tutto ciò è più necessario. Tra uv (il gestore di pacchetti basato su Rust e il programma di installazione di Astral), i montaggi della cache BuildKit e le immagini di base senza distribuzione, l'impostazione predefinita del 2026 per un contenitore Python di produzione è una build a più fasi che fornisce solo un interprete Python, il tuo ambiente virtuale e il tuo codice, nient'altro. Questa guida illustra il vecchio pattern, il nuovo pattern e un Dockerfile che puoi copiare oggi stesso in un progetto reale.

Il vecchio modello e perché è costoso

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

Questo modello ha tre costi che si accumulano man mano che il progetto cresce. Innanzitutto, python:3.13-slim viene già fornito con una compressione di circa 150-200 MB prima di aggiungere una singola dipendenza, perché "slim" contiene ancora un'area utente Debian completa, apt e le estensioni C della libreria standard Python. In secondo luogo, ogni livello RUN pip install viene memorizzato nella cache dall'hash del contenuto di Docker dei livelli precedenti, quindi modificando una riga in app.py prima di COPY . . non invalida nulla a monte, ma una modifica a requirements.txt invalida l'intera fase di installazione e scarica nuovamente ogni ruota. In terzo luogo, se una qualsiasi dipendenza necessita di una build nativa (una ruota manylinux mancante, una versione insolita del pacchetto), pip inserisce silenziosamente gli strumenti di build che poi rimangono nell'immagine finale per sempre, perché c'è solo una fase in cui buttarli via.

Il nuovo modello: una fase di costruzione uv più una fase finale senza distro

La correzione è una build in più fasi: una fase che ha un compilatore, uv e il file di lock, e una seconda fase, molto più piccola, che copia solo l'ambiente virtuale finito e l'albero dei sorgenti. La fase finale non vede mai pip, uv, apt o un compilatore 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 non ha shell, nessun gestore di pacchetti e nessun pip, che è esattamente ciò che desideri in produzione: non è rimasto nulla nel contenitore che un utente malintenzionato che ottiene l'esecuzione del codice possa utilizzare per inserire più strumenti. L'immagine finale sono i pacchetti del sito del tuo ambiente virtuale più il tuo codice, che per un tipico servizio FastAPI arriva ben al di sotto dei 50 MB invece che oltre i 200 MB.

Supporti cache BuildKit: non scaricare mai nuovamente la stessa ruota due volte

La riga --mount=type=cache,target=/root/.cache/uv sta facendo il vero lavoro qui. Senza di essa, ogni "docker build" su un nuovo corridore scarica nuovamente ogni ruota da PyPI, perché la cache dei livelli sopravvive solo se i livelli precedenti, incluso "COPY uv.lock", sono identici in byte. Un montaggio della cache BuildKit è un volume persistente al di fuori del grafico del livello: sopravvive tra build sullo stesso host o, con un backend della cache del registro, tra i corridori CI, anche quando il file di lock cambia. Abbinalo a uv sync --frozen, che rifiuta di toccare il file di lock e fallisce rumorosamente se pyproject.toml e uv.lock non sono sincronizzati, invece di risolvere silenziosamente un albero delle dipendenze diverso da quello su cui sono stati eseguiti i test.

Dividere il passaggio uv sync --no-install-project dal passo finale uv sync è l'altra metà del trucco: la prima chiamata installa solo le dipendenze di terze parti (memorizzate nella cache finché uv.lock non cambia), e la seconda chiamata, eseguita dopo COPY . /app, installa il tuo progetto senza ritoccare pacchetti di terze parti. Modifica una riga del codice dell'applicazione e il livello di dipendenza rimane memorizzato nella cache.

Dipendenze native: Numpy, Pillow e il problema ManyLinux

Pacchetti come Numpy e Pillow forniscono manylinux wheel precostruiti per le piattaforme comuni, quindi su linux/amd64 o linux/arm64 uv sync normalmente scarica solo un wheel e non tocca mai un compilatore. La modalità di errore si manifesta quando si utilizza un'architettura non comune, si inserisce una versione molto nuova prima che le ruote raggiungano il livello o si dipende da un pacchetto che fornisce solo una distribuzione sorgente. In tal caso la fase di costruzione necessita di strumenti di costruzione, la fase finale non deve mai vedere:

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" (intestazioni gcc, make, libc) risiede solo nella fase di creazione. La fase finale senza distribuzione copia il file .venv già compilato, mai il compilatore che lo ha creato. Se vuoi esercitarti a creare e distribuire un vero servizio containerizzato end-to-end, il corso FastAPI illustra esattamente questo tipo di API, dal primo endpoint a una distribuzione a forma di produzione.

Python gestito da uv: smette di dipendere dall'interprete fornito dall'immagine di base

python:3.13-slim ti fornisce qualsiasi patch che Debian abbia deciso di rilasciare quella settimana. uv può invece gestire l'interprete stesso: uv python install 3.13.1 scarica una build CPython specifica e riproducibile dal progetto python-build-standalone e la inserisce in .python-version, in modo che la fase di creazione e il laptop eseguano esattamente lo stesso interprete fino alla versione patch. Combinato con UV_PYTHON_DOWNLOADS=never nel Dockerfile, quindi una versione errata fallisce rumorosamente la compilazione invece di scaricare silenziosamente qualcosa di diverso, questo chiude il gap "funziona sulla mia macchina, fallisce sul corridore" che un tag mobile dell'immagine di base lascia aperto.

Debug di un contenitore senza shell

L'unico vero costo di Distroless: non esiste una shell, quindi docker exec -it container sh non funziona. Per il debug locale, crea lo stesso Dockerfile con una destinazione che si ferma nella fase di creazione (docker build --target=builder -t myapp:debug .), che ha ancora l'area utente completa di python:3.13-slim e una shell. Mantieni la destinazione di debug fuori dalla pipeline di distribuzione; esiste per il tuo terminale, non per la produzione.

Il modello a file singolo

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"]

Copialo, modifica il nome del modulo ENTRYPOINT in modo che corrisponda al layout del tuo progetto e avrai una build più piccola, più veloce nelle build ripetute e più difficile da attaccare rispetto al pattern pip install -r require.txt che la maggior parte dei tutorial insegna ancora. Pronto a mettere in pratica il backend che va all'interno di questo contenitore? Inizia con FastAPI.

Domande frequenti

È necessario che BuildKit sia abilitato affinché il montaggio della cache funzioni?

Sì, --mount=type=cache è una funzionalità disponibile solo per BuildKit. È stato il builder predefinito in Docker Desktop e nelle recenti versioni di Docker Engine, ma sui runner CI più vecchi potrebbe essere necessario impostare esplicitamente "DOCKER_BUILDKIT=1" o "docker buildx build" anziché il semplice "docker build".

Perché non usare semplicemente python:3.13-alpine per un'immagine più piccola?

Musl libc di Alpine rompe il presupposto della ruota manylinux su cui si basano Numpy, Pillow e la maggior parte dei pacchetti compilati, quindi si perdono le ruote precostruite e si compila dal sorgente o si riscontrano sottili bug di runtime. distroless-glibc evita questa trappola.

uv sostituisce completamente pip e Poetry?

Per la maggior parte dei nuovi progetti, sì. uv si risolve e si installa più velocemente, legge lo stesso pyproject.toml e genera un file di blocco, ma se un team dipende già dai plugin Poetry o da un flusso di lavoro che uv non copre ancora, non c'è urgenza di migrare a metà progetto. Prova prima uv su un nuovo servizio.

Cosa succede se la mia immagine di base necessita di una patch di sicurezza dopo aver già creato l'immagine?

Ricostruire. Aggiungi un digest, non solo un tag, per python:3.13-slim nella fase di creazione e ricostruisci secondo un programma, settimanale è ragionevole per la maggior parte dei team, quindi un'immagine di base con patch confluisce in una nuova build invece di rimanere stantia per mesi.

Posso utilizzare questo modello per un progetto Django anziché FastAPI?

Sì, al Dockerfile non interessa quale framework esegui; cambia solo l'ultimo ENTRYPOINT (ad esempio gunicorn myproject.wsgi:application). Il corso FastAPI è un modo rapido per esercitarsi con il modello dall'inizio alla fine se stai avviando un nuovo servizio.

È pronto per la produzione distroless o è ancora sperimentale?

È ampiamente utilizzato nella produzione; Google lo mantiene e molti team lo forniscono come base Python predefinita. Il compromesso da accettare è la mancanza di una shell, che è una scelta di sicurezza deliberata, non un bug.

Pronto a provarlo?

15 lezioni gratuite previa iscrizione gratuita — nessuna tessera. Prova la piattaforma e poi decidi.

Inizia gratis →

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.