Aller au contenu principal
Semestre 2 · Bloc 1 · Travaux pratiques 12

Conteneuriser les trois services de Listify

Compétences
C3

Étape 1 : le backend, et la découverte du cache (1 h 30)​

1.1 Un premier Containerfile​

Dans votre dépôt, créez backend/Containerfile :

backend/Containerfile
FROM python:3.12-slim

# Un utilisateur non-root (moindre privilège, ch. 17 §4.2)
RUN useradd --system --no-create-home --shell /usr/sbin/nologin listify

WORKDIR /app

# Dépendances AVANT le code : le cache de build (ch. 17 §2)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Le code applicatif ensuite
COPY app.py wsgi.py ./

# Exécution en non-root, sur toutes les interfaces DU CONTENEUR
USER listify
EXPOSE 8000
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "wsgi:app"]

Et un backend/.containerignore pour ne pas polluer le contexte de build (ch. 17 §2) :

backend/.containerignore
.venv/
__pycache__/
*.pyc

Construisez et lisez la sortie couche par couche :

cd ~/Github/edu/listify # adaptez à votre chemin
podman build -t listify-backend:1.0 ./backend
podman images | grep listify-backend # ~150 Mo

1.2 Éprouver le cache​

Le cœur pédagogique de l'étape. Faites une modification dans le code (backend/app.py, ajoutez un commentaire), puis reconstruisez :

podman build -t listify-backend:1.1 ./backend

Observez : les couches pip install sont marquées Using cache ; seule la couche COPY app.py et les suivantes sont refaites. Le build est quasi instantané. Maintenant, modifiez requirements.txt (même trivialement) et reconstruisez : cette fois pip install est refait. Vous venez de vérifier de vos yeux la règle « dépendances avant code » (ch. 17 §2). Consignez les deux durées : l'écart est l'argument.

Point de contrôle n° 1 : le backend tourne, isolé

Le backend a besoin d'une base. Lancez un PostgreSQL jetable et testez :

podman network create listify-net
podman run -d --name db --network listify-net \
-e POSTGRES_DB=listify -e POSTGRES_USER=listify -e POSTGRES_PASSWORD=secret \
docker.io/library/postgres:16-alpine

# ATTENDRE que la base soit prête (au 1er démarrage, initdb prend quelques
# secondes ; sans cette attente, le health renverrait 503 "database error").
for i in $(seq 1 20); do podman exec db pg_isready -U listify >/dev/null 2>&1 && break; sleep 1; done

# charger le schéma :
podman exec -i db psql -U listify -d listify < db/schema.sql

# lancer le backend, qui joint "db" PAR SON NOM (DNS de réseau, ch. 18) :
podman run -d --name backend --network listify-net \
-e DB_HOST=db -e DB_PASSWORD=secret listify-backend:1.0
sleep 2
podman exec backend python3 -c "import urllib.request as u; print(u.urlopen('http://127.0.0.1:8000/api/health').read().decode())"
# {"api":"ok","database":"ok"}

Notez : DB_HOST=db fonctionne sans connaître d'adresse IP, c'est le DNS interne du réseau Podman (ch. 18 §1.2). Et si le health renvoie 503 (database error), ce n'est pas un bug : c'est que la base n'était pas encore prête, exactement le problème d'ordre de démarrage que le depends_on: condition: service_healthy du TP 13 résout déclarativement. Nettoyez ensuite les conteneurs et le réseau (sinon network create échouera au prochain passage avec « network already exists ») :

podman rm -f db backend
podman network rm listify-net

Étape 2 : le frontend et Nginx (1 h)​

Le frontend sert les fichiers statiques et fait office de reverse proxy vers le backend (exactement le rôle de Nginx au S1). Créez frontend/Containerfile :

frontend/Containerfile
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html app.js style.css /usr/share/nginx/html/
EXPOSE 80

Et la configuration Nginx frontend/nginx.conf, où le proxy pointe vers le service backend (résolu par le DNS du réseau) :

frontend/nginx.conf
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;

location / {
try_files $uri $uri/ =404;
}
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
podman build -t listify-frontend:1.0 ./frontend
podman images | grep listify-frontend # ~50 Mo, grâce à la base alpine

Étape 3 : la base et le volume (45 min)​

La base n'a pas de Containerfile : on utilise l'image officielle postgres:16-alpine. Ce qui compte, c'est de persister ses données dans un volume (ch. 18 §2), sans quoi elles disparaîtraient avec le conteneur :

