0%
Introduction à Docker

Introduction

Les concepts et l'architecture avant les commandes.

10-15 min

Introduction à Docker

« Chez moi ça marche. » Cette phrase résume à elle seule le problème que Docker attaque. Une application ne se réduit jamais à son code : elle dépend d’une version d’interpréteur, d’une bibliothèque système, d’une variable d’environnement, d’un fichier de configuration posé au bon endroit, d’un service de base de données joignable sur un port précis. Ce halo de dépendances n’est presque jamais écrit nulle part. Il vit dans la machine du développeur qui a fait fonctionner le projet le premier, et il se reconstitue à la main, avec plus ou moins de chance, sur chaque nouvelle machine.

La conséquence n’est pas seulement un mauvais moment passé à installer un projet. C’est une classe entière de bugs qui n’existent que sur un environnement : un test qui passe en local et échoue en intégration, un correctif validé en préproduction qui casse en production parce que la version d’OpenSSL n’est pas la même. Tant que l’environnement d’exécution reste implicite, il reste une variable non contrôlée de votre système, et vous déboguez des différences plutôt que du code.

Docker propose une réponse simple à énoncer : rendre l’environnement explicite, versionné, et transportable au même titre que le code. On décrit dans un fichier texte tout ce dont l’application a besoin pour tourner, on en construit un artefact figé, et on exécute cet artefact tel quel partout — poste de développement, serveur d’intégration, production. Ce n’est plus « installe ces sept choses puis lance », c’est « exécute cet artefact ».

Cette première leçon ne contient volontairement presque aucune configuration. Elle installe le vocabulaire et le modèle mental — image, conteneur, couche, registre — parce que la quasi-totalité des blocages que rencontrent les débutants avec Docker viennent d’une confusion sur ces quatre mots, pas d’une option de commande mal orthographiée.

Conteneur ou machine virtuelle : où est la vraie différence

L’analogie « un conteneur, c’est une petite VM » est la plus répandue et la plus coûteuse. Elle est fausse sur le point qui compte : ce qui est virtualisé.

Une machine virtuelle virtualise du matériel. Un hyperviseur présente à l’invité un processeur, de la mémoire, des disques simulés, et cet invité démarre un noyau complet, son propre système d’exploitation, ses services d’amorçage. Deux VM Linux sur un même hôte font tourner deux noyaux Linux distincts.

Un conteneur ne virtualise rien de tout cela. Il utilise le noyau de l’hôte et se contente de lui demander une vue restreinte du système, grâce à deux mécanismes du noyau Linux :

  • les namespaces, qui cloisonnent ce que le processus voit — sa propre arborescence de processus (il se croit PID 1), sa propre pile réseau, ses propres points de montage, son propre nom d’hôte ;
  • les cgroups (control groups), qui limitent ce que le processus consomme — quota de CPU, plafond de mémoire, débit d’entrées-sorties.

Un conteneur est donc un processus ordinaire de l’hôte, isolé et bridé. C’est ce qui explique tous ses écarts de comportement avec une VM :

Machine virtuelleConteneur
Ce qui est virtualiséLe matérielLa vue du système (namespaces)
NoyauUn noyau par VMCelui de l’hôte, partagé
DémarrageDizaines de secondes (amorçage OS)Millisecondes à quelques secondes
Poids sur disquePlusieurs gigaoctetsQuelques mégaoctets à centaines de Mo
Frontière de sécuritéForte (hyperviseur)Plus fine (noyau partagé)
Systèmes hébergeablesN’importe quel OSCeux compatibles avec le noyau hôte

Les deux dernières lignes méritent d’être retenues plutôt que survolées. Parce que le noyau est partagé, une faille d’échappement de conteneur donne accès à l’hôte : pour de l’exécution de code non fiable et multi-locataire, la VM reste la frontière de référence, et l’industrie combine généralement les deux (des conteneurs à l’intérieur de VM). Et parce que le noyau est partagé, une image Linux a besoin d’un noyau Linux. Quand vous lancez un conteneur Linux sur macOS ou Windows, Docker Desktop fait tourner discrètement une machine virtuelle Linux pour vous : le noyau vient de là.

