Aller au contenu principal
outilsConteneurs et images

Conteneurs et images

Ce que ce chapitre apporte

  • Lancer un conteneur et comprendre chacune des options courantes.
  • Distinguer ce qui survit à l'arrêt d'un conteneur de ce qui disparaît avec lui.
  • Écrire un Dockerfile pour un projet, et l'ordonner pour tirer parti du cache de couches.
  • Prévoir quelles couches seront reconstruites après une modification.
  • Publier un port et monter un volume à bon escient.
  • Décrire une pile de plusieurs services dans un fichier unique.
  • Reconnaître les pratiques qui rendent une image lourde ou dangereuse.
Où on va
Un environnement isolé règle le cas des bibliothèques d'un langage. Il ne dit rien de la version de l'interpréteur, des paquets système, de la base de données ni du système d'exploitation. Le conteneur élargit l'isolement à tout cela. Les notions d'image et de conteneur sont posées dans le bloc génie logiciel ; ce chapitre les met en pratique : lancer, construire, écrire une recette qui ne reconstruit pas tout à chaque virgule, et monter une pile de plusieurs services.

Lancer, regarder, entrer

Terminal8 commandes
$docker run --rm hello-world# exécute puis supprime le conteneur
$docker run --rm -it python:3.12 python# un interpréteur, en interactif
$docker run -d -p 8080:80 --name web nginx# en arrière-plan, port 80 publié sur 8080
$docker ps# les conteneurs en cours
$docker ps -a# y compris ceux qui sont arrêtés
$docker logs web# ce que le programme a écrit
$docker exec -it web sh# ouvrir un shell dans le conteneur qui tourne
$docker stop web && docker rm web# arrêter, puis supprimer

Les options qui reviennent tout le temps :

OptionEffet
--rmsupprime le conteneur à la sortie, évite l'accumulation
-itgarde l'entrée ouverte et attache un terminal
-ddétache, le conteneur tourne en arrière-plan
-p 8080:80le port 80 dans le conteneur devient 8080 sur la machine
-v $(pwd):/appmonte un dossier de la machine dans le conteneur
-e CLE=valeurdéfinit une variable d'environnement
L'ordre des ports
-p 8080:80 se lit « de l'extérieur vers l'intérieur » : à gauche le port sur votre machine, à droite celui que le programme écoute dans le conteneur.
Inverser les deux donne une page injoignable, sans message d'erreur explicite, puisque le conteneur démarre normalement.

Ce qui disparaît

Un conteneur possède son propre système de fichiers, créé au lancement à partir de l'image et détruit avec lui. Tout ce que le programme a écrit dedans est perdu à la suppression du conteneur : fichiers produits, base de données, journaux.

C'est voulu. Un conteneur est censé être jetable et remplaçable. Ce qui doit survivre se range donc explicitement ailleurs.

Terminal2 commandes
$docker run -v mes-donnees:/var/lib/postgresql/data postgres:16# volume nommé, géré par Docker
$docker run -v "$(pwd)/src:/app/src" mon-image # dossier de la machine, monté

Le volume nommé sert aux données persistantes d'un service. Le montage d'un dossier de la machine sert surtout au développement : le code édité sur le poste est vu immédiatement dans le conteneur, sans reconstruire l'image.

Construire son image

Une image se décrit dans un Dockerfile, à la racine du projet. Chaque instruction produit une couche, et une couche inchangée est réutilisée telle quelle lors de la construction suivante.

FROM python:3.12-slim

WORKDIR /app

# Les dépendances d'abord : cette couche ne se reconstruit
# que si requirements.txt change.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Le code ensuite : il change souvent, et ne coûte presque rien à recopier.
COPY src/ ./src/

ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["python", "-m", "src.serveur"]
Terminal3 commandes
$docker build -t rapport:1.0 .# construire, en nommant l'image
$docker images# les images présentes
$docker run --rm -p 8000:8000 rapport:1.0

Un fichier .dockerignore, sur le modèle du .gitignore, évite d'envoyer au moteur de construction le .git, le .venv et les données locales. Sans lui, la construction commence par transférer plusieurs centaines de mégaoctets inutiles.

L'ordre des instructions décide du temps de construction

C'est le point qui distingue une image bien écrite d'une image pénible. Le cache est invalidé à la première instruction dont l'entrée a changé, et tout ce qui suit est reconstruit, même si cela n'avait aucune raison de changer.

main.py
Sortie
>_ Prêt à exécuter…

Quarante secondes contre une, pour la même modification. La règle qui en découle vaut pour tous les langages : ce qui change rarement se copie en premier, ce qui change souvent se copie en dernier.

Une couche n'oublie rien
Copier un fichier de secrets puis le supprimer dans une instruction suivante ne le retire pas de l'image : la couche qui l'a ajouté existe toujours, et quiconque récupère l'image peut la lire.
Les secrets se passent au lancement, par variable d'environnement ou par montage, jamais à la construction.

Une pile de plusieurs services

