Docker pour Python en 2026 : le guide de construction moderne uv + en plusieurs étapes
Ouvrez n'importe quel didacticiel « Dockerize your Python app » vieux de cinq ans et vous trouverez les quatre mêmes lignes : FROM python:3.11-slim, COPY conditions.txt ., RUN pip install -r conditions.txt, COPY . .. Ça marche. Il fournit également une image de plus de 200 Mo, se reconstruit lentement car chaque installation de dépendance est un cache complet manqué au moment où une ligne de code d'application change, et glisse silencieusement un compilateur C et des fichiers d'en-tête que votre conteneur en cours d'exécution ne touchera plus jamais une fois la construction terminée.
Plus rien de tout cela n’est nécessaire. Entre uv (le gestionnaire de packages et l'installateur basés sur Rust d'Astral), les montages de cache BuildKit et les images de base sans distribution, la valeur par défaut de 2026 pour un conteneur Python de production est une construction en plusieurs étapes qui ne contient qu'un interpréteur Python, votre environnement virtuel et votre code, rien d'autre. Ce guide présente l'ancien modèle, le nouveau modèle et un Dockerfile que vous pouvez copier dans un projet réel aujourd'hui.
L'ancien modèle et pourquoi il est cher
DOCKERFILEFROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]
Ce modèle comporte trois coûts qui s’aggravent à mesure que le projet se développe. Premièrement, python:3.13-slim est déjà livré avec environ 150 à 200 Mo compressés avant d'ajouter une seule dépendance, car "slim" contient toujours un espace utilisateur Debian complet, apt et les extensions C de la bibliothèque standard Python. Deuxièmement, chaque couche RUN pip install est mise en cache par le hachage de contenu Docker des couches précédentes, modifiant ainsi une ligne dans app.py avant COPY . . n'invalide rien en amont, mais une modification de requirements.txt invalide toute l'étape d'installation et télécharge à nouveau chaque roue. Troisièmement, si une dépendance nécessite une version native (une roue manylinux manquante, une version de paquet inhabituelle), pip récupère discrètement les outils de construction qui restent ensuite pour toujours dans l'image finale, car il n'y a qu'une seule étape pour la jeter.
Le nouveau modèle : une étape uv builder plus une étape finale sans distribution
Le correctif est une construction en plusieurs étapes : une étape qui contient un compilateur, uv et votre fichier de verrouillage, et une deuxième étape, beaucoup plus petite, qui copie uniquement l'environnement virtuel terminé et votre arborescence source. La dernière étape ne voit jamais pip, uv, apt ou un compilateur C.
DOCKERFILEFROM 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'a pas de shell, pas de gestionnaire de paquets et pas de pip, ce qui est exactement ce que vous voulez en production : il ne reste rien dans le conteneur qu'un attaquant qui obtient l'exécution de code pourrait utiliser pour extraire plus d'outils. L'image finale est constituée des packages de site de votre environnement virtuel ainsi que de votre code, qui, pour un service FastAPI typique, pèse bien moins de 50 Mo au lieu de 200 Mo et plus.
Montages de cache BuildKit : ne retéléchargez jamais deux fois la même roue
La ligne --mount=type=cache,target=/root/.cache/uv fait le vrai travail ici. Sans cela, chaque « construction de docker » sur un nouveau programme d'exécution télécharge à nouveau chaque roue de PyPI, car le cache de couches ne survit que si les couches précédentes, y compris « COPY uv.lock », sont identiques en octets. Un montage de cache BuildKit est un volume persistant en dehors du graphe de couches : il survit entre les builds sur le même hôte ou, avec un backend de cache de registre, entre les exécuteurs CI, même lorsque le fichier de verrouillage change. Associez-le à uv sync --frozen, qui refuse de toucher le fichier de verrouillage et échoue bruyamment si pyproject.toml et uv.lock sont désynchronisés, au lieu de résoudre silencieusement une arborescence de dépendances différente de celle sur laquelle vos tests ont été exécutés.
Séparer l'étape uv sync --no-install-project de la uv sync finale est l'autre moitié de l'astuce : le premier appel installe uniquement les dépendances tierces (mises en cache tant que uv.lock ne change pas), et le deuxième appel, exécuté après COPY . /app, installe votre propre projet sans retoucher les packages tiers. Modifiez une ligne de code d'application et la couche de dépendance reste mise en cache.
Dépendances natives : numpy, Pillow et le problème Manylinux
Des packages comme numpy et Pillow fournissent de nombreuses roues Linux prédéfinies pour les plates-formes communes, ainsi sur linux/amd64 ou linux/arm64 uv sync télécharge normalement simplement une roue et ne touche jamais un compilateur. Le mode d'échec apparaît lorsque vous êtes sur une architecture inhabituelle, que vous épinglez une toute nouvelle version avant que les roues ne rattrapent leur retard ou que vous dépendez d'un package qui ne fournit qu'une distribution source. Dans ce cas, l'étape de construction nécessite des outils de construction, l'étape finale ne doit jamais voir :
DOCKERFILEFROM 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 (en-têtes gcc, make, libc) ne vit qu'au stade du constructeur. L'étape finale sans distribution copie le « .venv » déjà compilé, jamais le compilateur qui l'a construit. Si vous souhaitez vous entraîner à créer et à expédier un véritable service conteneurisé de bout en bout, le cours FastAPI décrit exactement ce type d'API, du premier point de terminaison jusqu'à un déploiement en forme de production.
Python géré par uv : arrêtez en fonction de l'interpréteur fourni par l'image de base
python:3.13-slim vous donne la version du correctif que Debian a décidé de publier cette semaine-là. uv peut gérer l'interpréteur lui-même à la place : uv python install 3.13.1 télécharge une version CPython spécifique et reproductible à partir du projet python-build-standalone et l'épingle dans .python-version, de sorte que votre étape de construction et votre ordinateur portable exécutent exactement le même interpréteur jusqu'à la version du correctif. Combiné avec UV_PYTHON_DOWNLOADS=never dans le Dockerfile, de sorte qu'une version mal épinglée fait échouer la construction bruyamment au lieu de télécharger silencieusement quelque chose de différent, cela ferme l'espace "fonctionne sur ma machine, échoue sur le coureur" qu'une balise d'image de base flottante laisse ouverte.
Débogage d'un conteneur sans shell
Le seul coût réel de la distribution : il n'y a pas de shell, donc docker exec -it containers sh ne fonctionne pas. Pour le débogage local, créez le même Dockerfile avec une cible qui s'arrête au stade du constructeur (docker build --target=builder -t myapp:debug .), qui a toujours l'espace utilisateur complet de python:3.13-slim et un shell. Gardez cette cible de débogage hors de votre pipeline de déploiement ; il existe pour votre terminal, pas pour la production.
Le modèle à fichier unique
DOCKERFILEFROM 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"]
Copiez ceci, modifiez le nom du module ENTRYPOINT pour qu'il corresponde à la disposition de votre projet, et vous obtenez une version plus petite, plus rapide lors des versions répétées et plus difficile à attaquer que le modèle pip install -r Requirements.txt que la plupart des didacticiels enseignent encore. Prêt à mettre en pratique le backend qui se trouve à l’intérieur de ce conteneur ? Commencez avec FastAPI.
##FAQ
Dois-je activer BuildKit pour que le montage du cache fonctionne ?
Oui, --mount=type=cache est une fonctionnalité uniquement BuildKit. Il s'agit du constructeur par défaut dans Docker Desktop et les versions récentes de Docker Engine, mais sur les anciens exécuteurs CI, vous devrez peut-être définir explicitement « DOCKER_BUILDKIT=1 » ou « docker buildx build » au lieu de « docker build » simple.
Pourquoi ne pas simplement utiliser python:3.13-alpine pour une image plus petite ?
La libc musl d'Alpine brise l'hypothèse de la roue Manylinux sur laquelle s'appuient numpy, Pillow et la plupart des packages compilés, vous perdez donc les roues prédéfinies et soit compilez à partir des sources, soit rencontrez des bugs d'exécution subtils. distroless-glibc évite ce piège.
Est-ce que uv remplace complètement pip et Poetry ?
Pour la plupart des nouveaux projets, oui. uv se résout et s'installe plus rapidement, lit le même « pyproject.toml » et génère un fichier de verrouillage, mais si une équipe dépend déjà des plugins Poetry ou si un workflow que uv ne couvre pas encore, il n'y a aucune urgence de migrer à mi-projet. Essayez d'abord uv sur un nouveau service.
Que se passe-t-il si mon image de base a besoin d'un correctif de sécurité après avoir déjà créé l'image ?
Reconstruire. Épinglez un résumé, pas seulement une balise, pour python:3.13-slim dans la phase de construction, et reconstruisez selon un calendrier hebdomadaire, ce qui est raisonnable pour la plupart des équipes, de sorte qu'une image de base corrigée est transférée dans une nouvelle version au lieu de rester obsolète pendant des mois.
Puis-je utiliser ce modèle pour un projet Django au lieu de FastAPI ?
Oui, le Dockerfile ne se soucie pas du framework que vous exécutez ; seul le ENTRYPOINT final change (par exemple gunicorn myproject.wsgi:application). Le cours FastAPI est un moyen rapide de mettre en pratique le modèle de bout en bout si vous démarrez un nouveau service.
Est-il prêt pour la production sans distribution ou est-il encore expérimental ?
Il est largement utilisé en production ; Google le maintient et de nombreuses équipes le fournissent comme base Python par défaut. Le compromis à accepter est l’absence de shell, ce qui est un choix de sécurité délibéré et non un bug.