Vous pouvez vérifier vous-même le partage du noyau, une fois Docker installé :

# Version du noyau de votre hôte
uname -r

# Version du noyau vue depuis l'interieur d'un conteneur Ubuntu.
# Le resultat est identique a la ligne du dessus : c'est le meme noyau.
docker run --rm ubuntu:24.04 uname -r

En revanche la distribution, elle, diffère : c’est l’image qui la fournit.

# L'hote est peut-etre une Debian, le conteneur se presente en Ubuntu 24.04
docker run --rm ubuntu:24.04 cat /etc/os-release

Une image ne contient donc pas un système d’exploitation complet — pas de noyau, pas de pilotes. Elle contient l’espace utilisateur d’une distribution : ses bibliothèques, ses binaires, son arborescence de fichiers. D’où sa légèreté.

Image, conteneur, registre : trois objets à ne pas confondre

Trois mots, trois natures différentes. La confusion entre les deux premiers est de très loin la première cause d’incompréhension.

Une image est un modèle en lecture seule : une pile de fichiers figée, plus des métadonnées (commande à lancer, port exposé, variables d’environnement, utilisateur). Une image ne s’exécute pas ; elle ne « tourne » jamais. C’est l’équivalent d’un exécutable sur disque, ou d’une classe en programmation objet.

Un conteneur est une instance en cours d’exécution d’une image. Docker prend l’image, y ajoute une fine couche modifiable en écriture, et démarre le processus déclaré. C’est l’équivalent d’un processus lancé, ou d’un objet instancié. Une même image peut donner naissance à cent conteneurs indépendants.

Un registre est un entrepôt d’images, adressable par le réseau. Docker Hub est le registre public par défaut ; il en existe des privés (GitLab, GitHub, AWS ECR, Harbor). C’est l’équivalent d’un dépôt de paquets comme npm ou PyPI, mais pour des environnements complets.

# 1. Recuperer une image depuis un registre vers le cache local
docker pull nginx:1.27-alpine

# 2. Lister les images presentes localement
docker images

# 3. Instancier DEUX conteneurs a partir de la MEME image,
#    sur deux ports differents de l'hote
docker run -d --name web1 -p 8081:80 nginx:1.27-alpine
docker run -d --name web2 -p 8082:80 nginx:1.27-alpine

# 4. Les voir tourner cote a cote
docker ps

Deux conteneurs, une seule image, aucune duplication des fichiers sur le disque. Modifier un fichier dans web1 ne touche ni web2 ni l’image : l’écriture atterrit dans la couche modifiable propre à web1, et disparaît avec lui.

docker exec web1 sh -c 'echo "modifie" > /usr/share/nginx/html/index.html'

# web1 a change...
docker exec web1 cat /usr/share/nginx/html/index.html

# ...web2 n'a rien vu.
docker exec web2 cat /usr/share/nginx/html/index.html

Ce détail est fondateur : un conteneur est éphémère par conception. Tout ce qu’il écrit dans son propre système de fichiers meurt avec lui. Ce n’est pas une limitation à contourner, c’est la propriété qui rend les conteneurs reproductibles — et c’est la raison d’être des volumes, que la leçon 5 traite en détail.

La référence d’une image : registre, dépôt, tag

Un nom d’image comme nginx:1.27-alpine est une forme abrégée. La forme complète est :

[registre/][espace-de-noms/]depot[:tag]

docker.io/library/nginx:1.27-alpine
│         │       │     │
│         │       │     └── tag : la version
│         │       └──────── depot
│         └──────────────── espace de noms (library = images officielles)
└────────────────────────── registre (docker.io par defaut)

Trois pièges méritent d’être signalés immédiatement.

