03 Guide pas-a-pas - Atelier 3 - Semaine 2

Dockerisez la borne
intelligente

Trois options : aventure narrative LLM, Q&A streame ou classification d'images. Vous embarquez une intelligence artificielle locale dans un container Docker. Pas de cloud, pas d'API distante, fonctionnement hors-ligne.

Ce qu'on vous fournit

Le dossier labos/semaine-2/demande-3-borne-intelligente/ contient trois options - vous en choisissez une seule :

Vous y ajoutez : Dockerfile, entrypoint.sh (sauf vision), .gitignore, construire.sh, demarrer.sh, arreter.sh, votre README.md.

Avant de commencer

Pre-requis et choix d'option

Quelle option choisir ?

Option Narrative - Aventure 6 etapes pilotee par LLM

Niveau : avance - Stack : Python + Pygame + ollama + qwen2.5:0.5b - Image : ~600 Mo + 400 Mo de volume

Le visiteur incarne un soigneur stagiaire et fait des choix. L'IA improvise l'histoire en temps reel. Pattern Docker phare : plusieurs processus dans un container orchestres par entrypoint.sh.

Option Q&A - Reponses streamees jeton par jeton

Niveau : avance - Stack : Python + Pygame + ollama + qwen2.5:0.5b - Image : ~600 Mo + 400 Mo de volume (partage avec narrative)

Le visiteur clique une question, l'IA repond en streaming (effet "qui s'ecrit"). Memes patterns que narrative, mais avec mode stream: true de l'API ollama et threading Python.

Option Vision - Classification ONNX hors-ligne

Niveau : intermediaire - Stack : Python + Pygame + onnxruntime + Pillow + numpy - Image : ~300 Mo, autonome

Galerie de photos, clic = top-3 predictions par MobileNetV2. Pattern Docker phare : donnees lourdes integrees au build (modele + labels via wget). Aucun volume, aucun reseau au runtime.

Patterns Docker importants

Trois patterns que vous saurez expliquer

Cette demande illustre trois patterns qu'on peut comparer et expliquer. Vous devez comprendre les trois meme si vous n'en codez qu'un.

PATTERN 1

Plusieurs processus dans un meme container

Options Narrative et Q&A : un script entrypoint.sh lance ollama serve en arriere-plan, attend qu'il reponde, fait ollama pull du modele si absent, puis exec python xxx.py. Le container vit aussi longtemps que Python tourne.

PATTERN 2

Donnees lourdes au runtime via volume nomme

