ADR 011 â Image de prod : multi-stage, Python 3.12-slim pinnĂ©, pas de distroless (2026-06-04)
Contexte
Issues #32 (Dockerfile multi-stage), #30 (CVE de l'image de base), #12 (Python 3.12).
Le Dockerfile d'origine était single-stage et accumulait plusieurs défauts :
FROM python:3.10-slimalors querequirements.txtétait déjà compilé pour Python 3.12 (et le venv local en 3.12). Incohérence de version, fragile.- Les dépendances de test embarquées en prod :
requirements.txtcontenaitpytest,pytest-cov,coverage,httpx,wheel... installées dans l'image finale. Surface d'attaque et poids inutiles. curlinstallé uniquement pour unHEALTHCHECKDocker redondant : sous Kubernetes, les probes liveness/readiness (/healthz/live,/healthz/ready, #47) assurent déjà la santé du pod. Le HEALTHCHECK tapait/(pas un endpoint health).- Base par tag mutable (
python:3.10-slim), incohérent avec la logique de promote par digest (#65, "l'image scannée == l'image déployée").
DĂ©cision 1 â Multi-stage + split des requirements
Le Dockerfile passe en deux stages :
- builder (
python:3.12-slim) : installe les deps prod dans un venv isolé (python -m venv /venv). Voir Amendement #67 ci-dessous. - runtime (
python:3.12-slim) : ne reçoit que/venv(ni pip, ni outils de build, ni harnais de test).
Les dépendances sont scindées :
requirements.in/requirements.txt: runtime de prod uniquement.requirements-dev.in/requirements-dev.txt:pytest,pytest-cov,httpx(contraints par-c requirements.txtpour aligner les versions partagées).
Le gain principal du multi-stage ici n'est pas la compilation (psycopg[binary]
fournit des wheels précompilés, pas de toolchain C nécessaire) mais le fait de
ne plus embarquer les deps de test dans l'image de prod.
Le job CI run-tests installe désormais requirements.txt et
requirements-dev.txt.
Amendement #67 â 2026-06-09 : passage de --prefix Ă python -m venv
pip install --prefix=/install a été remplacé par python -m venv /venv dans le stage builder.
Cause racine : --prefix ne réinstalle pas les paquets déjà présents dans l'image de base du builder. packaging était préinstallé dans python:3.12-slim, donc skippé dans /install. Copié dans le runtime (base propre), packaging était absent. pip check levait une erreur masquée car le CMD n'invoque pas gunicorn.
Fix : Un venv est un environnement Python entiĂšrement isolĂ© â pip installe toutes les dĂ©pendances dĂ©clarĂ©es, sans tenir compte de la base image. Comportement dĂ©terministe quel que soit le contenu du builder.
# Stage builder
RUN python -m venv /venv && /venv/bin/pip install --no-cache-dir --upgrade pip wheel
RUN /venv/bin/pip install --no-cache-dir -r requirements.txt
# Stage runtime
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
En bonus : gunicorn retiré des dépendances (inutile sous Kubernetes, scaling assuré par replicas/HPA).
DĂ©cision 2 â Base python:3.12-slim pinnĂ©e par digest, PAS distroless
Le choix a été tranché par mesure réelle (build + trivy image), pas par
intuition. Les deux candidats comparés, multi-stage équivalent :
| CritĂšre | python:3.12-slim | distroless/python3-debian12 |
|---|---|---|
| Version Python | 3.12 (conforme #12) | 3.11.2 (figée, < #12) |
| Taille image | 212 MB | 161 MB |
Trivy CI (--ignore-unfixed, CRIT/HIGH) |
0 / 0 â pipeline vert | 0 / 7 HIGH â pipeline rouge |
| Surface totale (fixed+unfixed) | 2 CRIT + 7 HIGH | 5 CRIT + 18 HIGH |
| Dont CVE dans l'interpréteur Python | 0 | 3 HIGH (libpython3.11) |
| Patchable par nous | oui (rebuild fréquent Docker) | non sans rebuild upstream |
| Debug (shell) | présent | absent |
Conclusion contre-intuitive : distroless n'est pas plus sûr dans ce cas.
L'image python:3.12-slim est rebuildée en continu (désormais sur Debian 13
trixie, 0 CVE OS), donc ses seules CVE résiduelles sont unfixed et légitimement
masquées par --ignore-unfixed. Le distroless officiel est figé sur un
Python 3.11.2 ancien, dont des CVE déjà corrigées en amont (dont 3 HIGH dans
l'interprĂ©teur lui-mĂȘme) ne sont pas intĂ©grĂ©es : elles bloqueraient le pipeline
et ne sont pas patchables cÎté projet (pas d'apt). La fraßcheur de l'image prime
sur la minimalité.
La base est pinnée par digest (python:3.12-slim@sha256:090ba77e...) : build
reproductible, cohérent avec le promote par digest (#65). Le tag est conservé pour
la lisibilité.
Note : une 3e voie (Chainguard/Wolfi, Python à jour + minimal) existe mais dépend d'un registre tiers ; hors scope ici, à réévaluer si la minimalité redevient prioritaire.
DĂ©cision 3 â Retrait de curl et du HEALTHCHECK Docker
curl et la directive HEALTHCHECK sont retirés de l'image : sous Kubernetes, les
probes assurent déjà liveness/readiness. Une dépendance réseau et une surface
d'attaque en moins.
Conséquence assumée : les healthchecks des docker-compose-dev.yaml /
docker-compose-prod.yaml reposaient sur curl. Ils passent Ă un check en
Python stdlib (urllib) sur l'endpoint dédié /healthz/live (corrige au
passage l'ancien check sur /).
Conséquences
- Image plus légÚre et alignée sur Python 3.12 ; Trivy CI à 0 CRITICAL / 0 HIGH.
requirements.txtne contient plus aucune dépendance de test.- Validé en local avant merge : build +
trivy image(0/0),pytest(37 passed, 95%) sous 3.12, et dĂ©marrage end-to-end viadocker-compose-dev(alembic upgrade head+ PostgreSQL +/healthz/readyâ 200). - La mĂ©canique de build CI (kaniko â Trivy â promote par digest) est inchangĂ©e : le multi-stage produit un nouveau digest, scannĂ© puis promu comme avant.
- Pas de directive
# syntax=dans le Dockerfile : kaniko (builder CI) ne gĂšre pas le frontend BuildKit.
Date : 2026-06-04 Sprint : 3 Issues : #32, #30, #12