Aller au contenu

Comprendre les images Docker et leur structure

Mise à jour :

Quand on commence à vouloir conteneuriser une application, on entend vite parler des images de conteneurs. Mais qu’est-ce que c’est exactement ? Une image, c’est un peu comme un modèle figé : elle contient tout ce qu’il faut pour faire tourner une application – code, dépendances, système de fichiers. C’est à partir de cette image que les conteneurs seront lancés.

Pour bien les utiliser, il faut d’abord comprendre comment elles fonctionnent… et surtout comment les construire soi-même. C’est ce que je vais vous montrer.

Comment est construite une image de conteneur ?

Pour bien comprendre ce qu’on fait quand on écrit un Dockerfile, il faut savoir ce qui se passe sous le capot. Construire une image Docker, ce n’est pas juste enchaîner des commandes. C’est empiler des couches, chacune résultant d’une instruction, à partir d’une image de base.

Tout commence par une image de base

L’image de base, c’est le point de départ. Elle peut être :

  • Une distribution minimaliste comme alpine
  • Une image préconfigurée comme python:3.11-slim ou nginx
  • Ou même scratch, qui signifie… aucune base du tout

On construit une nouvelle image à partir de cette base

Pour construire une image, on va ajouter des couches à cette image de base. Chaque couche est le résultat d’une instruction dans le Dockerfile. Ces instructions sont exécutées dans l’ordre, et chaque instruction crée une nouvelle couche.

Qu’est-ce qu’un Dockerfile ?

Un Dockerfile est un fichier texte qui définit une suite d’instructions qui vont permettre d’installer tous les éléments nécessaires à l’exécution de notre application.

Nous avons à notre disposition une dizaine d’instructions. Si peu ? Oui et c’est suffisant. À cela s’ajoute aussi la possibilité de mettre des commentaires.

Prenons un exemple simple :

FROM debian:bullseye-slim

Cette instruction dit à Docker : « télécharge cette image de base depuis un registre » (par défaut Docker Hub). C’est la première couche, sur laquelle on va construire tout le reste.

Chaque instruction crée une nouvelle couche

À chaque ligne du Dockerfile, Docker va :

  1. Créer un conteneur temporaire
  2. Exécuter l’instruction (par exemple une commande RUN)
  3. Capturer la différence de fichiers
  4. La stocker comme une nouvelle couche (en tar.gz dans les blobs)

Prenons ce Dockerfile :

FROM python:3.11-slim
COPY . /app
RUN pip install -r /app/requirements.txt

Voici ce que fait Docker :

  • 1re couche : image de base python:3.11-slim
  • 2e couche : copie de vos fichiers dans /app
  • 3e couche : installation des dépendances Python

Chaque couche est immutabilité et cacheable. Si on relance le build sans changer les fichiers copiés, Docker réutilisera les couches précédentes.

Comment une image est-elle structurée ?

Pour aller plus loin, on peut extraire une image Docker pour voir comment elle est structurée. Par exemple, prenons l’image nginx:alpine :

Terminal window
docker save nginx:alpine -o nginx-alpine.tar
mkdir nginx-image && tar -xf nginx-alpine.tar -C nginx-image

