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
ounginx
- 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 :
- Créer un conteneur temporaire
- Exécuter l’instruction (par exemple une commande
RUN
) - Capturer la différence de fichiers
- La stocker comme une nouvelle couche (en tar.gz dans les blobs)
Prenons ce Dockerfile :
FROM python:3.11-slimCOPY . /appRUN 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
:
docker save nginx:alpine -o nginx-alpine.tarmkdir 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.