Une application réelle en a rarement un seul. Le fichier compose.yaml décrit l'ensemble, ses réseaux et ses volumes, et remplace une page de commandes.

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      BASE_URL: postgresql://app:secret@base:5432/rapports
    depends_on:
      - base

  base:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: rapports
    volumes:
      - donnees:/var/lib/postgresql/data

volumes:
  donnees:
Terminal3 commandes
$docker compose up -d# démarre la pile
$docker compose logs -f api# suit les journaux d'un service
$docker compose down# arrête et supprime les conteneurs, garde les volumes

Un détail qui coûte souvent une soirée : à l'intérieur de la pile, les services se joignent par leur nom, ici base, et non par localhost. Chaque conteneur a sa propre pile réseau ; localhost, pour l'API, désigne l'API elle-même.

Quelques règles de bonne image

Partir d'une base mince. python:3.12-slim plutôt que l'image complète. Moins de paquets, moins de failles, transferts plus rapides.

Ne pas tourner en administrateur. Créer un utilisateur dans l'image et l'utiliser. Le conteneur limite les dégâts, il ne les annule pas.

Épingler les versions. python:3.12-slim et non python:latest. Une image dont la base bouge toute seule n'est plus reproductible, et c'est justement ce qu'on cherchait.

Séparer construction et exécution quand un langage compile, pour ne pas embarquer les outils de développement dans l'image finale. Le bloc génie logiciel en donne un exemple complet.

Exercices type

Quelle différence entre une image et un conteneur ?

L'image est un modèle immuable : un empilement de couches en lecture seule, qui contient l'application et ses dépendances. Elle ne s'exécute pas.

Le conteneur est une exécution de cette image, avec une couche modifiable par-dessus, ses propres processus et son propre réseau.

La relation est celle d'une classe à ses instances : une image, autant de conteneurs qu'on veut, tous identiques au départ.

Vous supprimez un conteneur de base de données et les données ont disparu. Pourquoi ?

Parce que le système de fichiers d'un conteneur est créé à son lancement et détruit avec lui. Tout ce qui a été écrit dans cette couche modifiable disparaît.

La correction est de déclarer un volume sur le dossier où le service écrit ses données. Le volume a une existence propre : il survit à la suppression du conteneur et se remonte sur le suivant.

C'est ce que fait volumes: donnees:/var/lib/postgresql/data dans le fichier de composition.

Pourquoi copier requirements.txt avant le reste du code ?

Pour que l'installation des dépendances forme une couche qui ne dépend que de ce fichier.

Le cache est invalidé à la première instruction dont l'entrée a changé, et tout ce qui suit est reconstruit. Si le code est copié avant l'installation, la moindre modification d'une ligne fait réinstaller toutes les dépendances.

En séparant, une modification du code ne reconstruit que la copie du code, soit une seconde au lieu d'une minute.

Un mot de passe a été copié dans l'image puis supprimé à l'instruction suivante. Est-il protégé ?

Non. Une image est un empilement de couches, et la couche qui a ajouté le fichier reste présente. N'importe qui disposant de l'image peut l'extraire.

C'est la même logique qu'un secret commité dans Git : l'effacement ultérieur ne retire rien de l'historique.

Les secrets se fournissent au lancement, par variable d'environnement, par fichier monté ou par un gestionnaire de secrets, et jamais pendant la construction.

L'API n'arrive pas à joindre la base sur localhost:5432 dans une pile composée. Pourquoi ?

Parce que chaque conteneur a sa propre pile réseau. Pour le conteneur de l'API, localhost désigne l'API elle-même, pas la machine hôte ni les autres services.

Dans une pile, les services se joignent par leur nom de service, qui est résolu par le réseau interne : ici base:5432.

C'est aussi ce qui permet de démarrer plusieurs piles en parallèle sans conflit de ports, tant qu'on ne publie pas les mêmes ports vers l'extérieur.

La méthode

  1. --rm par défaut sur les conteneurs d'essai, sinon ils s'accumulent.
  2. Déclare un volume pour tout ce qui doit survivre à l'arrêt.
  3. Ordonne le Dockerfile du plus stable au plus changeant.
  4. Écris un .dockerignore en même temps que le Dockerfile.
  5. Épingle les versions de base, jamais latest.
  6. Passe les secrets au lancement, jamais à la construction.
  7. Décris la pile dans un fichier de composition dès qu'il y a plus d'un service.

En résumé

  • Un conteneur est jetable : son système de fichiers meurt avec lui.
  • Un volume est ce qui survit, et il se déclare explicitement.
  • -p externe:interne publie un port ; l'ordre n'est pas interchangeable.
  • Chaque instruction du Dockerfile produit une couche, réutilisée si son entrée n'a pas changé.
  • Le cache tombe à la première instruction touchée, et tout ce qui suit est reconstruit.
  • Ce qui change rarement se copie en premier, ce qui change souvent en dernier.
  • Une couche conserve ce qu'elle a ajouté, même supprimé ensuite.
  • Dans une pile, les services se joignent par leur nom, pas par localhost.

Et ensuite ? Une image reproductible ne dit pas si le programme est correct. Le chapitre suivant s'occupe de cette vérification : les tests.

Conteneurs et images | Plateforme ETS