Les images : couches, étiquettes, empreintes et registres
Pourquoi
Jusqu'ici, nous avons utilisé les images comme des boîtes noires : alpine:3.22, postgres:18, et cela fonctionnait. En exploitation, cette insouciance coûte cher :
- un serveur de recette et un serveur de production qui lancent tous les deux
api:2.3exécutent deux logiciels différents, parce que l'étiquette a été réécrite entre-temps ; - une chaîne CI s'arrête un matin avec une erreur
429 Too Many Requestsparce que Docker Hub limite les téléchargements anonymes ; - le disque d'un poste de développement contient 100 Go d'images oubliées ;
- une image construite sur un Mac M3 refuse de démarrer sur un serveur x86.
Toutes ces situations se comprennent dès qu'on sait ce qu'est une image au niveau des octets. C'est l'objet de cette leçon : nous allons démonter une image directement depuis l'API du registre, avec curl, puis observer comment Docker l'assemble sur le disque.
Les concepts
Une référence d'image
Une référence complète a cette forme :
rg.fr-par.scw.cloud/lyneko-apps/apprendre:main-84d3170@sha256:5291449c...
└────────┬────────┘ └─────────┬─────────┘ └────┬─────┘ └──────┬───────┘
registre dépôt étiquette empreinte- Le registre est le serveur qui stocke les images. S'il est omis, Docker utilise
docker.io(Docker Hub). - Le dépôt (repository) regroupe les versions d'une même image. Sur Docker Hub, les images officielles vivent dans l'espace
library/:alpinesignifie en réalitédocker.io/library/alpine. - L'étiquette (tag) est un nom lisible pointant vers une version. Si elle est omise, Docker utilise
latest, qui n'a rien de magique : c'est une étiquette comme une autre, que l'éditeur peut faire pointer où il veut, ou ne pas publier du tout. - L'empreinte (digest) est le hachage SHA-256 du contenu. Elle identifie une image de façon unique et immuable.
La différence entre les deux dernières est fondamentale : une étiquette est un pointeur mobile, une empreinte est une identité. Une étiquette peut être déplacée à tout moment par quiconque a le droit de pousser dans le dépôt ; une empreinte désigne pour toujours les mêmes octets, puisqu'elle en est calculée.
Les objets OCI : un graphe adressé par contenu
Une image OCI n'est pas un fichier unique, mais un petit graphe d'objets, chacun désigné par l'empreinte SHA-256 de son contenu :
flowchart TB
I["Index<br/>(liste des plateformes)"] --> M1["Manifeste linux/amd64"]
I --> M2["Manifeste linux/arm64"]
I --> A["Attestations<br/>(provenance, SBOM)"]
M1 --> C1["Configuration<br/>(Cmd, Env, User, historique, diff_ids)"]
M1 --> L1["Couche 1<br/>(archive tar.gz)"]
M1 --> L2["Couche 2"]
M2 --> C2["Configuration"]
M2 --> L3["Couches arm64..."]
- L'index (image index, autrefois manifest list) liste une variante de l'image par plateforme. C'est lui que désigne une étiquette multi-architecture.
- Le manifeste d'une plateforme liste les empreintes de sa configuration et de ses couches.
- La configuration contient les valeurs par défaut vues à la leçon 5 (
Cmd,Entrypoint,Env,User...), l'historique de construction et la liste des empreintes des couches décompressées (diff_ids). - Chaque couche est une archive tar (généralement compressée) des fichiers ajoutés, modifiés ou supprimés par une étape de construction.
Comme chaque objet est désigné par l'empreinte de son contenu et que chaque objet parent contient les empreintes de ses enfants, l'empreinte de l'index garantit l'intégrité de toute l'image : modifier un seul octet d'une couche change son empreinte, donc le manifeste, donc l'index. C'est le même principe que les commits de Git ou les arbres de Merkle.
Les couches et la copie à l'écriture
Les couches s'empilent pour former le système de fichiers du conteneur grâce à overlayfs, un système de fichiers du noyau qui superpose plusieurs répertoires :
Vue du conteneur (/) ← ce que voit le processus
┌─────────────────────────────────┐
│ upperdir : couche inscriptible │ ← propre au conteneur, supprimée avec lui
├─────────────────────────────────┤
│ lowerdir : couche N de l'image │ ┐
│ ... │ ├ lecture seule, partagées par tous
│ lowerdir : couche 1 de l'image │ ┘ les conteneurs de cette image
└─────────────────────────────────┘- À la lecture, overlayfs cherche le fichier de haut en bas et rend la première version trouvée.
- À la première modification d'un fichier venant de l'image, overlayfs le copie entièrement dans la couche inscriptible (copy-up), puis modifie la copie. Les couches de l'image ne changent jamais.
- Une suppression est enregistrée dans la couche inscriptible sous la forme d'un marqueur (whiteout) qui masque le fichier des couches inférieures. Le fichier existe toujours dans l'image : supprimer un fichier dans une couche ne réduit jamais la taille d'une image.
Deux conséquences pratiques en découlent. Dix conteneurs de la même image ne consomment l'espace de l'image qu'une fois. Et la couche inscriptible n'est pas faite pour des écritures intensives : la copie d'un gros fichier au premier accès est coûteuse, et tout disparaît avec le conteneur. Les données vont dans des volumes (leçon 9).
Les registres
Un registre est un serveur HTTP qui implémente la spécification de distribution OCI : quelques points d'API pour lire et écrire des manifestes et des blobs (les couches et configurations). Les principaux :
| Registre | Type | Remarque |
|---|---|---|
Docker Hub (docker.io) | Public, hébergé aux États-Unis | Images officielles ; limites de téléchargement |
GitHub Container Registry (ghcr.io) | Hébergé | Lié aux dépôts et droits GitHub |
Scaleway Container Registry (rg.fr-par.scw.cloud) | Hébergé en France | Utilisé par Lyneko ; espaces de noms publics ou privés |
| Harbor, Zot, CNCF Distribution | Auto-hébergés | Libres ; Harbor ajoute analyse de vulnérabilités, réplication, RBAC |
Docker Hub limite les téléchargements : 100 par période de 6 heures par adresse IPv4 (ou sous-réseau IPv6 /64) sans authentification, 200 avec un compte gratuit. Une chaîne CI dont tous les agents sortent par la même adresse IP atteint vite cette limite.
En pratique
Démonter une image depuis l'API du registre
Plutôt que de croire le schéma, allons chercher l'image alpine:3.22 directement auprès de Docker Hub, avec curl et jq. Ce script est rejouable tel quel :
#!/usr/bin/env bash
set -euo pipefail
REPO=library/alpine
TAG=3.22
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$REPO:pull" | jq -r .token)
api() { curl -sL -H "Authorization: Bearer $TOKEN" -H "Accept: $1" "https://registry-1.docker.io/v2/$REPO/$2"; }
echo "## 1. L'index"
api application/vnd.oci.image.index.v1+json manifests/$TAG > index.json
sha256sum index.json
jq -r '.manifests[] | select(.platform.os != "unknown") | "\(.platform.architecture)\(.platform.variant // "")\t\(.digest)"' index.json
echo "## 2. Le manifeste amd64"
M=$(jq -r '.manifests[] | select(.platform.architecture=="amd64") | .digest' index.json)
api application/vnd.oci.image.manifest.v1+json manifests/$M > manifeste.json
jq '{config: .config.digest, layers: [.layers[] | {mediaType, digest, size}]}' manifeste.json
echo "## 3. La configuration"
C=$(jq -r .config.digest manifeste.json)
api application/json blobs/$C > config.json
jq '{architecture, os, Cmd: .config.Cmd, rootfs, history}' config.json
echo "## 4. La couche"
L=$(jq -r '.layers[0].digest' manifeste.json)
curl -sL -H "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/$REPO/blobs/$L" -o couche.tar.gz
echo "attendu : $L"
echo "calculé : sha256:$(sha256sum couche.tar.gz | cut -d' ' -f1)"
echo "diff_id : sha256:$(gunzip -c couche.tar.gz | sha256sum | cut -d' ' -f1)"
tar -tzf couche.tar.gz | head -5 || trueLe registre de Docker Hub exige un jeton, même pour une image publique : on le demande anonymement au service d'authentification, pour le seul droit pull sur ce dépôt. L'en-tête Accept indique le type d'objet attendu. -L suit les redirections : les couches sont servies par un réseau de diffusion de contenu, pas par le registre lui-même.
1. L'index.
## 1. L'index
5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 index.json
amd64 sha256:3e9b4b680bfc9fb5269227cffbd6d42be39fbf7c0b908123913864aa4447e764
armv6 sha256:450c744b1ef46c709ee72b733c54813f149999273fffb17f2097f79160aba27a
armv7 sha256:947bab19f99aef448855af6d1886613d95d311a1b8bf9d32e7b329eb76ab4e44
arm64v8 sha256:2e1a7aa4cbc4e9e5222bb4c24a839aa1a6170ea5492d644777ce7b178824e44f
386 sha256:1136d3a024321ad150667cedbb3828db613d57391e471fd87af096a88e2adce5
ppc64le sha256:d3f9354d41e5bc6cd8b4e7127860553fa3c3a76369c4bedca0b01d8922b29627
riscv64 sha256:ddd567990d0fe41158fd851e03e23f1a65c60cb9dd152afe318c9476ecb85e7f
s390x sha256:5fd1c1a839a5c24fe563cb20862fca94368fa229800ad960bdc22fe166b06d6cHuit plateformes. Le filtre select(.platform.os != "unknown") écarte les entrées unknown/unknown, qui sont des attestations (provenance de la construction, inventaire logiciel) attachées à chaque variante. Et regardez la première ligne : le SHA-256 du fichier index.json que nous venons de télécharger, 5291449c..., est exactement l'empreinte que Docker avait affichée lors du tout premier docker run alpine:3.22 de la leçon 1 (Digest: sha256:5291449c3df7...). L'empreinte d'une image n'est rien d'autre que le hachage de son index.
2. Le manifeste amd64.
{
"config": "sha256:c83674e1999044d33d751661371b873539f47e5b5c5ca3320c7e0377acca6238",
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:53f8f5e03afd86ade91b7aa57a749f5a3d1419c113be5d8c10e7ee61bb5ab887",
"size": 3792075
}
]
}Alpine tient en une seule couche de 3,8 Mo compressés. Le préfixe 53f8f5e03afd figurait aussi dans la sortie du premier téléchargement (53f8f5e03afd: Pull complete) : Docker affiche les couches par le début de leur empreinte.
3. La configuration.
{
"architecture": "amd64",
"os": "linux",
"Cmd": [
"/bin/sh"
],
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:e477571b896b8d18635f33e4efb8851e6c4929111b0a4fe9b17b34a16fd9c37f"
]
},
"history": [
{
"created": "2026-09-17T20:37:44.212246058Z",
"created_by": "ADD alpine-minirootfs-3.22.6-x86_64.tar.gz / # buildkit",
"comment": "buildkit.dockerfile.v0"
},
{
"created": "2026-09-17T20:37:44.212246058Z",
"created_by": "CMD [\"/bin/sh\"]",
"comment": "buildkit.dockerfile.v0",
"empty_layer": true
}
]
}On retrouve le Cmd de la leçon 5, et l'historique de construction : l'image a été faite avec un Dockerfile minimal : FROM scratch (qui ne laisse pas de trace dans l'historique), un ADD de l'archive minirootfs d'Alpine et un CMD. La seconde étape ne produit pas de couche (empty_layer) : elle ne modifie que la configuration.
4. La couche.
## 4. La couche
attendu : sha256:53f8f5e03afd86ade91b7aa57a749f5a3d1419c113be5d8c10e7ee61bb5ab887
calculé : sha256:53f8f5e03afd86ade91b7aa57a749f5a3d1419c113be5d8c10e7ee61bb5ab887
diff_id : sha256:e477571b896b8d18635f33e4efb8851e6c4929111b0a4fe9b17b34a16fd9c37f
bin/
bin/arch
bin/ash
bin/base64
bin/bbconfigLe hachage calculé sur l'archive téléchargée est identique à celui annoncé par le manifeste : c'est exactement la vérification que fait Docker à chaque téléchargement, et la raison pour laquelle un registre ou un intermédiaire ne peut pas altérer une couche sans être détecté. Le hachage de l'archive décompressée donne le diff_id listé dans la configuration. La couche n'est qu'une archive tar ordinaire : bin/, bin/ash... le système de fichiers d'Alpine.
Docker range ces mêmes informations localement :
$ docker image inspect alpine:3.22 --format '{{.ID}} {{json .RootFS.Layers}}'
sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 ["sha256:e477571b896b8d18635f33e4efb8851e6c4929111b0a4fe9b17b34a16fd9c37f"]
Avec le magasin d'images containerd (Docker 29), l'identifiant local de l'image est l'empreinte de son index.
Une image multi-architecture, vue par Docker
$ docker image ls --tree nginx:1.29
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
nginx:1.29 1881968aff6f 240MB 65.8MB
├─ linux/amd64 ab15d428b6a7 237MB 63MB
├─ linux/arm/v5 ffc53e3b7885 0B 0B
├─ linux/arm/v7 cb4a8f310edf 0B 0B
├─ linux/arm64/v8 c5c2b964a499 0B 0B
├─ linux/386 b3e4fcd73ae4 0B 0B
├─ linux/ppc64le aa5111c369c4 0B 0B
├─ linux/riscv64 6b6f696d2fc4 0B 0B
└─ linux/s390x 525129cffca2 0B 0B
Docker connaît l'index complet, mais n'a téléchargé que la variante de sa plateforme (linux/amd64, 63 Mo compressés, 237 Mo décompressés). Les annotations OCI de chaque manifeste renseignent sur l'origine de l'image :
$ docker buildx imagetools inspect nginx:1.29
Name: docker.io/library/nginx:1.29
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:1881968aff6f7cdcc4b888c00a11f4ce241ad7ec957e0cb4a9e19e93a3ff87ea
Manifests:
Name: docker.io/library/nginx:1.29@sha256:ab15d428b6a7121511a38221591a41f835933ac16f996fafd102d128a0fa20f7
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/amd64
Annotations:
com.docker.official-images.bashbrew.arch: amd64
org.opencontainers.image.base.digest: sha256:486b1c3d3a6a836d2518d5ac1a7b522050a034ae83b47c063fd45d550b2b9dbf
org.opencontainers.image.base.name: debian:trixie-slim
org.opencontainers.image.created: 2026-05-08T19:22:14Z
org.opencontainers.image.revision: 71081b25390771f6b1275ddf7c73c965f304493f
org.opencontainers.image.source: https://github.com/nginx/docker-nginx.git#71081b25390771f6b1275ddf7c73c965f304493f:mainline/debian
org.opencontainers.image.url: https://hub.docker.com/_/nginx
org.opencontainers.image.version: 1.29.8
...
(Sortie abrégée.) Image de base (debian:trixie-slim, avec son empreinte), date de construction, commit exact du dépôt source : ces annotations normalisées org.opencontainers.image.* permettent de remonter de l'image à son code. Pensez à les renseigner dans vos propres images (cours suivant).
Lire les couches d'une image avec docker history
$ docker history postgres:18 --format '{{.Size}}\t{{.CreatedBy}}' --no-trunc | cut -c1-95
0B CMD ["postgres"]
0B EXPOSE map[5432/tcp:{}]
0B STOPSIGNAL SIGINT
0B ENTRYPOINT ["docker-entrypoint.sh"]
16.4kB RUN /bin/sh -c ln -sT docker-ensure-initdb.sh /usr/local/bin/docker-enforce-initdb.sh #
36.9kB COPY docker-entrypoint.sh docker-ensure-initdb.sh /usr/local/bin/ # buildkit
0B VOLUME [/var/lib/postgresql]
0B ENV PGDATA=/var/lib/postgresql/18/docker
12.3kB RUN /bin/sh -c install --verbose --directory --owner postgres --group postgres --mode 37
106kB RUN /bin/sh -c set -eux; dpkg-divert --add --rename --divert "/usr/share/postgresql/post
342MB RUN /bin/sh -c set -ex; export PYTHONDONTWRITEBYTECODE=1; dpkgArch="$(dpkg --print-ar
0B ENV PG_VERSION=18.6-1.pgdg13+2
...
87.6MB # debian.sh --arch 'amd64' out/ 'trixie' '@1789689600'
(Sortie abrégée.) L'historique se lit de bas en haut : la couche Debian de base (87,6 Mo), puis les étapes successives, jusqu'au CMD final. Une seule étape pèse 342 Mo : l'installation de PostgreSQL. Les lignes à 0B ne modifient que la configuration. On repère aussi le VOLUME qui explique le volume anonyme de la leçon 6, et le STOPSIGNAL SIGINT, le signal d'arrêt rapide de PostgreSQL.
docker history est le premier outil pour comprendre pourquoi une image est lourde. Il a une limite : il montre les commandes, pas les fichiers. Des outils comme dive explorent le contenu de chaque couche.
La copie à l'écriture, mesurée
Lançons deux conteneurs de la même image, et modifions l'un d'eux :
$ docker run -d --name c1 alpine:3.22 sleep 300
$ docker run -d --name c2 alpine:3.22 sleep 300
$ docker exec c1 sh -c 'echo "# modifié" >> /etc/profile; rm /etc/motd; mkdir /donnees; dd if=/dev/zero of=/donnees/gros bs=1M count=20 2>/dev/null'
$ docker diff c1
C /etc
C /etc/profile
D /etc/motd
A /donnees
A /donnees/gros
$ docker exec c2 tail -1 /etc/profile
unset script
$ docker ps -s --filter name=c1 --filter name=c2 --format 'table {{.Names}}\t{{.Size}}'
NAMES SIZE
c2 4.1kB (virtual 8.99MB)
c1 21MB (virtual 30MB)
$ docker rm -f c1 c2
/etc/profile, modifié, a été copié dans la couche dec1(C) ;c2voit toujours l'original./etc/motdest marqué supprimé (D) dansc1, mais il est toujours dans l'image.docker ps -saffiche la taille de la couche inscriptible, puis entre parenthèses la taille « virtuelle » (image + couche). Les 8,99 Mo de l'image ne sont stockés qu'une fois pour les deux conteneurs ; seuls les 21 Mo écrits parc1lui sont propres.
Pousser dans un registre
Pour s'exercer sans compte, faisons tourner un registre localement avec l'image registry:3 (le projet Distribution de la CNCF, qui sert de base à de nombreux registres) :
$ docker run -d --name registre -p 127.0.0.1:5000:5000 registry:3
Pousser une image, c'est d'abord lui donner un nom qui désigne ce registre, avec docker tag, qui crée un nouveau nom pour la même image sans rien copier :
$ docker tag alpine:3.22 localhost:5000/lyneko/outil:1.0
$ docker push localhost:5000/lyneko/outil:1.0
...
Info -> Not all multiplatform-content is present and only the available single-platform image was pushed
sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8 -> sha256:3e9b4b680bfc9fb5269227cffbd6d42be39fbf7c0b908123913864aa4447e764
$ curl -s http://localhost:5000/v2/_catalog
{"repositories":["lyneko/outil"]}
$ curl -s http://localhost:5000/v2/lyneko/outil/tags/list
{"name":"lyneko/outil","tags":["1.0"]}
Le message Info est instructif : localement, Docker ne possède que la variante amd64 de l'index Alpine. Il ne peut donc pas pousser l'index complet (5291449c...) et pousse à la place le seul manifeste amd64 (3e9b4b68...), le même que celui trouvé dans l'index tout à l'heure. L'image poussée n'est plus multi-architecture.
Une étiquette peut mentir
Maintenant, quelqu'un (une erreur de CI, un collègue pressé, un attaquant qui a obtenu le droit de pousser) réutilise la même étiquette pour une autre image :
$ docker tag busybox:1.37 localhost:5000/lyneko/outil:1.0
$ docker push localhost:5000/lyneko/outil:1.0
...
sha256:bdf57e528e45e4433820e045b29b4597825a1c9e38353532d90a01445013f82e -> sha256:66a6306db78bf2dbf3487f293aa8d6990d8e506fdffab9cc43fe422becf886e4
Le registre a accepté sans broncher. Sur un serveur qui récupère outil:1.0, on obtient désormais BusyBox :
$ docker pull localhost:5000/lyneko/outil:1.0
$ docker run --rm localhost:5000/lyneko/outil:1.0 sh -c 'head -1 /etc/os-release 2>/dev/null || busybox | head -1'
BusyBox v1.37.0 (2024-09-26 21:31:42 UTC) multi-call binary.
Mais une référence par empreinte désigne toujours l'image d'origine :
$ docker run --rm localhost:5000/lyneko/outil@sha256:3e9b4b680bfc9fb5269227cffbd6d42be39fbf7c0b908123913864aa4447e764 head -1 /etc/os-release
NAME="Alpine Linux"
$ docker image ls localhost:5000/lyneko/outil --digests --format 'table {{.Repository}}\t{{.Tag}}\t{{.Digest}}'
REPOSITORY TAG DIGEST
localhost:5000/lyneko/outil <none> sha256:3e9b4b680bfc9fb5269227cffbd6d42be39fbf7c0b908123913864aa4447e764
localhost:5000/lyneko/outil 1.0 sha256:66a6306db78bf2dbf3487f293aa8d6990d8e506fdffab9cc43fe422becf886e4
Voilà pourquoi la règle de production est d'épingler par empreinte (ou d'utiliser un registre qui interdit la réécriture des étiquettes, une option que proposent par exemple Harbor ou Amazon ECR). On écrit souvent les deux, pour la lisibilité : alpine:3.22@sha256:5291449c.... Docker ignore alors l'étiquette et vérifie l'empreinte. Supprimez le registre de test : docker rm -f registre.
Archiver une image sans registre
Pour transférer une image vers une machine isolée (un réseau d'administration sans accès à Internet, situation courante chez les clients publics), docker save produit une archive :
$ docker save alpine:3.22 -o alpine.tar
$ tar -tf alpine.tar | head -8
blobs/
blobs/sha256/
blobs/sha256/0629b44bdb87903cfdaa4c97ce1a17a8a343977962ffec3d8b1ea3a703986a23
blobs/sha256/136a7e91b81ee0a601b537ae15392eca6a3c8f6e8496f1f256acf29160bc1fe1
blobs/sha256/3e9b4b680bfc9fb5269227cffbd6d42be39fbf7c0b908123913864aa4447e764
blobs/sha256/5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
blobs/sha256/53f8f5e03afd86ade91b7aa57a749f5a3d1419c113be5d8c10e7ee61bb5ab887
blobs/sha256/b2e1ce860133129476df71a596793a37c2fde5100e14faeb746048197d8637ce
On reconnaît nos objets : l'index 5291449c..., le manifeste 3e9b4b68..., la couche 53f8f5e0.... L'archive suit le format OCI Image Layout : un répertoire de blobs nommés par leur empreinte. docker load -i alpine.tar la réimporte de l'autre côté.
L'espace disque
Les images s'accumulent vite. Voici l'état réel du poste de développement sur lequel ce cours a été écrit :
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 129 11 115.9GB 100GB (86%)
Containers 12 12 679.4MB 0B (0%)
Local Volumes 38 8 7.258GB 6.768GB (93%)
Build Cache 1025 0 164.4GB 142.5GB
Près de 290 Go, dont l'essentiel récupérable : des images qu'aucun conteneur n'utilise, et surtout le cache de construction. Les commandes de nettoyage :
docker image prunesupprime les images orphelines (dangling) : celles qui n'ont plus d'étiquette, typiquement les anciennes versions d'une image reconstruite sous le même nom ;docker image prune -asupprime toutes les images qu'aucun conteneur n'utilise, y compris étiquetées : elles seront téléchargées de nouveau au besoin ;docker builder prunevide le cache de construction ;docker system prunecombine conteneurs arrêtés, réseaux inutilisés, images orphelines et cache.
Warning
Les commandes prune sont globales à la machine. Sur un poste partagé entre plusieurs projets, ou sur un serveur, elles suppriment aussi ce qui appartient aux autres : un conteneur arrêté que quelqu'un comptait relancer, un volume anonyme contenant des données. Lisez la confirmation, et préférez des filtres (--filter until=720h, --filter label=projet=x). docker volume prune mérite une prudence particulière : un volume, ce sont des données.
Sous le capot
Avec le magasin d'images containerd, les objets sont rangés en deux endroits :
- le content store (
/var/lib/containerd/io.containerd.content.v1.content/blobs/sha256/) garde les objets tels que téléchargés : index, manifestes, configurations, couches compressées, chacun dans un fichier nommé par son empreinte, exactement comme dans l'archive dedocker save; - le snapshotter overlayfs (
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/<n>/fs) garde les couches décompressées, une par répertoire, prêtes à être empilées.
Au démarrage d'un conteneur, containerd crée un nouveau snapshot vide pour la couche inscriptible, puis monte un overlay dont les lowerdir sont les snapshots des couches de l'image, du plus haut au plus bas. C'est le montage que nous avons vu dans la sortie de mount à la leçon 2. Comme les couches sont identifiées par leur contenu, deux images qui partagent la même image de base partagent aussi ses snapshots sur le disque.
Un téléchargement (docker pull) suit le chemin que notre script a parcouru à la main : résolution de l'étiquette en index, choix du manifeste de la plateforme locale, téléchargement en parallèle des couches absentes du content store (d'où les lignes Already exists quand une couche est déjà là), vérification de chaque empreinte, puis décompression dans le snapshotter.
Pièges courants
latest en production. latest n'est pas « la dernière version » : c'est l'étiquette par défaut, que l'éditeur fait pointer où il veut. Deux docker pull à une semaine d'intervalle peuvent ramener deux versions majeures différentes. N'utilisez jamais latest, ni implicitement (docker run nginx), dans un fichier qui sera relu ou déployé.
exec format error. L'image ne contient pas de variante pour la plateforme de l'hôte (cas d'une image construite sur un Mac ARM et poussée sans index multi-architecture), ou l'on a forcé --platform. Vérifiez avec docker image ls --tree ou docker buildx imagetools inspect.
toomanyrequests sur Docker Hub. L'erreur 429 Too Many Requests signale la limite de téléchargements anonymes. Solutions : s'authentifier (docker login), utiliser un miroir ou un cache de registre (Harbor en mode proxy, ou l'option registry-mirrors de daemon.json), ou recopier les images de base utilisées dans votre propre registre.
Croire qu'un rm dans une couche allège l'image. Le fichier reste dans la couche où il a été ajouté ; la suppression ne fait qu'ajouter un marqueur. C'est aussi un piège de sécurité : un secret copié dans une couche puis supprimé dans la suivante reste lisible par quiconque télécharge l'image. Le cours suivant montre comment l'éviter (constructions multi-étapes, secrets de construction).
Une étiquette locale périmée. docker run nginx:1.29 utilise l'image locale si elle existe, sans vérifier le registre. Si 1.29 a été mise à jour en amont depuis, vous lancez l'ancienne. docker pull (ou docker run --pull always) force la vérification.
Sécurité
- L'empreinte est votre garantie d'intégrité. Elle protège contre la réécriture d'étiquette, qu'elle soit accidentelle ou malveillante, et contre l'altération en transit. Elle ne dit rien, en revanche, de qui a construit l'image : c'est le rôle des signatures (Sigstore Cosign, Notation), traitées dans le chapitre Livrer avec la sécurité de la chaîne d'approvisionnement.
- Choisissez vos sources. Les Docker Official Images (
library/) et les éditeurs vérifiés sont maintenus et reconstruits régulièrement. Une image publiée par un inconnu peut contenir n'importe quoi, y compris un mineur de cryptomonnaie : des campagnes de ce type ont été retrouvées à de nombreuses reprises sur Docker Hub. - Une image se périme. Ses paquets accumulent des vulnérabilités connues. Épingler par empreinte garantit la reproductibilité, pas la fraîcheur : il faut un processus pour mettre à jour les empreintes (des outils comme Renovate ou Dependabot ouvrent automatiquement les demandes de mise à jour) et analyser les images (Trivy, Grype), traités dans le cours Gestion des vulnérabilités.
- Souveraineté. Pour des clients publics ou soumis à des exigences de localisation des données, un registre hébergé en France (Scaleway Container Registry, ou un Harbor auto-hébergé) évite de dépendre d'un service extra-européen pour chaque déploiement, et peut servir de cache pour les images publiques.
En production
- Un registre privé par organisation, proche des serveurs qui consomment les images. Chez Lyneko, les images applicatives sont poussées par la CI dans
rg.fr-par.scw.cloud, et les manifestes de déploiement référencent une étiquette unique par construction (main-<sha du commit>), jamais réécrite. - Étiquettes immuables, empreintes dans les déploiements. Configurez le registre pour refuser la réécriture d'étiquettes quand il le permet, et faites résoudre les étiquettes en empreintes par l'outillage de déploiement.
- Images multi-architectures dès qu'il existe des postes ARM ou des nœuds ARM (souvent moins chers chez les fournisseurs de cloud) : le cours suivant montre comment les construire avec
docker buildx. - Rétention. Un registre grossit indéfiniment si l'on ne supprime rien. Définissez une politique (garder les N dernières versions, plus celles déployées) et ne nettoyez jamais une image encore référencée par un environnement.
Exercices
1. Décomposer des références. Pour chacune, donnez le registre, le dépôt complet, l'étiquette et dites si elle est immuable : (a) python:3.14-slim ; (b) ghcr.io/lyneko-team/cvizer:1.4.0 ; (c) rg.fr-par.scw.cloud/lyneko-apps/pricer@sha256:4f1c... ; (d) mon-registre:5000/outil.
Solution
(a) docker.io, library/python, 3.14-slim, mutable (l'étiquette avance à chaque correctif de Python 3.14 ou de Debian). (b) ghcr.io, lyneko-team/cvizer, 1.4.0, mutable par nature, même si l'équipe s'interdit de la réécrire. (c) rg.fr-par.scw.cloud, lyneko-apps/pricer, pas d'étiquette, référence par empreinte : immuable. (d) mon-registre:5000 (le premier élément du chemin désigne un registre s'il contient un point, un deux-points suivi d'un port, ou s'il vaut localhost), dépôt outil, étiquette latest implicite, mutable.
2. Explorer un autre index. Adaptez le script de la leçon pour l'image library/python et l'étiquette 3.14-slim. Combien de couches a la variante amd64 ? Quelle est la plus grosse ? Retrouvez dans la configuration l'étape qui l'a produite.
Solution
Changez REPO=library/python et TAG=3.14-slim. Au 1er octobre 2026, la variante amd64 compte quatre couches, de tailles compressées 29,8 Mo, 1,3 Mo, 12,4 Mo et 249 octets (jq '[.layers[] | .size]' manifeste.json). Dans config.json, les entrées de history qui ne sont pas empty_layer correspondent dans l'ordre aux couches : la plus grosse est la base Debian (# debian.sh --arch 'amd64' out/ 'trixie'...), puis viennent les dépendances système (apt-get install), la compilation et l'installation de Python (12,4 Mo), et enfin quelques liens symboliques (for src in idle3 pip3 pydoc3 python3...), d'où les 249 octets.
3. Épingler. Récupérez l'empreinte de l'index de postgres:18 présente sur votre machine, puis réécrivez une commande docker run qui l'utilise. Que se passe-t-il si vous modifiez un caractère de l'empreinte ?
Solution
$ docker image inspect postgres:18 --format '{{.ID}}'
$ docker run --rm postgres:18@sha256:<empreinte> postgres --version
Avec le magasin containerd, .ID est l'empreinte de l'index. On peut aussi la lire dans docker image ls --digests ou docker buildx imagetools inspect postgres:18. Si vous modifiez un caractère, Docker ne trouve pas l'image localement et la demande au registre, qui répond qu'elle n'existe pas :
docker: Error response from daemon: failed to resolve reference "docker.io/library/postgres@sha256:5a5a84b1...": ... not foundAucune autre image ne peut avoir cette empreinte.
4. Un secret dans une couche (niveau 200). Un développeur a construit une image dont le Dockerfile copie un fichier .env contenant un mot de passe, puis le supprime dans l'étape suivante. Il affirme que le secret n'est pas dans l'image puisque docker run image cat /app/.env échoue. Montrez-lui qu'il a tort, avec les outils de cette leçon, sans exécuter l'image.
Solution
docker save image -o image.tar, puis listez les couches dans blobs/sha256/ : chaque couche est une archive tar. Celle produite par l'étape COPY .env contient le fichier (tar -tzf blobs/sha256/<couche> | grep .env, puis tar -xzf ... app/.env -O). La couche suivante ne contient qu'un marqueur de suppression (.wh..env). Le secret est récupérable par toute personne qui peut télécharger l'image. Il faut le considérer comme compromis et le changer.
Récapitulatif
- Une référence d'image se lit registre / dépôt : étiquette @ empreinte. Sans registre, c'est Docker Hub ; sans étiquette, c'est
latest. - Une image OCI est un graphe d'objets adressés par contenu : index → manifeste par plateforme → configuration + couches. L'empreinte de l'index garantit l'intégrité de l'ensemble.
- Une étiquette est un pointeur mobile, une empreinte une identité. En production, on épingle par empreinte.
- Les couches s'empilent avec overlayfs : partagées en lecture, copiées à la première écriture, jamais modifiées. Supprimer un fichier n'allège pas l'image et n'efface pas un secret.
- Un registre est une API HTTP normalisée ; Docker Hub limite les téléchargements anonymes.
docker system dfmesure, les commandesprunenettoient, avec prudence.
Pour aller plus loin
- La spécification d'image OCI, courte et très lisible :
manifest.md,image-index.mdetconfig.mdsuffisent pour tout comprendre. - La spécification de distribution OCI, pour écrire un client de registre ou comprendre une erreur d'API.
- La documentation du noyau sur overlayfs, en particulier les sections sur les whiteouts et le copy up.
- Le cours Construire des images de conteneurs, qui part de ces notions pour produire des images petites, rapides à construire et sûres.
- Leçon suivante : écrire un premier Dockerfile pour l'application du fil conducteur.
Sources
- OCI Image Format Specification (index, manifeste, configuration, couches)
- OCI Distribution Specification
- Overlay Filesystem, documentation du noyau Linux
- Docker, Storage drivers et OverlayFS
- Docker Hub, pull usage and limits
- Docker, Docker Official Images
- Scaleway, Container Registry : documentation
- CNCF Distribution (registry:3), documentation