Picarones / Dockerfile
Claude
fix(docker): corrige le drift port/version (P0.4)
14ca2d3 unverified
# Dockerfile — Picarones
# Image Docker multi-étape avec Tesseract OCR pré-installé
#
# Usage :
# docker build -t picarones:latest .
# docker run -p 7860:7860 picarones:latest
# docker run -p 7860:7860 -v $(pwd)/corpus:/app/corpus picarones:latest
#
# Variables d'environnement supportées :
# OPENAI_API_KEY, ANTHROPIC_API_KEY, MISTRAL_API_KEY
# GOOGLE_APPLICATION_CREDENTIALS
# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION
# AZURE_DOC_INTEL_ENDPOINT, AZURE_DOC_INTEL_KEY
# ──────────────────────────────────────────────────────────────────
# Étape 1 : builder — installe les dépendances Python dans un venv
# ──────────────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────────────
# Sprint A8 (M-2) + Sprint A16 (build déterministe) — image de base
# épinglée à la fois par tag (lisibilité humaine) et par digest sha256
# (reproductibilité bit-à-bit).
#
# Pourquoi le digest : ``python:3.11.13-slim`` peut être re-publié au
# fil des patches Debian avec un même tag mais un contenu différent.
# Pour la reproductibilité institutionnelle BnF, ``@sha256:...`` fige
# l'image binaire — deux ``docker build`` séparés produisent une
# couche de base identique octet par octet.
#
# Rotation trimestrielle (avant chaque release majeure) :
#
# TOKEN=$(curl -s "https://auth.docker.io/token?\
# service=registry.docker.io&scope=repository:library/python:pull" \
# | jq -r .token)
# curl -sI -H "Authorization: Bearer $TOKEN" \
# -H "Accept: application/vnd.oci.image.index.v1+json" \
# https://registry-1.docker.io/v2/library/python/manifests/3.11.13-slim \
# | grep -i docker-content-digest
# # → mettre à jour le digest ci-dessous + bumper PYTHON_BASE_IMAGE
#
# La forme ``image:tag@sha256:...`` est documentée par Docker comme
# valide ; les machines de développement sans registry proxy peuvent
# pull aussi bien que par tag — le digest étant immuable, le pull
# est strictement équivalent à un pull par tag actuel.
# ──────────────────────────────────────────────────────────────────
ARG PYTHON_BASE_IMAGE=python:3.11.13-slim@sha256:9bffe4353b925a1656688797ebc68f9c525e79b1d377a764d232182a519eeec4
FROM ${PYTHON_BASE_IMAGE} AS builder
WORKDIR /app
# Sprint A14 (correctif suite scan Trivy CI) — applique en priorité les
# patches Debian disponibles AVANT d'installer build-essential/git, pour
# éviter d'embarquer les CVE de la base image (libssl3t64, libc6, etc.).
#
# ``--fix-missing`` + ``Acquire::Retries=3`` : tolère la rotation du pool
# debian-security. Quand Debian publie un point-release, l'ancien .deb
# est émondé du pool quasi immédiatement alors que l'index fraîchement
# récupéré le référence encore → 404 transitoire sur un paquet hors
# scope (ex. linux-libc-dev, en-têtes noyau inutiles au runtime). On
# saute ce paquet au lieu de casser le build ; tous les patches CVE
# téléchargeables (libssl3t64, libc6, openssl, zlib1g…) sont appliqués.
RUN apt-get update -o Acquire::Retries=3 && \
apt-get upgrade -y --fix-missing -o Acquire::Retries=3 && \
apt-get install -y --no-install-recommends -o Acquire::Retries=3 \
build-essential \
git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copier les fichiers de configuration du package + lock file Docker.
# ``requirements-docker.lock`` (Sprint A16) gèle l'arbre de dépendances
# transitif résolu par ``uv pip compile pyproject.toml --extra web --extra llm``.
COPY pyproject.toml .
COPY README.md .
COPY requirements-docker.lock .
COPY picarones/ picarones/
# Crée le venv isolé /opt/venv et l'active pour les ``RUN`` suivants.
# Le runtime fera ``COPY --from=builder /opt/venv /opt/venv`` ; sans cette
# création explicite le COPY échoue (régression remontée par CI A14).
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Sprint A16 : installation déterministe via lock file.
#
# 1. Patch pip/setuptools/wheel (Trivy scanne /opt/venv/lib/python3.11/
# site-packages — sans patch les CVE setuptools/wheel ressortent).
# 2. ``--no-deps`` sur le lock empêche pip de re-résoudre — l'arbre
# pinné par ``uv pip compile`` est complet, transitives incluses.
# 3. ``--no-deps`` sur picarones lui-même : le lock contient déjà
# toutes ses dépendances ; cette ligne installe juste le code.
RUN pip install --upgrade --no-cache-dir \
"pip>=24.2" "setuptools>=78.1.1" "wheel>=0.46.2" && \
pip install --no-cache-dir --no-deps -r requirements-docker.lock && \
pip install --no-cache-dir --no-deps -e . && \
pip cache purge
# Patch également la copie système de pip/setuptools/wheel (hors venv)
# que Trivy détecte via ``/usr/local/lib/python3.11/site-packages`` —
# subsiste dans l'image runtime même quand le venv est utilisé.
RUN /usr/local/bin/pip install --upgrade --no-cache-dir \
"pip>=24.2" "setuptools>=78.1.1" "wheel>=0.46.2"
# ──────────────────────────────────────────────────────────────────
# Étape 2 : runtime — image finale légère avec Tesseract
# ──────────────────────────────────────────────────────────────────
# ARG redéclaré ici car les variables ARG hors ``FROM`` sont scopées
# par étape ; sans cette redéclaration le ``FROM`` du runtime perd
# l'épinglage du builder. La valeur DOIT correspondre à celle de
# l'étape builder (digest inclus) — sinon les couches OS divergent.
ARG PYTHON_BASE_IMAGE=python:3.11.13-slim@sha256:9bffe4353b925a1656688797ebc68f9c525e79b1d377a764d232182a519eeec4
FROM ${PYTHON_BASE_IMAGE} AS runtime
# Version injectée au build (``--build-arg PICARONES_VERSION=…``) ;
# défaut ``dev`` plutôt qu'un ``1.0.0`` figé qui dérive de la version
# réelle (setuptools-scm). Le Makefile la dérive automatiquement.
ARG PICARONES_VERSION=dev
LABEL description="Picarones — Plateforme de comparaison de moteurs OCR pour documents patrimoniaux"
LABEL org.opencontainers.image.version="${PICARONES_VERSION}"
LABEL org.opencontainers.image.source="https://github.com/maribakulj/Picarones"
LABEL org.opencontainers.image.licenses="Apache-2.0"
WORKDIR /app
# ── Dépendances système ─────────────────────────────────────────
# Sprint A14 (correctif Trivy) : ``apt-get upgrade -y`` avant install
# pour récupérer les patches de sécurité Debian (libssl3t64, libc6,
# openssl, etc.) — la base image Python ne les inclut pas par défaut.
# ``--fix-missing`` + ``Acquire::Retries=3`` : même résilience à la
# rotation du pool debian-security que l'étape builder (cf. supra).
#
# Sprint S6.1 — reproductibilité institutionnelle (BnF) :
#
# ``tesseract-ocr`` n'est PAS pinné à une version exacte (ex :
# ``=5.3.0-2``) car Debian point-release rebump fréquemment :
# ``5.3.0-2`` → ``5.3.0-2+deb12u1`` → ``5.3.4-1``. Un pin exact
# casse le build dès que la version disparaît du miroir.
#
# Le contrat de reproductibilité repose plutôt sur :
#
# 1. La base image Python pinée par digest SHA256 (cf. ``ARG
# PYTHON_BASE_IMAGE`` ci-dessus) — Debian bookworm garantit la
# stabilité ABI au sein du même point-release.
# 2. ``requirements-docker.lock`` qui fige les versions Python.
# 3. Le ``RunManifest.dependencies_lock`` capture la version
# Tesseract effective au runtime (``tesseract --version``)
# pour traçabilité scientifique.
#
# Si une version mineure de Tesseract introduit une régression
# CER, le mainteneur peut pinner explicitement ICI à ce moment-là
# (avec une note CHANGELOG).
RUN apt-get update -o Acquire::Retries=3 && \
apt-get upgrade -y --fix-missing -o Acquire::Retries=3 && \
apt-get install -y --no-install-recommends -o Acquire::Retries=3 \
# Tesseract OCR 5 + modèles de langues (Debian bookworm).
tesseract-ocr \
tesseract-ocr-fra \
tesseract-ocr-lat \
tesseract-ocr-eng \
tesseract-ocr-deu \
tesseract-ocr-ita \
tesseract-ocr-spa \
# Bibliothèques image pour Pillow
libpng16-16 \
libjpeg62-turbo \
libtiff6 \
libwebp7 \
# Utilitaires
curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# ── Vérification fail-fast de Tesseract (incident 2026-05-16) ───
# ``apt-get upgrade --fix-missing`` (résilience à la rotation du pool
# debian-security, commit d5d68ae) peut laisser un jeu de libs
# runtime INCOHÉRENT si un .deb co-dépendant est sauté : le binaire
# ``tesseract`` se fige alors à la reconnaissance (deadlock OpenMP /
# mismatch ABI liblept/libstdc++) et le run prod timeoute sur CHAQUE
# document — sans que le build n'ait rien signalé.
#
# On exige donc, au build : (a) que Tesseract charge ses libs et
# expose la langue ``fra``, (b) qu'une reconnaissance réelle se
# termine sous 30 s. Un Tesseract cassé OU figé casse désormais le
# BUILD (le Space conserve l'image précédente fonctionnelle) au lieu
# de déployer une image qui gèle en silence. ``--fix-missing`` est
# conservé : l'intention CVE de d5d68ae n'est pas régressée, seule
# la défaillance silencieuse est convertie en échec bruyant.
RUN set -eu; \
timeout 30 tesseract --version; \
timeout 30 tesseract --list-langs 2>&1 | grep -qx fra; \
printf 'P1\n16 16\n' > /tmp/tess_smoke.pbm; \
i=0; while [ "$i" -lt 16 ]; do \
printf '1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0\n' >> /tmp/tess_smoke.pbm; \
i=$((i + 1)); \
done; \
rc=0; \
timeout 30 tesseract /tmp/tess_smoke.pbm - -l fra --psm 6 \
> /dev/null 2>&1 || rc=$?; \
rm -f /tmp/tess_smoke.pbm; \
if [ "$rc" = 124 ]; then \
echo "FATAL: tesseract a gelé (timeout reconnaissance) — \
build refusé, libs runtime probablement incohérentes." >&2; \
exit 1; \
fi
# Patch pip/setuptools/wheel système du runtime (en dehors du venv).
# Trivy scanne /usr/local/lib/python3.11/site-packages indépendamment.
RUN /usr/local/bin/pip install --upgrade --no-cache-dir \
"pip>=24.2" "setuptools>=78.1.1" "wheel>=0.46.2"
# ── Venv Python depuis le builder ──────────────────────────────
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# ── Code source de l'application ───────────────────────────────
COPY --from=builder /app /app
# ── Répertoires de données ──────────────────────────────────────
RUN mkdir -p /app/corpus /app/rapports /app/data
# ── Utilisateur non-root pour la sécurité ──────────────────────
RUN useradd -m -u 1000 picarones && \
chown -R picarones:picarones /app /opt/venv
USER picarones
# ── Variables d'environnement par défaut ───────────────────────
ENV PYTHONUNBUFFERED=1
ENV PYTHONIOENCODING=utf-8
ENV TESSDATA_PREFIX=/usr/share/tesseract-ocr/5/tessdata
# Tesseract LSTM (oem=3) parallélise via OpenMP. Sur le Space HF
# (~2 vCPU partagés) l'OpenMP non borné suroccupe le CPU (N threads
# pour 2 cœurs) → OCR plus LENT et instable. Le forcer mono-thread
# par appel est ici PLUS rapide et déterministe (le parallélisme
# inter-documents est déjà géré par le ThreadPool du CorpusRunner).
ENV OMP_THREAD_LIMIT=1
# ── Ports ───────────────────────────────────────────────────────
EXPOSE 7860
# ── Health check ────────────────────────────────────────────────
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD curl -f http://localhost:7860/health || exit 1
# ── Démarrage ───────────────────────────────────────────────────
CMD ["picarones", "serve", "--host", "0.0.0.0", "--port", "7860"]