Une fois extraite, on y trouve :

  • Répertoireblobs/
    • Répertoiresha256/
      • 08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350
      • 0bf5be05f1809d9c54b50ef92deba77c37c22d875842d556edd4ad202b61d033
      • 1ff4bb4faebcfb1f7e01144fa9904a570ab9bab88694457855feb6c6bba3fa07
      • 252b6db79fae151ab547c0f86a873dc97274d8b61f3921158d480b4242fef957
      • 252e179fd8212a3dc5b070bb4d702df97f20b4b52aa65a59cf61c4d85e138b55
      • 4b6e8e243d7d2a5c7af927e2137d9426d039591b0b24a368c6f9d5a3e80132e1
      • 540b632878f7db4231da7c610f2b3271efba724392e56da1569c1d94900870b4
      • 60531a653758d21ddd46bf4c65e7beec5f0e35332409fccf4500e5a02dae1c53
      • 80a2d6b308417d56c551f88704e46ab844d0db3db9ad7f2b5e283da916312e65
      • 84e2259b5616d28e968f0f5162447105736880570791aadcd4be9dabd23a2bbc
      • 8f3c313eb1240a3b86e0c76d0abda7a6fa7df30ad3151e98c4e3725a3fb710dc
      • 9af9e76ea07fe05a1f7660b80ec2417bc3fe500991df4995b0adfa13aade20b6
      • bbf43b514ee66a048d0b4cd55e455bf91481f1c04252e4c1564cf9fc7b3f0213
      • c1761f3c364a963ec0ebd4d728cb6dd5aa24273f7dba0c3dd2fdb8411682ef0a
      • c18897d5e3dd125d3d9f2ca7f361cb6b05cf7fad8ef9bc00548f3eb6f3def644
      • c9ce8cb4e76a801ef89c226cb8657556e62e3bb962b3641b051bb25f13dd1a26
      • f1f70b13aacc43849d4f4ab87a889304a4300210ecd32be5a55305486af5f1ea
      • fabf3656cbba722e0a4e35f33a5d2b67eb772a6608e467014787d0a637d7ea55
  • index.json
  • manifest.json
  • oci-layout
  • repositories
  • manifest.json : décrit les couches et la configuration.
  • repositories : indique le nom de l’image et son tag.
  • Un ou plusieurs dossiers avec des noms hashés : ce sont les couches, chacune contenant :
    • layer.tar : la vraie couche de fichiers
    • des métadonnées (VERSION, json, etc.)

Dans le format OCI Image Layout, chaque élément de l’image est identifié par un digest SHA256. Les fichiers qui sont dans blobs/sha256/, sont des blobs binaires compressés représentant soit :

  • des couches de fichiers (layer.tar.gz)
  • des manifests JSON
  • des configurations de conteneur

Chaque fichier est nommé par le digest SHA256 de son contenu, ce qui garantit l’intégrité et la déduplication.

On peut alors voir les fichiers ajoutés/modifiés à cette étape. En répétant ça pour chaque couche, on comprend exactement ce que chaque commande du Dockerfile a produit.

C’est aussi ce que fait automatiquement l’outil dive : il lit les couches d’une image et affiche ce qui a été ajouté ou modifié, couche par couche.

Description du fichier manifest.json

Ce fichier manifest.json est un élément clé d’une image Docker exportée au format OCI. Il décrit la composition complète de l’image, notamment :

  • le fichier de configuration (Config)
  • les couches de fichiers (Layers)
  • les informations sur chaque blob (LayerSources)
  • les tags associés à l’image (RepoTags)

Notre exemple de manifest.json :