Le modele LLM (~400 Mo) n'est pas dans l'image (l'image resterait grosse et impossible a pousser sur un registre, le rebuild serait lent). Il vit dans un volume Docker persistant zoo-ollama-cache cree par demarrer.sh. Premier demarrage : long. Demarrages suivants : instantanes.

PATTERN 3

Donnees lourdes au build, image autonome

Option Vision : le modele ONNX (14 Mo) est petit, donc on l'embarque dans l'image via RUN wget au build. Pas de volume, pas de reseau au runtime, image immuable. C'est le compromis inverse du pattern 2 : image plus grosse, mais autonome immediate.

Le choix entre le pattern 2 et le pattern 3 depend du poids des donnees et de la frequence de mise a jour. LLM : trop gros, change souvent -> volume. Modele de classification petit et fige -> embarque.

Vue d'ensemble

Le parcours en huit etapes

Etape 01

Inspecter et choisir l'option

Etape 02

Comprendre le pattern Docker cible

Etape 03

Ecrire le Dockerfile

Etape 04

Ecrire entrypoint.sh (LLM)

Etape 05

Injecter la banniere

Etape 06

Construire et demarrer

Etape 07

Encapsuler dans 3 scripts

Etape 08

Finaliser README et .gitignore

01

Inspecter le materiel et choisir votre option

Lisez le code de l'option choisie. Vous devez identifier les points cles avant de coder le Dockerfile.

NARRATIVE

Lisez option-narrative/application/narrative.py et prompts.py. Reperez : le system prompt par ecosysteme, l'appel HTTP a http://127.0.0.1:11434/api/chat, la machine a etats Pygame (choix ecosysteme -> aventure -> conclusion), le thread daemon qui appelle l'IA sans bloquer le rendu.

QA

Lisez option-qa/application/qa.py et questions.py. Reperez : la liste des 12 questions pre-redigees, l'appel POST /api/chat avec stream: true, le thread qui parse le streaming JSON ligne par ligne et accumule dans un buffer protege par threading.Lock.

VISION

Lisez option-vision/application/vision.py et classifieur.py. Reperez : le chargement singleton du modele ONNX au demarrage, le preprocessing ImageNet (224x224, normalisation mean/std), softmax stable, top-K via argpartition. Lisez aussi generer-photos-test.py qui sera execute pendant le build.

Vous savez decrire l'experience visiteur. Vous savez ou se fait l'appel a l'IA dans le code. Vous savez quels artefacts (modele, photos, sons) sont generes a quel moment.
02

Comprendre le pattern Docker cible de votre option

Avant de coder le Dockerfile, dessinez mentalement le diagramme : que contient l'image, que contient le volume, que se passe-t-il au build vs au premier demarrage vs aux demarrages suivants ?

NARRATIVE / QA

Au build

  • Image Python 3.12-slim
  • Pygame + requests installes par pip
  • ollama installe par curl ... | sh
  • Code source copie dans /borne

Au premier demarrage

  • Volume zoo-ollama-cache cree (vide)
  • entrypoint.sh lance ollama, attend, telecharge le modele dans le volume (~400 Mo)
  • Lance Python qui ouvre la fenetre

Aux demarrages suivants

  • Memes etapes mais le modele est deja dans le volume, le pull est instantane
VISION

Au build

  • Image Python 3.12-slim
  • Pygame + onnxruntime + Pillow + numpy installes
  • wget rapatrie mobilenetv2-12.onnx et synset.txt
  • Generation des 9 photos factices via generer-photos-test.py

Au premier demarrage (et tous les suivants)

  • Aucun volume, aucun reseau
  • Python charge le modele en memoire (singleton, < 1 sec)
  • Boucle Pygame
Vous pouvez expliquer en 30 secondes : ce qu'il y a dans l'image, ce qu'il y a dans le volume (le cas echeant), pourquoi ce decoupage.
03

Ecrire le Dockerfile

Creer le fichier Dockerfile a la racine de votre dossier d'option.

Squelette - options Narrative et Q&A

Dockerfile - Narrative ou Q&A
FROM python:3.12-slim
LABEL borne="[narrative ou qa]"

# Polices SDL2 + curl pour installer ollama + healthcheck
RUN apt-get update && apt-get install -y \
    fonts-dejavu-core curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Installation de ollama (script officiel)
RUN curl -fsSL https://ollama.com/install.sh | sh

# Wheels Python pinnees pour la reproductibilite
RUN pip install --no-cache-dir pygame==2.5.2 requests==2.32.3

WORKDIR /borne
COPY application/ /borne/

RUN chmod +x /borne/entrypoint.sh

# Le port 11434 reste interne, ollama ecoute sur 127.0.0.1
ENTRYPOINT ["/borne/entrypoint.sh"]

Squelette - option Vision

Dockerfile - Vision
FROM python:3.12-slim
LABEL borne="vision"

RUN apt-get update && apt-get install -y \
    fonts-dejavu-core wget ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir \
    pygame==2.5.2 onnxruntime==1.18.1 Pillow==10.4.0 numpy==1.26.4

WORKDIR /borne

# Modele ONNX et labels - URLs documentees
# https://github.com/onnx/models/tree/main/validated/vision/classification/mobilenet
RUN wget -q -O /borne/mobilenetv2-12.onnx \
    https://github.com/onnx/models/raw/main/validated/vision/classification/mobilenet/model/mobilenetv2-12.onnx
RUN wget -q -O /borne/imagenet-labels.txt \
    https://raw.githubusercontent.com/onnx/models/main/validated/vision/classification/synset.txt

COPY application/ /borne/

# Generer 9 photos factices pour que la borne marche du premier coup
RUN python /borne/generer-photos-test.py /borne/photos

CMD ["python", "/borne/vision.py"]
Pour Narrative et Q&A : ne mettez pas RUN ollama pull qwen2.5:0.5b dans le Dockerfile. Ca ne marche pas (ollama serve n'est pas demarre pendant le build) et ca empaqueterait 400 Mo dans l'image. Le pull se fait dans entrypoint.sh au runtime.
Pour Vision : si le wget echoue (reseau coupe pendant le build), le build echoue. Documenter une URL miroir dans un commentaire. La solution fournie pointe sur huggingface.co/onnx/mobilenetv2-12 en alternative.
Le Dockerfile compile sans erreur. Pour Vision, l'image fait environ 300 Mo. Pour Narrative/Q&A, environ 600 Mo (Python + ollama + wheels).
04

Ecrire entrypoint.sh (uniquement Narrative et Q&A)

Le script entrypoint.sh orchestre les deux processus du container : il lance ollama en arriere-plan, attend qu'il reponde, telecharge le modele si necessaire, puis lance Python.

Si vous avez choisi Vision, sautez cette etape : votre CMD lance directement Python, pas besoin de script intermediaire.

Squelette de entrypoint.sh

entrypoint.sh
#!/bin/bash
set -e

nomModele="qwen2.5:0.5b"

# Lancer ollama serve en arriere-plan
echo "[entrypoint] Demarrage de ollama serve..."
ollama serve &
pidOllama=$!

# Attendre qu'il reponde sur 11434 (max 30 sec)
for tentative in {1..30}; do
    if curl -sf http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
        echo "[entrypoint] ollama est pret."
        break
    fi
    sleep 1
done

# Pull du modele si pas deja dans le volume
if ! ollama list | grep -q "$nomModele"; then
    echo "[entrypoint] Telechargement du modele $nomModele (premier demarrage, 2-3 min)..."
    ollama pull "$nomModele"
fi

# Lancer Python (au premier plan, c'est le processus principal du container)
echo "[entrypoint] Lancement de l'application Pygame."
exec python /borne/[narrative.py ou qa.py]
Le exec a la fin est important : sans exec, le container a deux processus principaux et la gestion des signaux devient batarde. Avec exec, Python remplace le shell et recoit directement les signaux Docker (SIGTERM lors d'un docker stop).
Si vous oubliez chmod +x dans le Dockerfile (RUN chmod +x /borne/entrypoint.sh), le container echoue avec "permission denied" au demarrage. Remediation : ajouter le chmod, rebuild.
Le ollama list | grep -q peut etre piege si le nom du modele a un format inhabituel. Test : si le pull se relance a chaque demarrage meme apres premier succes, votre comparaison est fausse.
Premier demarrage : vous voyez les messages "[entrypoint] ..." dans docker logs, le pull avance. Deuxieme demarrage : pas de pull, demarrage rapide.
05

Injecter la banniere d'identification

Comme pour les autres demandes : nom etudiant, matricule, date de build dans la banniere de la fenetre Pygame.

Modification du Dockerfile (toutes options)

Dockerfile - extrait
ARG NOM_ETUDIANT="Etudiant Inconnu"
ARG MATRICULE="0000000"
ARG BUILD_DATE="date-non-fournie"
ENV NOM_ETUDIANT=$NOM_ETUDIANT
ENV MATRICULE=$MATRICULE
ENV BUILD_DATE=$BUILD_DATE

Le code Python lit deja ces variables avec os.environ.get("NOM_ETUDIANT", "Inconnu"). Verifiez la fonction qui dessine la banniere en haut de la fenetre.

Apres rebuild avec --build-arg NOM_ETUDIANT="Votre Nom", votre identite apparait dans la banniere de la fenetre Pygame.
06

Construire et demarrer

Build avec banniere, demarrage avec X11 et (selon option) volume nomme.

Build

Terminal
$ docker build \
    --build-arg NOM_ETUDIANT="Prenom Nom" \
    --build-arg MATRICULE="1234567" \
    --build-arg BUILD_DATE="$(date -I)" \
    -t zoo-borne-intelligente-[option]:1.0 .

Demarrage

NARRATIVE / QA
docker volume create zoo-ollama-cache
xhost +local:docker
docker run -d \
  --name borne-llm \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  -v zoo-ollama-cache:/root/.ollama \
  zoo-borne-intelligente-narrative:1.0
docker logs -f borne-llm

Premier demarrage : 2-3 minutes pour le pull du modele. Vous voyez le progres dans les logs. Une fois pret, la fenetre Pygame s'ouvre.

VISION
xhost +local:docker
docker run -d \
  --name borne-vision \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  zoo-borne-intelligente-vision:1.0

Demarrage instantane (pas de modele a telecharger). La galerie 3x3 apparait.

Narrative / Q&A : si vous omettez le volume -v zoo-ollama-cache:/root/.ollama, le modele est telecharge a chaque ./demarrer.sh. C'est ce que le volume sert a eviter.
Vision : les predictions sur les photos factices sont absurdes (envelope, menu, carton...). C'est attendu : le pipeline marche, c'est juste que les photos sont des carres colores. Documentez ce comportement dans votre README pour montrer que vous avez compris.
L'experience visiteur fonctionne : Narrative donne 6 etapes d'aventure, Q&A streame les reponses, Vision affiche le top-3. Banniere visible.
07

Encapsuler dans trois scripts d'usage

Trois scripts en kebab-case. Particularite pour Narrative/Q&A : demarrer.sh doit creer le volume si absent (commande idempotente).

Squelette de demarrer.sh - Narrative ou Q&A

demarrer.sh
#!/bin/bash
set -e

nomImage="zoo-borne-intelligente-[option]:1.0"
nomContainer="[nom]-en-marche"
nomVolume="zoo-ollama-cache"

# Creer le volume s'il n'existe pas (idempotent)
docker volume inspect "$nomVolume" >/dev/null 2>&1 \
    || docker volume create "$nomVolume"

# Autoriser X11 du container
xhost +local:docker

# Nettoyer un eventuel ancien container du meme nom
if docker ps -a --format "{{.Names}}" | grep -q "^$nomContainer$"; then
    docker rm -f "$nomContainer"
fi

docker run -d \
    --name "$nomContainer" \
    -e DISPLAY="$DISPLAY" \
    -v /tmp/.X11-unix:/tmp/.X11-unix \
    -v "$nomVolume":/root/.ollama \
    "$nomImage"

echo "[ok] Borne lancee. Premier demarrage : 2-3 min de pull du modele."
echo "[ok] Suivez avec : docker logs -f $nomContainer"

arreter.sh

Comme la demande 2 : docker stop, docker rm, xhost -local:docker. Ne supprimez pas le volume : il contient le cache du modele que vous voulez conserver entre runs. Si l'utilisateur veut vraiment le purger, il fera docker volume rm zoo-ollama-cache manuellement.

La sequence ./construire.sh "Mon Nom" 1234567 ; ./demarrer.sh ; ./arreter.sh ; ./demarrer.sh demontre que le second demarrage est rapide grace au volume.
08

Finaliser le README et le .gitignore

Documenter votre travail et exclure de Git ce qui est genere automatiquement.

.gitignore par option

NARRATIVE / QA

A exclure : __pycache__/, *.pyc, captures, logs, scripts temporaires.

Le modele LLM n'est jamais commit (il vit dans le volume Docker, pas dans le code source).

VISION

A exclure : __pycache__/, *.onnx, imagenet-labels.txt, application/photos/. Tous ces artefacts sont generes pendant le build a partir du Dockerfile.

Si un developpeur copie de vraies photos dans application/photos/, il devra commenter cette ligne dans .gitignore.

README.md - sections obligatoires

  • Ce que c'est : option choisie, scenario du Zoo, ce que voit le visiteur.
  • Pattern Docker phare : multi-process / volume nomme / build-time bundling. Expliquez en 2-3 phrases pourquoi ce pattern et pas l'autre.
  • Comment lancer : la sequence des trois scripts.
  • Premier demarrage vs suivants : pour Narrative/Q&A, mentionnez le pull initial. Pour Vision, mentionnez les photos factices et leurs predictions absurdes.
  • Ce qu'on voit : capture d'ecran de l'aventure / des reponses / de la galerie + top-3.
  • Choix techniques : pourquoi qwen2.5:0.5b et pas plus gros, pourquoi MobileNetV2 et pas ResNet, pourquoi onnxruntime CPU et pas PyTorch.

Bonus pedagogique : pour Vision, mesurer le temps d'inference (premier clic vs clics suivants) et le documenter. Pour Narrative/Q&A, mesurer la latence par etape et documenter.

Quelqu'un qui n'a pas vu votre travail peut lancer la borne en suivant uniquement votre README. La premiere fois, l'instruction "ce sera long" est claire.

Defis optionnels par option

Defi A (Narrative) - Modele plus gros pour une narration plus riche

Remplacez qwen2.5:0.5b par gemma2:2b (1.5 Go, excellent en francais). Mesurez la latence et la qualite des sorties. Documentez le compromis taille/qualite.

Defi B (Q&A) - Ajout d'une 13e question personnelle

Ajoutez une question sur une espece observable depuis votre cegep (Bas-Saint-Laurent). Verifiez que le system prompt fournit assez de contexte pour que l'IA reponde avec des informations factuelles.

Defi C (Vision) - Vraies photos d'animaux

Remplacez les 9 photos factices par 9 vraies photos libres de droits (Wikimedia Commons). Modifiez generer-photos-test.py ou le Dockerfile pour copier vos vraies photos. Comparez les predictions avant/apres dans votre README.

Defi commun - Healthcheck Docker

Ajoutez un HEALTHCHECK qui verifie que le processus principal repond. Pour Narrative/Q&A : curl -sf http://127.0.0.1:11434/api/tags (ollama). Pour Vision : verification du fichier modele present.