Dockerfile
Rama main · réplica de lectura del repositorio original.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
COPY app/static/ ./static/
# Catalogues i18n : les .po sont versionnés, les .mo sont compilés ici. Un
# catalogue mal formé fait échouer le build plutôt que de partir en production
# avec des traductions muettes. Voir docs/i18n.md.
RUN pybabel compile -d app/translations
# Compte de service dédié : rien ici n'a besoin de root, et une exécution en root
# transforme une évasion de conteneur en compromission de l'hôte. UID fixe (et
# non attribué au hasard) pour que le propriétaire du volume de données reste le
# même d'une image à la suivante.
#
# ATTENTION à la mise à jour d'une instance existante : son volume appartient à
# root, l'application ne pourra plus y écrire. Une fois, conteneur arrêté :
# docker run --rm -v excerpta_data:/data alpine chown -R 10001:0 /data
RUN useradd --system --uid 10001 --gid 0 --no-create-home excerpta \
&& mkdir -p data \
&& chown -R 10001:0 /app \
&& chmod -R g=u /app
ARG GIT_COMMIT=unknown
ENV APP_VERSION=$GIT_COMMIT
# Confiance aux en-têtes X-Forwarded-* : uvicorn s'en sert pour
# `request.client.host`, donc pour la limitation de débit, et pour le schéma
# d'URL. Il parcourt X-Forwarded-For depuis la droite et retient la première
# adresse qui n'est PAS dans cette liste : celle que le reverse proxy a ajoutée.
#
# Jamais `*` : uvicorn prend alors le PREMIER élément de la liste, c'est-à-dire
# celui qu'écrit le client. Changer cette valeur à chaque requête suffisait à
# repartir d'un compteur neuf et annulait toutes les limites de débit.
#
# Par défaut : boucle locale et réseaux privés, où se trouvent le proxy et la
# passerelle Docker. Restreindre à l'adresse exacte du proxy quand elle est
# connue (voir FORWARDED_ALLOW_IPS dans .env.example).
ENV FORWARDED_ALLOW_IPS=127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7
USER 10001
EXPOSE 8000
# L'en-tête Host doit porter le hostname de BASE_URL : StrictHostMiddleware
# répond 400 à tout le reste, y compris à un appel sur 127.0.0.1 — un
# healthcheck naïf resterait rouge en permanence.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD python -c "import os, sys, urllib.request as u; from urllib.parse import urlparse; \
host = urlparse(os.environ.get('BASE_URL', 'http://localhost')).hostname or 'localhost'; \
sys.exit(0 if u.urlopen(u.Request('http://127.0.0.1:8000/health', headers={'Host': host}), timeout=4).status == 200 else 1)"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]