[
{
"Config": "blobs/sha256/1ff4bb4faebcfb1f7e01144fa9904a570ab9bab88694457855feb6c6bba3fa07",
"RepoTags": [
"nginx:alpine"
],
"Layers": [
"blobs/sha256/08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350",
"blobs/sha256/c1761f3c364a963ec0ebd4d728cb6dd5aa24273f7dba0c3dd2fdb8411682ef0a",
"blobs/sha256/8f3c313eb1240a3b86e0c76d0abda7a6fa7df30ad3151e98c4e3725a3fb710dc",
"blobs/sha256/c9ce8cb4e76a801ef89c226cb8657556e62e3bb962b3641b051bb25f13dd1a26",
"blobs/sha256/252b6db79fae151ab547c0f86a873dc97274d8b61f3921158d480b4242fef957",
"blobs/sha256/f1f70b13aacc43849d4f4ab87a889304a4300210ecd32be5a55305486af5f1ea",
"blobs/sha256/9af9e76ea07fe05a1f7660b80ec2417bc3fe500991df4995b0adfa13aade20b6",
"blobs/sha256/c18897d5e3dd125d3d9f2ca7f361cb6b05cf7fad8ef9bc00548f3eb6f3def644"
],
"LayerSources": {
"sha256:08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 8120832,
"digest": "sha256:08000c18d16dadf9553d747a58cf44023423a9ab010aab96cf263d2216b8b350"
},
"sha256:252b6db79fae151ab547c0f86a873dc97274d8b61f3921158d480b4242fef957": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 2560,
"digest": "sha256:252b6db79fae151ab547c0f86a873dc97274d8b61f3921158d480b4242fef957"
},
"sha256:8f3c313eb1240a3b86e0c76d0abda7a6fa7df30ad3151e98c4e3725a3fb710dc": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 3584,
"digest": "sha256:8f3c313eb1240a3b86e0c76d0abda7a6fa7df30ad3151e98c4e3725a3fb710dc"
},
"sha256:9af9e76ea07fe05a1f7660b80ec2417bc3fe500991df4995b0adfa13aade20b6": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 7168,
"digest": "sha256:9af9e76ea07fe05a1f7660b80ec2417bc3fe500991df4995b0adfa13aade20b6"
},
"sha256:c1761f3c364a963ec0ebd4d728cb6dd5aa24273f7dba0c3dd2fdb8411682ef0a": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 4504064,
"digest": "sha256:c1761f3c364a963ec0ebd4d728cb6dd5aa24273f7dba0c3dd2fdb8411682ef0a"
},
"sha256:c18897d5e3dd125d3d9f2ca7f361cb6b05cf7fad8ef9bc00548f3eb6f3def644": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 36649472,
"digest": "sha256:c18897d5e3dd125d3d9f2ca7f361cb6b05cf7fad8ef9bc00548f3eb6f3def644"
},
"sha256:c9ce8cb4e76a801ef89c226cb8657556e62e3bb962b3641b051bb25f13dd1a26": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 4608,
"digest": "sha256:c9ce8cb4e76a801ef89c226cb8657556e62e3bb962b3641b051bb25f13dd1a26"
},
"sha256:f1f70b13aacc43849d4f4ab87a889304a4300210ecd32be5a55305486af5f1ea": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 5120,
"digest": "sha256:f1f70b13aacc43849d4f4ab87a889304a4300210ecd32be5a55305486af5f1ea"
}
}
}
]```
Voici une explication détaillée de chacune des parties de ce fichier `manifest.json` :
### `"Config"` : métadonnées de l’image
```json
"Config": "blobs/sha256/1ff4bb4faebcfb1f7e01144fa9904a570ab9bab88694457855feb6c6bba3fa07"

Ce fichier (un blob JSON) contient toutes les instructions du Dockerfile compilées : la commande par défaut (CMD), les variables d’environnement, les volumes, les labels, etc. C’est le cerveau du conteneur.

"RepoTags" : nom et version de l’image

"RepoTags": ["nginx:alpine"]

Cela indique que l’image correspond au tag nginx:alpine. C’est le nom que vous verriez avec docker images.

"Layers" : couches empilées de l’image

"Layers": [
"blobs/sha256/08000c18d16da...",
"blobs/sha256/c1761f3c364a...",
...
]

Chaque fichier listé ici correspond à une couche de fichiers (layer.tar) ajoutée lors du docker build. Elles sont appliquées dans l’ordre, de la base vers le haut. Le contenu de ces archives représente les ajouts ou suppressions de fichiers à chaque étape du build.

"LayerSources" : métadonnées des couches

Cette section associe à chaque digest :

  • le type MIME du fichier (mediaType)
  • la taille en octets
  • le digest SHA256, qui sert d’identifiant unique

Exemple :

"sha256:c18897d5e3dd125...": {
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"size": 36649472,
"digest": "sha256:c18897d5e3dd125..."
}

Cela indique que la couche contient une archive .tar, non compressée ici, de 36 Mo environ.

En résumé

Cette image est donc composée de :

  • 8 couches, empilées dans un ordre précis
  • Un fichier de configuration global
  • Un tag nginx:alpine associé
  • Des métadonnées bien définies pour chaque élément

Et techniquement, Docker ou un autre moteur (comme containerd) reconstruit le système de fichiers final en appliquant ces couches les unes après les autres, sur la base des instructions du Config.

Conclusion

Vous voilà maintenant armé pour comprendre ce qu’est vraiment une image de conteneur. On a vu ensemble comment elle est structurée, comment elle est construite à partir d’une image de base, couche par couche, et comment Docker assemble tout cela de manière transparente.

On a également exploré la logique derrière le manifest.json, les couches dans les blobs/sha256/, et les bonnes pratiques de construction, sans encore plonger dans le détail de chaque instruction.

À mon avis, maîtriser cette partie théorique est indispensable avant de passer à l’action. Et maintenant que vous savez ce qu’est une image Docker, il est temps de passer à l’étape suivante : écrire vos propres images en apprenant à manier la syntaxe complète du Dockerfile.