Le tag latest n’est pas une promesse de fraîcheur : c’est un tag par défaut comme un autre, celui utilisé quand vous n’en précisez aucun. Il est mobile : l’image derrière latest change dans le temps. Deux docker pull à six mois d’écart ne rapportent pas le même contenu, ce qui ruine la reproductibilité recherchée.

Le suffixe -alpine désigne des images bâties sur Alpine Linux, très petites parce qu’elles utilisent la bibliothèque C musl au lieu de glibc. Cette économie a un coût réel : certains binaires précompilés attendent glibc et échouent. Les variantes -slim (base Debian réduite) sont souvent le meilleur compromis.

Enfin, pour verrouiller totalement une image, on peut la référencer par son digest, l’empreinte cryptographique de son contenu, qui ne bouge jamais :

# Un tag est mobile ; un digest est immuable
docker pull nginx@sha256:0c86dddac19f2ce4fb916fcd7015e1272ea67e3a3fbb6f88df3eb0e1c9a5c1d1

# Afficher le digest des images locales
docker images --digests

L’architecture : client, daemon, registre

Quand vous tapez docker run, vous ne lancez pas le conteneur. Vous exécutez un client en ligne de commande, qui envoie une requête HTTP à une API REST exposée par un daemondockerd — lequel fait le travail réel : résoudre l’image, la télécharger si besoin, créer les namespaces et les cgroups, démarrer le processus.

   docker CLI  ──── API REST (socket UNIX) ────>  dockerd  ────>  conteneurs
  (votre shell)   /var/run/docker.sock            (le daemon)     (processus isoles)

                                                      └──── HTTPS ────> registre

Cette séparation n’est pas un détail d’implémentation, elle a trois conséquences pratiques.

D’abord, presque toutes les erreurs du type « Cannot connect to the Docker daemon » ne sont pas des erreurs de commande : le daemon n’est pas démarré, ou votre utilisateur n’a pas le droit d’écrire sur le socket. La leçon suivante règle ce point.

Ensuite, le client peut piloter un daemon distant. C’est ce qui permet de construire une image sur un serveur costaud depuis un portable :

# Piloter un daemon distant via SSH, sans rien installer de plus
docker --host ssh://deploy@build-01.exemple.com ps

# Ou en fixant le contexte de facon persistante
docker context create serveur --docker "host=ssh://deploy@build-01.exemple.com"
docker context use serveur
docker context ls        # affiche le contexte actif
docker context use default

Enfin, il faut savoir que le daemon tourne avec les privilèges du superutilisateur. Donner à un compte l’accès au socket Docker équivaut donc, en pratique, à lui donner la racine sur l’hôte — un point à garder en tête pour tout serveur partagé.

Vue d’ensemble de l’installation en une commande :

# Versions du client et du daemon, systeme de stockage, nombre de conteneurs
docker version
docker info

Le système de couches : pourquoi les images sont légères et rapides

Une image n’est pas un gros fichier monolithique. C’est une pile de couches (layers) empilées et fusionnées à la lecture par un système de fichiers union — overlay2 sur Linux. Chaque instruction de construction produit une couche qui n’enregistre que sa différence avec la précédente.

# Afficher l'historique en couches d'une image, taille par couche
docker history nginx:1.27-alpine

Deux propriétés en découlent, et elles expliquent la moitié des gains de performance de Docker.

Les couches sont partagées entre images. Si dix de vos images dérivent de python:3.12-slim, cette base n’est stockée qu’une fois sur le disque et n’est téléchargée qu’une fois. Vous pouvez l’observer directement : le premier pull d’une famille d’images est long, les suivants annoncent Already exists sur la majorité des couches.

docker pull python:3.12-slim
# Les couches communes s'affichent en "Already exists", pas retelechargees
docker pull python:3.12

Les couches sont immuables et adressées par contenu. Une couche inchangée est réutilisée depuis le cache au lieu d’être reconstruite. C’est le levier central de l’optimisation des Dockerfiles — sujet de la leçon 3 — et la raison pour laquelle l’ordre des instructions dans un Dockerfile change radicalement le temps de build.