podman volume create listify-data
podman run -d --name db \
-e POSTGRES_DB=listify -e POSTGRES_USER=listify -e POSTGRES_PASSWORD=secret \
-v listify-data:/var/lib/postgresql/data \
docker.io/library/postgres:16-alpine
# attendre la disponibilité avant de charger le schéma
for i in $(seq 1 20); do podman exec db pg_isready -U listify >/dev/null 2>&1 && break; sleep 1; done
podman exec -i db psql -U listify -d listify < db/schema.sql

(Cette étape isole la base : pas besoin du réseau nommé, aucun autre conteneur ne la joint par son nom ici.)

Point de contrôle n° 2 : la persistance

Prouvez que le volume survit au conteneur :

podman exec db psql -U listify -d listify -c "INSERT INTO tasks (title) VALUES ('survivra');"
podman rm -f db # le conteneur MEURT
podman run -d --name db \
-e POSTGRES_DB=listify -e POSTGRES_USER=listify -e POSTGRES_PASSWORD=secret \
-v listify-data:/var/lib/postgresql/data docker.io/library/postgres:16-alpine
for i in $(seq 1 20); do podman exec db pg_isready -U listify >/dev/null 2>&1 && break; sleep 1; done
podman exec db psql -U listify -d listify -c "SELECT title FROM tasks;" # 'survivra' est là

Le conteneur est jetable ; le volume garde les données. C'est la distinction stateless/stateful du S1, au niveau du stockage (ch. 18 §2). Nettoyez : podman rm -f db && podman volume rm listify-data.

Étape 4 : mesurer et optimiser (45 min)​

Inspectez ce que contiennent vos images et leur poids :

podman images --format "table {{.Repository}} {{.Tag}} {{.Size}}"
podman history listify-backend:1.0 # la taille de CHAQUE couche

Expérimentez une optimisation et mesurez :

  • Comparez python:3.12 (base complète) et python:3.12-slim (notre choix) : changez le FROM, reconstruisez, comparez les tailles. L'écart (~300 Mo) justifie le -slim.
  • Bonus : l'outil dive (s'il est disponible) explore une image couche par couche et signale le gaspillage.

Consignez le tableau des tailles : c'est un livrable, et l'argument concret des bonnes pratiques du chapitre 17.

Point de contrôle final​

  • Les trois tiers conteneurisés : listify-backend, listify-frontend construits ; postgres avec volume
  • Cache de build éprouvé (modif code = rapide ; modif requirements = pip refait), durées consignées
  • Backend joignant la base par le nom db (DNS de réseau)
  • Persistance prouvée : donnée survivant à la destruction du conteneur (volume)
  • Tableau des tailles d'images ; effet du -slim mesuré
  • Containerfile + .containerignore committés dans le dépôt

Pour aller plus loin (bonus)​

  1. Utilisateur non-root vérifié : podman run --rm listify-backend:1.0 id : le processus tourne-t-il bien en listify et non en root ? Et en rootless, à quel UID cela correspond-il sur l'hôte (podman top) ?
  2. Multi-stage anticipé : le frontend deviendra multi-stage quand il aura un build (Vite). Écrivez dès maintenant un Containerfile multi-stage fictif (étape node qui « build », étape nginx qui sert) pour vous entraîner à la syntaxe COPY --from= (ch. 17 §3).
  3. Digest vs tag : podman image inspect listify-backend:1.0 --format '{{.Digest}}' : notez le digest. Reconstruisez à l'identique : le digest change-t-il ? (Découverte de la non-reproductibilité des builds, ch. 17, Regard recherche.)

Questions de compréhension (à préparer pour le TD et l'examen)​

  1. Expliquez, couche par couche, pourquoi modifier app.py reconstruit beaucoup moins que modifier requirements.txt, dans notre Containerfile. Reliez au copy-on-write du chapitre 15.
  2. Le code de Listify n'a pas changé depuis le S1. Qu'est-ce qui a changé, alors, et en quoi cela règle-t-il le « ça marche sur ma machine » ? (Réponse attendue : l'artefact, l'immutabilité.)
  3. Pourquoi la base de données a-t-elle besoin d'un volume alors que le backend n'en a pas ? Généralisez à la distinction stateless/stateful.
  4. EXPOSE 8000 ne « publie » aucun port. À quoi sert cette instruction, alors, et quelle commande publie réellement le port ?