02 Guide pas-a-pas - Atelier 2 - Semaine 2

Dockerisez la borne
medias interactive

Trois options au choix, toutes livrees comme apps desktop kiosque (X11, pas de port reseau) : Pygame (Python natif), Qt (Python + audio Pulse), ou three.js + Electron (JS + Chromium embarque). Le travail Docker est equivalent, l'interet pedagogique est de comparer ce qui change selon le toolkit.

Ce qu'on vous fournit

Le dossier labos/semaine-2/demande-2-borne-medias/ contient trois sous-dossiers - vous en choisissez un seul :

Vous y ajoutez : Dockerfile, .dockerignore, construire.sh, demarrer.sh, arreter.sh, votre README.md.

Avant de commencer

Pre-requis et choix d'option

Quelle option choisir ?

Option Pygame - Quiz interactif

Niveau : intermediaire - Toolkit : Python + SDL2 - Image finale : ~250 Mo

L'option la plus simple a dockeriser. Un seul stage, dependances Python pures. Bon choix si vous voulez vous concentrer sur les fondamentaux Docker (ARG, ENV, X11) sans bagarrer avec un toolkit complexe.

Option Qt - Encyclopedie sonore

Niveau : avance - Toolkit : Python + Qt6 + GStreamer + PulseAudio - Image finale : ~700 Mo

L'option la plus enrichissante techniquement : vous touchez a la lecture audio dans Docker (montage du socket PulseAudio), aux librairies systeme XCB de Qt, et a la generation d'artefacts pendant le build (sons synthetises). Bon choix si vous etes a l'aise avec Linux.

Option three.js - Jeu 3D WebGL en app desktop

Niveau : intermediaire - Toolkit : three.js + Vite + Electron - Image finale : ~600 Mo

Le meme code JavaScript que pour un site web, mais enveloppe avec Electron pour devenir une vraie app desktop kiosque. Affichage via X11 (comme Pygame/Qt), aucun port reseau publie. Bon choix si vous voulez voir comment une app web devient une app native.

Vue d'ensemble

Le parcours en sept etapes

Chaque etape detaille d'abord le tronc commun, puis les specificites par option (encadrees en blocs colores).

Etape 01

Inspecter le materiel et choisir l'option

Etape 02

Comprendre l'architecture cible

Etape 03

Ecrire le Dockerfile

Etape 04

Injecter la banniere ARG -> ENV

Etape 05

Construire et demarrer

Etape 06

Encapsuler dans 3 scripts

Etape 07

Finaliser README et .dockerignore

01

Inspecter le materiel et choisir votre option

Lire le code source de l'option choisie. Vous devez savoir ce qu'elle fait avant de l'emballer. Le code est court (200 a 500 lignes selon l'option).

Indications par option

PYGAME

Lisez option-pygame/application/quiz.py. Reperez la boucle Pygame (events / update / render), la machine a etats (accueil, question, resultat), le tableau des questions (4 ecosystemes x 2 ou 3 questions). Notez ou est lue la banniere : os.environ.get("NOM_ETUDIANT", ...).

QT

Lisez option-qt/application/encyclopedie.py. Reperez la classe FenetreEncyclopedie (QMainWindow), la liste a gauche (QListWidget), la fiche a droite (QLabel + QPushButton), le lecteur audio (QMediaPlayer + QAudioOutput). Lisez aussi generer-sons.py qui sera execute pendant le build.

THREEJS

Lisez option-threejs/src/main.js. Reperez la creation de la scene (THREE.Scene, PerspectiveCamera), le groupe de belugas, l'animation des vagues, le raycaster pour le clic. Lisez aussi vite.config.js pour comprendre le mecanisme define qui injecte la banniere dans le bundle.

Vous savez decrire l'experience visiteur en 3 phrases. Vous savez ou la banniere est lue dans le code. Vous avez identifie les dependances principales.
02

Comprendre l'architecture Docker cible

Avant de coder le Dockerfile, comprenez ce qu'il doit produire. Les contraintes ne sont pas les memes selon l'option.

PYGAME

Stack visee

python:3.12-slim + pygame==2.5.2 + fonts-dejavu-core.

Communication avec l'hote

  • X11 : monter /tmp/.X11-unix, lire $DISPLAY
  • Aucun port reseau (c'est une app, pas un serveur)
QT

Stack visee

python:3.12-slim + PySide6==6.7.2 + libxcb* + gstreamer* + fonts-dejavu-core.

Communication avec l'hote

  • X11 : monter /tmp/.X11-unix, lire $DISPLAY
  • Audio : monter le socket PulseAudio /run/user/$(id -u)/pulse/native et exposer PULSE_SERVER
  • Aucun port reseau
THREEJS

Stack visee

node:20 avec three + vite + electron, plus les dependances graphiques Linux (libgtk-3-0, libnss3, libasound2, libxss1, libxtst6, libdrm2, libgbm1).

Communication avec l'hote

  • X11 : monter /tmp/.X11-unix, lire $DISPLAY (comme Pygame / Qt)
  • Aucun audio
  • Aucun port reseau (Electron affiche la fenetre directement, pas de serveur HTTP)
Vous savez nommer l'image de base, les dependances systeme principales, et comment le container communique avec l'hote pour votre option.
03

Ecrire le Dockerfile

Creer le fichier Dockerfile a la racine de votre dossier d'option (a cote de application/ ou des sources).

Squelette - option Pygame

Dockerfile - Pygame
FROM python:3.12-slim
LABEL borne="medias-pygame"

# Polices systeme pour le rendu texte SDL2
RUN apt-get update && apt-get install -y \
    fonts-dejavu-core \
    && rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir pygame==2.5.2

WORKDIR /borne
COPY application/ /borne/

CMD ["python", "[fichier-quiz]"]

Squelette - option Qt

Dockerfile - Qt
FROM python:3.12-slim
LABEL borne="medias-qt"

# Couche XCB pour Qt + GStreamer pour l'audio + polices
RUN apt-get update && apt-get install -y \
    libxcb-xinerama0 libxcb-cursor0 libxcb-icccm4 libxcb-image0 \
    libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 \
    libxcb-sync1 libxcb-xfixes0 libxcb-xkb1 libxkbcommon-x11-0 \
    libgl1 libegl1 libfontconfig1 libdbus-1-3 \
    gstreamer1.0-plugins-good gstreamer1.0-plugins-base \
    gstreamer1.0-pulseaudio \
    fonts-dejavu-core \
    && rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir PySide6==6.7.2

WORKDIR /borne
COPY application/ /borne/

# Generer les .wav AU BUILD pour qu'ils soient empaquetes
RUN python /borne/generer-sons.py /borne/sons

CMD ["python", "[fichier-encyclopedie]"]

Squelette - option three.js (Electron, app desktop)

On enveloppe le code three.js fourni dans Electron : meme bundle Vite, mais affiche dans une fenetre Chromium native au lieu d'etre servi par un serveur web. Deux approches possibles, dans les deux cas on ne modifie PAS le zip fourni : on travaille dans le dossier voisin ou cote Dockerfile.

Note de nommage importante. Le zip livre contient deja un src/main.js (c'est le code three.js du jeu). Pour eviter la confusion, on appelle le fichier d'entree Electron fenetre-electron.js et non main.js. Le main dans package.json pointe vers ce nouveau nom.

Approche A - Modifier le projet dans un dossier de travail (recommandee)

Vous copiez le contenu du zip dans un dossier de travail (par exemple borne-jeu3d/), puis vous y ajoutez les fichiers Electron. Le zip d'origine reste intact dans le sous-dossier option-threejs/. C'est l'approche la plus simple : tout est en clair, lisible, modifiable a la main.

Etape 1. Copier les fichiers du zip dans votre dossier de travail.

Terminal - preparer le dossier de travail
$ mkdir borne-jeu3d
$ cp -r option-threejs/* borne-jeu3d/
$ cd borne-jeu3d/

Etape 2. Ajouter fenetre-electron.js a la racine.

fenetre-electron.js (a creer a la racine du dossier de travail)
const { app, BrowserWindow } = require('electron')

app.whenReady().then(() => {
  const fenetre = new BrowserWindow({
    fullscreen: true,
    webPreferences: { contextIsolation: true }
  })
  fenetre.loadFile('dist/index.html')
})

app.on('window-all-closed', () => app.quit())

Etape 3. Modifier package.json pour ajouter Electron et pointer sur fenetre-electron.js.

package.json (apres modification)
{
  "name": "borne-jeu3d",
  "version": "1.0.0",
  "main": "fenetre-electron.js",
  "scripts": {
    "build": "vite build",
    "start": "electron ."
  },
  "dependencies": { "three": "..." },
  "devDependencies": {
    "vite": "...",
    "electron": "^31.0.0"
  }
}

Etape 4. Le Dockerfile (a cote dans le dossier de travail) reste simple : il copie tout, fait npm install puis npm run build, et lance Electron.

Dockerfile - approche A (source deja prete)
FROM node:20

# Dependances graphiques Linux requises par Electron / Chromium
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgtk-3-0 libnss3 libasound2 libxss1 libxtst6 libdrm2 libgbm1 \
    fonts-dejavu-core \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Cache : package.json AVANT le code source
COPY package*.json ./
RUN npm install

COPY . ./
RUN npm run build

# Pas de EXPOSE : c'est une app desktop, pas un serveur
CMD ["npx", "electron", ".", "--no-sandbox"]

Approche B - Tout dans le Dockerfile, sans toucher au zip

Si vous voulez garder la source absolument intacte (par exemple parce que c'est un livrable verrouille), vous pouvez tout faire dans le Dockerfile : copier le zip tel quel, copier a cote un fenetre-electron.js que vous avez prepare, et patcher le package.json dans une couche RUN avec jq (ou sed). Plus complexe, mais le zip livre n'est jamais touche.

Structure du dossier de travail :

borne-jeu3d/
├── option-threejs/           (zip d'origine, intact)
│   ├── package.json
│   ├── src/
│   └── ...
├── fenetre-electron.js        (votre entree Electron)
├── Dockerfile
├── .dockerignore
└── construire.sh / demarrer.sh / arreter.sh
Dockerfile - approche B (patche le package.json dans la build)
FROM node:20

RUN apt-get update && apt-get install -y --no-install-recommends \
    libgtk-3-0 libnss3 libasound2 libxss1 libxtst6 libdrm2 libgbm1 \
    fonts-dejavu-core jq \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copier le zip tel quel a la racine de l'image
COPY option-threejs/ ./

# Copier l'entree Electron a cote
COPY fenetre-electron.js ./

# Patcher package.json : ajouter "main" et "electron" sans editer le zip d'origine
RUN jq '.main = "fenetre-electron.js" | .devDependencies.electron = "^31.0.0"' \
    package.json > package.json.tmp && mv package.json.tmp package.json

RUN npm install
RUN npm run build

CMD ["npx", "electron", ".", "--no-sandbox"]

Pourquoi jq. C'est l'outil standard pour modifier du JSON. Le filtre .main = "..." | .devDependencies.electron = "..." ajoute ou remplace ces deux cles sans toucher au reste du fichier. Plus robuste qu'un sed avec des expressions regulieres sur du JSON.

Comparaison rapide :

  • A (recommandee) : code prepare a la main, Dockerfile court, debug facile. Le zip d'origine reste a cote, vous travaillez sur une copie. Bon choix pour ce labo d'initiation.
  • B : zip jamais touche, tout est trace dans le Dockerfile. Plus complexe (jq, ordre des COPY important), mais reproductible a 100 % a partir du zip seul. Bon choix si vous devez livrer une demonstration "le zip d'origine plus mon Dockerfile suffisent".
Pour Pygame et Qt : ne lancez pas votre application avec un ENTRYPOINT en mode shell. Utilisez la forme exec (CMD ["python", "fichier.py"]). Sinon les signaux ne sont pas transmis et docker stop attend 10 secondes avant de tuer.
Pour Qt : si vous oubliez les libxcb-*, l'application demarre puis affiche qt.qpa.plugin: Could not load the Qt platform plugin "xcb" et plante. C'est l'erreur classique. Le Dockerfile fourni en exemple liste le minimum necessaire.
Pour three.js + Electron : si vous oubliez le .dockerignore, votre node_modules/ local est envoye au daemon a chaque build et peut peser plusieurs centaines de Mo. Voir etape 7.
Pour three.js + Electron : si la fenetre ne s'ouvre pas, c'est presque toujours X11. Verifiez que echo $DISPLAY n'est pas vide sur l'hote, et que vous avez bien fait xhost +local:docker avant le docker run. Le drapeau --no-sandbox dans le CMD est necessaire parce qu'Electron tourne en root dans le conteneur.
Le fichier Dockerfile existe, contient les bonnes instructions pour votre option, et la prochaine etape (le build) marche.
04

Injecter la banniere d'identification

Faire apparaitre votre nom, votre matricule et la date de build en haut de l'experience. La banniere est ce qui distingue votre image de celle de votre voisin.

PYGAME

Chaine ARG -> ENV -> Python

Dans le Dockerfile :

ARG NOM_ETUDIANT
ARG MATRICULE
ARG BUILD_DATE
ENV NOM_ETUDIANT=$NOM_ETUDIANT
ENV MATRICULE=$MATRICULE
ENV BUILD_DATE=$BUILD_DATE

Dans quiz.py, la lecture est deja faite : os.environ.get("NOM_ETUDIANT", "Inconnu"). Verifiez la zone d'affichage dans la fonction qui dessine la banniere.

QT

Chaine identique a Pygame

Memes ARG + ENV dans le Dockerfile. Dans encyclopedie.py, lecture par os.environ.get(...), affichage dans le QLabel de banniere en haut de la fenetre.

THREEJS

Chaine ARG -> vite.config.define -> bundle

Particularite : pas de process.env au runtime dans un bundle nginx. Vite remplace les identifiants au build via define.

Dans le Dockerfile :

ARG NOM_ETUDIANT
ARG MATRICULE
ARG BUILD_DATE
ENV NOM_ETUDIANT=$NOM_ETUDIANT
ENV MATRICULE=$MATRICULE
ENV BUILD_DATE=$BUILD_DATE
RUN npm run build

Le vite.config.js fourni lit process.env.NOM_ETUDIANT au moment du build et fait define: { __NOM_ETUDIANT__: JSON.stringify(...) }. Dans main.js, on utilise simplement __NOM_ETUDIANT__ comme identifiant litteral.

Pour les trois options : si vous ne passez pas --build-arg au moment du docker build, l'ARG reste a sa valeur par defaut. Vous verrez "Etudiant Inconnu" dans la banniere. Ce n'est pas un bug, c'est que vous n'avez pas appele construire.sh avec vos arguments.
Pour three.js : si vous mettez ENV NOM_ETUDIANT=... APRES RUN npm run build, votre bundle est fige sans la valeur. L'ordre dans le Dockerfile compte. La regle generale : tout ce qui doit etre lu pendant le build doit etre defini avant.
Apres rebuild, vous voyez votre nom, votre matricule et la date dans la banniere de la borne, peu importe l'option choisie.
05

Construire et demarrer

Build de l'image avec les bons --build-arg, demarrage avec les bonnes options de runtime.

Build (commun aux trois options)

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

Demarrage par option

PYGAME
xhost +local:docker
docker run -d \
  --name borne-pygame \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  zoo-borne-medias-pygame:1.0

Une fenetre Pygame doit s'ouvrir sur votre bureau dans les 2 secondes.

QT
xhost +local:docker
docker run -d \
  --name borne-qt \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  -v /run/user/$(id -u)/pulse/native:/run/pulse/native \
  -e PULSE_SERVER=unix:/run/pulse/native \
  zoo-borne-medias-qt:1.0

Une fenetre Qt s'ouvre, vous pouvez cliquer "Ecouter le son" sur une espece et entendre le son synthetise.

THREEJS
xhost +local:docker
docker run --rm \
  --name borne-threejs \
  -e DISPLAY=$DISPLAY \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  zoo-borne-medias-threejs:1.0

Une fenetre Electron en plein ecran s'ouvre directement sur votre bureau. La scene 3D s'affiche, les belugas sont cliquables. Aucun port reseau publie, aucun navigateur a ouvrir.

Pygame / Qt : "cannot connect to X server" -> vous n'avez pas fait xhost +local:docker. Ou $DISPLAY est vide (vous etes en SSH sans forwarding X). Verifiez avec echo $DISPLAY.
Qt : son inaudible -> le socket PulseAudio n'est pas monte ou le chemin /run/user/$(id -u)/pulse/native n'existe pas (PulseAudio pas installe ou utilisateur sans session graphique). Verifiez ls -la /run/user/$(id -u)/pulse/native sur l'hote.
three.js + Electron : "page blanche" -> le bundle n'a pas ete construit avant que main.js ne tente de charger dist/index.html. Verifiez que RUN npm run build est present AVANT le CMD. Verifiez aussi que dist/ contient bien index.html (docker exec borne-threejs ls /app/dist).
three.js + Electron : "cannot open X display" -> meme cause que pour Pygame/Qt. Faire xhost +local:docker avant docker run, verifier echo $DISPLAY.
L'experience visiteur fonctionne entierement : navigation, interaction, votre banniere visible. docker logs ne montre aucune erreur Python ou nginx.
06

Encapsuler dans trois scripts d'usage

Trois scripts en kebab-case, executables. construire.sh accepte deux arguments optionnels (nom, matricule) et calcule la date.

construire.sh - structure commune

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

nomEtudiant="${1:-Etudiant Inconnu}"
matricule="${2:-0000000}"
dateBuild="$(date -I)"
nomImage="zoo-borne-medias-[option]:1.0"

echo "[info] Build de $nomImage avec $nomEtudiant ($matricule)"
docker build \
    --build-arg NOM_ETUDIANT="$nomEtudiant" \
    --build-arg MATRICULE="$matricule" \
    --build-arg BUILD_DATE="$dateBuild" \
    -t "$nomImage" .
echo "[ok] Image $nomImage prete."

demarrer.sh - varie selon l'option

PYGAME / QT

Ajoutez xhost +local:docker en debut de script (sinon "cannot connect to X server"). Variables : nomImage, nomContainer. Pour Qt, montez aussi PulseAudio.

THREEJS

Variables : nomImage, nomContainer, portHote (8091), portContainer (80). Pas d'X11, pas d'audio. echo final qui annonce http://localhost:8091/.

arreter.sh

Comme la semaine 1 : docker stop, docker rm. Pour Pygame et Qt, ajoutez xhost -local:docker a la fin pour ne pas laisser le serveur X expose.

Une fois ecrits : chmod +x *.sh.

La sequence ./construire.sh "Mon Nom" 1234567 ; ./demarrer.sh ; ./arreter.sh fait le tour complet sans intervention manuelle.
07

Finaliser le README et le .dockerignore

Documenter votre travail et exclure du contexte de build ce qui n'a rien a y faire.

.dockerignore par option

PYGAME / QT

A exclure : __pycache__/, *.pyc, sons/ (genere au build pour Qt), captures, scripts .sh, README, .dockerignore lui-meme.

THREEJS

A exclure imperativement : node_modules/, dist/, .vite/. Sans ca, votre contexte de build pese plusieurs centaines de Mo et chaque rebuild devient lent.

Test : docker build --progress=plain ... doit afficher un contexte sous les 5 Mo.

README.md - sections obligatoires

  • Ce que c'est : option choisie, scenario du Zoo, ce que voit le visiteur.
  • Comment lancer : sequence des trois scripts avec un exemple complet (vrais nom et matricule).
  • Ce qu'on voit : URL ou comportement attendu, capture d'ecran, banniere visible.
  • Choix techniques : pourquoi cette option, pourquoi ces dependances, comment la banniere se propage.
  • Pieges rencontres : ce qui a coince et comment vous l'avez resolu (X11, son, multi-stage, taille d'image).
Quelqu'un qui n'a pas vu votre travail peut lancer la borne sur son poste en suivant uniquement votre README. Pour les options Pygame et Qt, votre README explique clairement les pre-requis X11 (et PulseAudio).

Defis optionnels par option

Defi A (Pygame) - Ajouter une question sur votre cegep

Ajoutez une question personnelle dans le tableau du quiz : "Quel oiseau peut-on observer depuis la fenetre du cegep de Matane / Rimouski en mai ?" avec quatre choix dont un correct. Modifiez le code et rebuilez.

Defi B (Qt) - Ajouter une espece avec son son

Ajoutez une dixieme espece (par exemple le castor) dans le catalogue, et ajoutez sa fonction de generation dans generer-sons.py (sons de roulette de bois sur l'eau, suggestion : oscillateurs filtres). Verifiez que le son est genere au build et ecoutable au runtime.

Defi C (three.js) - Effet visuel

Ajoutez un brouillard scene.fog = new THREE.FogExp2(0x..., 0.02) ou un soleil couchant (gradient du ciel). Ne touchez pas a la geometrie de la mer pour ne pas casser les performances mobiles.

Defi commun - Reduire la taille de l'image

Mesurez avec docker images zoo-borne-medias-*. Pour Pygame : passer a python:3.12-alpine (gain ~80 Mo, mais attention aux wheels SDL2 qui peuvent ne pas etre disponibles). Pour Qt : impossible de descendre sous 600 Mo, c'est Qt6 qui pese. Pour three.js : essayer nginx:alpine-slim (gain de quelques Mo).

Defi D (three.js) - Variante web avec nginx (au lieu d'Electron)

L'approche par defaut de ce guide enveloppe three.js dans Electron pour produire une vraie app desktop kiosque. Si vous voulez plutot voir la variante web (multi-stage Node -> nginx, port HTTP publie), c'est l'approche qui etait utilisee dans la version precedente du guide.

Voir la version archivee v0 (specifiquement etape 3 pour le Dockerfile multi-stage et etape 5 pour le docker run -p 8091:80).

La variante nginx est plus legere (~30 Mo contre ~600 Mo pour Electron) et illustre le pattern multi-stage. Mais elle expose un port HTTP, donc s'eloigne du pattern kiosque desktop des autres bornes du zoo. Documentez votre choix dans le README.