Les couches sont aussi additives, jamais soustractives : supprimer un fichier dans une couche ultérieure le masque mais ne le retire pas de l’image. Un secret copié puis « supprimé » reste extractible de l’image. On y reviendra.

Ce que Docker ne fait pas

Poser les limites tout de suite évite des attentes déçues.

Docker n’orchestre pas un parc de machines. Il gère les conteneurs d’un hôte. Répartir des conteneurs sur vingt serveurs, les redémarrer ailleurs quand une machine tombe, monter en charge automatiquement : c’est le travail d’un orchestrateur comme Kubernetes. Docker Compose, vu en leçon 4, coordonne plusieurs conteneurs sur un seul hôte.

Docker ne rend pas une application scalable ou sans état. Conteneuriser un logiciel qui écrit dans un dossier local et garde ses sessions en mémoire donne un conteneur qui écrit dans un dossier local et garde ses sessions en mémoire. Docker rend visibles ces choix d’architecture ; il ne les corrige pas.

Docker n’est pas un bac à sable de sécurité absolu. Par défaut un conteneur tourne en root (à l’intérieur de son namespace), et plusieurs options courantes — --privileged, le montage du socket Docker — ouvrent grand la porte vers l’hôte.

Enfin, Docker n’est plus la seule implémentation. Le format des images et le comportement d’exécution sont normalisés par l’Open Container Initiative. Des outils comme Podman ou containerd consomment les mêmes images. Ce que vous apprenez ici n’est donc pas enfermé dans un produit.

Le cycle de travail complet, en survol

Avant d’entrer dans les commandes, voici la boucle que les cinq leçons suivantes vont dérouler :

  Dockerfile  ──build──>  Image  ──push──>  Registre
                            │                   │
                            │                   pull
                            run                 │
                            v                   v
                        Conteneur  <────────  Serveur de production

Vous écrivez un Dockerfile (leçon 3), vous en construisez une image, vous l’exécutez localement pour vérifier, vous la poussez dans un registre, et un serveur la récupère pour l’exécuter à l’identique (leçon 6). Entre-temps, Compose (leçon 4) vous permet de faire vivre ensemble plusieurs conteneurs, reliés par un réseau et adossés à des volumes (leçon 5).

Voici cette boucle réduite à ses commandes essentielles, à titre de repère — chacune sera reprise en détail :

docker build -t mon-app:1.0 .     # Dockerfile -> image
docker run -d -p 8080:8080 mon-app:1.0   # image -> conteneur
docker push registre.exemple.com/mon-app:1.0   # image -> registre

À retenir

  • Un conteneur n’est pas une petite VM : c’est un processus de l’hôte isolé par les namespaces et bridé par les cgroups, qui utilise le noyau de l’hôte. D’où sa légèreté, sa vitesse de démarrage, et une frontière de sécurité plus fine que celle d’un hyperviseur.
  • Image = modèle figé en lecture seule ; conteneur = instance en exécution de ce modèle ; registre = entrepôt d’images. Une image ne tourne pas, un conteneur ne se partage pas.
  • Tout ce qu’un conteneur écrit dans son système de fichiers disparaît avec lui. C’est voulu, et c’est la raison d’être des volumes.
  • Une image est une pile de couches immuables partagées entre images et mises en cache : le levier majeur pour des builds rapides et des transferts légers.
  • Le client docker ne fait rien lui-même : il parle au daemon dockerd via une API. Ce découplage explique les erreurs de connexion au socket et permet de piloter un hôte distant.
  • Évitez le tag latest dès que la reproductibilité compte : épinglez une version, ou un digest sha256: pour un verrouillage total.

Repère : la documentation officielle est à docs.docker.com. Elle fait référence pour les options de commandes, qui évoluent d’une version à l’autre.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !