Une image construite en une seule étape garde tout ce qui a servi à la fabriquer : le compilateur, les sources, le cache des dépendances. Pour un petit programme Go, cela fait plus d'un gigaoctet à stocker, à transférer et à surveiller, alors que le programme lui-même pèse 9 Mo. Un build multi-étapes (multi-stage build) règle ce problème en découpant le Dockerfile en étapes : l'une compile, une autre teste, et l'image finale ne reçoit que le résultat.
Cette leçon va plus loin que la copie d'un binaire. Une image finale presque vide casse de façons précises : le programme ne démarre pas, une connexion HTTPS échoue, l'heure de Paris renvoie une erreur, l'utilisateur n'existe pas. Vous apprendrez à prévoir ces pannes, à les corriger, à faire échouer la construction quand un test échoue (un Dockerfile naïf ne le garantit pas), à contrôler qu'aucun outil ne reste, et à déboguer une image qui n'a même pas de shell. Toutes les mesures viennent de Docker 29.9.2, avec l'image golang:1.27 (Go 1.27.3), sur amd64.
La leçon Changer de socle sans casser l'application a déjà montré un multi-étapes minimal, avec un test de fumée dans la construction ; ici, le multi-étapes est le sujet. Le choix de l'image de base se discute dans Choisir et maintenir une image de base, et la réduction de taille au sens large dans Optimiser la taille des images : cette leçon-ci traite de ce que l'image finale doit contenir pour fonctionner, pas de la course au mégaoctet.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Construire une image finale à une couche et la comparer à l'image d'une construction en une étape
- Lister ce qu'une image
scratch, entièrement vide, doit recevoir : un binaire statique, des certificats, des fuseaux horaires, un utilisateur - Reconnaître les quatre erreurs qui surviennent quand l'image reçoit trop peu
- Choisir une image distroless quand
scratchdemande trop de copies - Placer les tests dans une étape pour qu'un test cassé bloque la construction, puis tester l'image finale elle-même
- Contrôler l'absence d'outils dans une image sans shell
- Déboguer sans rien ajouter à l'image livrée
Prérequis
Section intitulée « Prérequis »- Savoir écrire un Dockerfile (Écrire un Dockerfile) et connaître l'effet de
USER(Docker USER : exécuter une image en non-root) - Avoir lu l'ordre des couches et le cache (Organiser les couches et exploiter le cache)
- Savoir lire l'identité d'une image, tag et digest (Lire une image OCI et son identité)
- Un Docker 29.9.2 fonctionnel (installation de Docker), la commande
curl, etopensslpour la section sur les certificats
Quelle application sert d'exemple, et que pèse-t-elle construite en une étape ?
Section intitulée « Quelle application sert d'exemple, et que pèse-t-elle construite en une étape ? »L'exemple est un petit programme Go nommé pointage. Go produit un seul fichier exécutable, ce qui rend l'écart entre « avec » et « sans compilateur » très visible. Le programme répond sur /sante, affiche sur /heure l'heure de Paris (toujours le même instant fixe, pour que la sortie se reproduise) et sur /uid l'identifiant de l'utilisateur qui le fait tourner. Avec la sous-commande verifier URL, il interroge une adresse en HTTP ou HTTPS.
Créez un dossier de travail que la section de nettoyage supprime, puis les trois fichiers du programme.
mkdir -p ~/pointage && cd ~/pointagepackage main
import ( "fmt" "net/http" "os" "time")
func main() { if len(os.Args) > 2 && os.Args[1] == "verifier" { r, err := http.Get(os.Args[2]) if err != nil { fmt.Println("erreur:", err) os.Exit(1) } fmt.Println("statut:", r.Status) return } http.HandleFunc("/sante", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, "ok") }) http.HandleFunc("/heure", func(w http.ResponseWriter, _ *http.Request) { loc, err := time.LoadLocation("Europe/Paris") if err != nil { http.Error(w, err.Error(), 500) return } fmt.Fprintln(w, time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC).In(loc).Format("15:04 MST")) }) http.HandleFunc("/uid", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, os.Getuid()) }) fmt.Println("ecoute sur :8080") http.ListenAndServe(":8080", nil)}package main
import "testing"
func TestAddition(t *testing.T) { if 1+1 != 2 { t.Fatal("arithmetique") }}module pointage
go 1.27Le premier Dockerfile construit tout dans la même image : il part de l'image officielle golang, qui contient le compilateur, copie les sources et compile. L'option CGO_ENABLED=0 demande à Go un binaire statique, c'est-à-dire qui n'a besoin d'aucune bibliothèque du système ; la raison est donnée plus bas.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095bWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /usr/local/bin/pointage .CMD ["pointage"]docker build -q -f mono.Dockerfile -t pointage:mono .L'option -q n'affiche que l'identifiant de l'image construite. Gardez-la pour l'instant, le détail d'une construction n'a d'intérêt que dans la section sur les tests.
sha256:ca42c20c8c0ef6beb19354cae2ca052810f524550d3b8dbb47ca6533658c57a2L'identifiant affiché sera différent chez vous. Passons au second Dockerfile, qui est le sujet de la leçon.
Comment séparer la construction de l'exécution ?
Section intitulée « Comment séparer la construction de l'exécution ? »Un Dockerfile multi-étapes contient plusieurs instructions FROM. Chacune ouvre une étape, que AS nom permet de nommer. Seule la dernière étape donne l'image livrée ; les autres servent à préparer des fichiers, et COPY --from=nom va chercher dans une étape précédente exactement ce qu'on désigne. Ce qui n'est pas copié n'arrive pas dans l'image finale.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM scratchCOPY --from=build /pointage /pointageENTRYPOINT ["/pointage"]La première étape, build, est identique à celle de la construction en une étape. La seconde part de scratch, qui n'est pas une vraie image mais un point de départ entièrement vide : pas de shell, pas de bibliothèques, pas de fichiers. Elle ne reçoit que le binaire, et ENTRYPOINT désigne le programme que docker run lance. Construisez cette seconde image et comparez les deux.
docker build -q -f multi.Dockerfile -t pointage:multi .docker image ls pointageIMAGE ID DISK USAGE CONTENT SIZE EXTRApointage:mono ca42c20c8c0e 1.43GB 344MBpointage:multi 9c59c1812f36 14.7MB 5.34MBRegardez la colonne DISK USAGE, celle que cette leçon compare : 1,43 Go pour la construction en une étape, 14,7 Mo pour le multi-étapes, soit une centaine de fois moins. Le décompte exact des couches se lit avec docker image inspect.
for t in mono multi; do echo "--- $t"; docker image inspect --format '{{.Size}} octets, couches={{len .RootFS.Layers}}' pointage:$t; done--- mono1429170290 octets, couches=10--- multi14708917 octets, couches=1L'image en une étape compte 10 couches, dont plusieurs de plus de 200 Mo héritées de l'image golang elle-même. L'image multi-étapes en compte une seule, celle du COPY du binaire, comme le confirme docker history.
docker history pointage:multiIMAGE CREATED CREATED BY SIZE COMMENT9c59c1812f36 1 second ago ENTRYPOINT ["/pointage"] 0B buildkit.dockerfile.v0<missing> 1 second ago COPY /pointage /pointage # buildkit 9.37MB buildkit.dockerfile.v0La deuxième ligne est la seule couche qui pèse : 9,37 Mo, le binaire. La ligne ENTRYPOINT ne crée pas de fichier (0 octet). Le gain ne vient pas d'une option de compression : il vient du fait que le compilateur et les sources sont restés dans l'étape build, que Docker ne livre pas.
Que doit recevoir l'image finale pour que l'application tourne ?
Section intitulée « Que doit recevoir l'image finale pour que l'application tourne ? »Une image scratch ne contient que ce que vous y copiez, donc tout ce que le programme lit en passant doit être copié aussi. Quatre choses font défaut dans cet exemple, et chacune se reconnaît à un message précis. Les quatre sous-sections suivantes les provoquent une par une, avec le correctif.
Pourquoi un binaire dynamique échoue-t-il dans scratch ?
Section intitulée « Pourquoi un binaire dynamique échoue-t-il dans scratch ? »Un binaire dynamique ne contient pas la bibliothèque C de son système (la libc). Au démarrage, il s'appuie sur un programme du système, le chargeur, qui la charge, et ce chargeur doit exister dans l'image.
Go produit un tel binaire quand le cgo, le pont entre Go et le code C, est actif et qu'un paquet du programme s'en sert, comme net pour la résolution de noms. Dans une compilation native, le cgo est actif par défaut si un compilateur C est présent, ce qui est le cas dans l'image golang. CGO_ENABLED=0 le désactive et donne un binaire statique. La page Compiler et versionner un binaire Go détaille la compilation Go ; ici, vous vérifiez le résultat avec ldd, qui liste les bibliothèques demandées.
docker run --rm --entrypoint sh pointage:mono -c 'CGO_ENABLED=1 go build -o /tmp/dyn /src && ldd /tmp/dyn; CGO_ENABLED=0 go build -o /tmp/st /src && (ldd /tmp/st; echo code=$?)' linux-vdso.so.1 (0x00007fe300d1f000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe300b22000) /lib64/ld-linux-x86-64.so.2 (0x00007fe300d21000) not a dynamic executablecode=1Les trois premières lignes sont le binaire compilé avec cgo : il demande libc.so.6 et le chargeur ld-linux. Les deux dernières sont le binaire compilé sans cgo : ldd répond not a dynamic executable (avec le code 1, qui n'est pas une erreur ici, c'est la réponse attendue pour un binaire statique). Construisez maintenant l'image avec le binaire dynamique, pour voir l'échec.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=1 go build -o /pointage .
FROM scratchCOPY --from=build /pointage /pointageENTRYPOINT ["/pointage"]docker build -q -f dyn.Dockerfile -t pointage:dyn .docker run --rm pointage:dyn verifier https://172.17.0.1:8443/echo "code=$?"sha256:7408993347e6b2d9c46495965d521bb000fcf35f0e1c8e984ecf83a5b25bff3cexec /pointage: no such file or directorycode=255La construction réussit, rien ne prévient. Au lancement, le message dit no such file or directory alors que /pointage existe bel et bien : c'est le chargeur, absent de scratch, qui manque, et le message cite le fichier demandé au lieu de l'expliquer. Le correctif est dans le Dockerfile, CGO_ENABLED=0, comme dans multi.Dockerfile. L'adresse https://172.17.0.1:8443/ ne sert pas encore : le programme échoue avant de l'interroger.
Pourquoi une connexion HTTPS échoue-t-elle sans certificats ?
Section intitulée « Pourquoi une connexion HTTPS échoue-t-elle sans certificats ? »Pour vérifier un serveur HTTPS, un client doit connaître les autorités de certification (CA) auxquelles il fait confiance, c'est-à-dire les organismes dont la signature prouve l'identité d'un serveur. Sur Debian et ses dérivés, elles sont rassemblées dans le fichier /etc/ssl/certs/ca-certificates.crt. Une image scratch ne l'a pas.
Pour provoquer l'erreur sans dépendre d'Internet, la démonstration utilise une autorité privée locale, créée avec openssl, et un petit serveur HTTPS de test qui présente un certificat signé par elle. Ce n'est pas un site public : c'est la façon de reproduire sans réseau l'erreur « autorité inconnue ». Les trois premières commandes créent l'autorité, le certificat du serveur (valable pour l'adresse 172.17.0.1, celle de l'hôte vue depuis un conteneur sur le réseau Docker par défaut) et le signent ; la dernière démarre le serveur en arrière-plan.
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem -days 2 -subj "/CN=ca-locale" 2>/dev/nullopenssl req -newkey rsa:2048 -nodes -keyout srv.key -out srv.csr -subj "/CN=srv" 2>/dev/nullprintf 'subjectAltName=IP:172.17.0.1\n' > san.extopenssl x509 -req -in srv.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out srv.pem -days 2 -extfile san.ext 2>&1 | tail -1nohup openssl s_server -accept 8443 -cert srv.pem -key srv.key -www > s_server.log 2>&1 &Le serveur écoute sur toutes les interfaces de l'hôte, et le conteneur le joint par l'adresse de la passerelle Docker. Contrôlez d'abord que cette adresse existe, sinon les essais qui suivent échouent pour une autre raison que les certificats.
ip -4 addr show docker03: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0 valid_lft forever preferred_lft foreverL'état DOWN est normal tant qu'aucun conteneur n'est branché sur ce pont. Cherchez la ligne inet 172.17.0.1/16 : c'est l'adresse attendue sur le réseau Docker par défaut. Si elle diffère, adaptez san.ext et les URL en conséquence. Si l'adresse est bonne mais que les essais se terminent par un message i/o timeout au lieu d'une erreur de certificat, un pare-feu de l'hôte bloque probablement le port 8443 depuis le réseau Docker.
Les commandes openssl ont aussi déposé ca.key et srv.key dans le dossier. Les Dockerfiles de cette leçon font COPY . . dans l'étape build : ces clés de démonstration y sont copiées, mais jamais dans l'image finale, qui ne copie que le binaire et ce qu'elle désigne. Pour ne pas les envoyer du tout au moteur, un fichier .dockerignore les exclurait (voir Écrire un Dockerfile) ; ca.pem ne doit pas y figurer, car les Dockerfiles qui suivent la copient.
Interrogez maintenant ce serveur depuis l'image multi-étapes, qui n'a aucun certificat.
docker run --rm pointage:multi verifier https://172.17.0.1:8443/echo "code=$?"erreur: Get "https://172.17.0.1:8443/": tls: failed to verify certificate: x509: certificate signed by unknown authoritycode=1Le message x509: certificate signed by unknown authority est la signature de ce problème : le client a bien reçu un certificat, mais il ne connaît personne pour le garantir. Le correctif consiste à copier un fichier de certificats dans l'image. Pour cette démonstration, le fichier est celui de l'autorité privée ; une instruction COPY de plus, dans l'étape finale, le prend dans le dossier de travail et le place là où Go le cherche.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM scratchCOPY --from=build /pointage /pointageCOPY ca.pem /etc/ssl/certs/ca-certificates.crtENTRYPOINT ["/pointage"]docker build -q -f ca.Dockerfile -t pointage:ca .docker run --rm pointage:ca verifier https://172.17.0.1:8443/sha256:9d00876ae1c3eac76a2e0e7111cf2cf3ee6a352a5be54e0810771f80433b92c7statut: 200 okLa réponse 200 ok montre que, avec le fichier en place, la vérification passe. Pour un site HTTPS public, le correctif attendu est de copier le bundle de certificats de l'étape de build plutôt qu'une autorité privée :
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/La leçon n'exécute pas cette ligne contre un site HTTPS public. La démonstration ci-dessus prouve le mécanisme (un fichier à cet emplacement suffit à Go), pas le contenu du bundle de l'image golang.
Pourquoi l'heure de Paris renvoie-t-elle une erreur 500 ?
Section intitulée « Pourquoi l'heure de Paris renvoie-t-elle une erreur 500 ? »Un programme qui convertit une heure vers Europe/Paris lit la base des fuseaux horaires, un ensemble de fichiers (/usr/share/zoneinfo) que scratch ne fournit pas. Lancez l'image multi-étapes et interrogez /heure.
docker run -d --name pointage-test -p 8080:8080 pointage:multi >/dev/nullcurl -s -i http://127.0.0.1:8080/heureHTTP/1.1 500 Internal Server ErrorContent-Type: text/plain; charset=utf-8X-Content-Type-Options: nosniffDate: Tue, 03 Nov 2026 13:56:07 GMTContent-Length: 31
unknown time zone Europe/ParisLisez la première ligne et la dernière : le programme est vivant (il répond), mais il refuse la conversion avec unknown time zone Europe/Paris. Les deux autres adresses fonctionnent, ce qui montre que l'erreur est locale à cette route.
curl -s http://127.0.0.1:8080/santecurl -s http://127.0.0.1:8080/uiddocker rm -f pointage-testok0pointage-testLa seconde réponse, 0, sera utile plus bas : sans instruction contraire, le programme tourne sous l'utilisateur root, dont l'identifiant numérique (uid) est 0. Pour les fuseaux, il existe deux correctifs. Le premier copie l'archive des fuseaux que Go embarque avec son compilateur et la déclare par la variable d'environnement ZONEINFO.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM scratchCOPY --from=build /pointage /pointageCOPY --from=build /usr/local/go/lib/time/zoneinfo.zip /zoneinfo.zipENV ZONEINFO=/zoneinfo.zipENTRYPOINT ["/pointage"]Le second correctif n'ajoute aucun fichier : le drapeau de compilation -tags timetzdata fait embarquer la base des fuseaux dans le binaire. Il se place dans l'instruction RUN de l'étape build, le reste du Dockerfile ne change pas.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -tags timetzdata -o /pointage .
FROM scratchCOPY --from=build /pointage /pointageENTRYPOINT ["/pointage"]for v in tz tzdata; do docker build -q -f $v.Dockerfile -t pointage:$v . docker run -d --name pointage-test -p 8080:8080 pointage:$v >/dev/null sleep 1 curl -s http://127.0.0.1:8080/heure docker rm -f pointage-test >/dev/nulldonedocker image inspect --format '{{.Size}}' pointage:multi pointage:tzdata pointage:tz pointage:casha256:a3108f9aeba1e2d537578027d61d3ed64215880e95c4c4e8d8aa03f151898e1414:00 CESTsha256:0e89f69a95b51257b8c53c828254903deaabbaede369ca3d1d27f332abb9241314:00 CEST14708917152206461522013914730809Les deux correctifs donnent 14:00 CEST (midi UTC le 1er juillet, soit 14 h à Paris en heure d'été). Le coût se lit dans les quatre derniers nombres, dans l'ordre des images demandées : multi pèse 14,71 Mo, tzdata 15,22 Mo, tz 15,22 Mo, ca 14,73 Mo. Chaque correctif des fuseaux ajoute donc environ 0,5 Mo ; celui des certificats privés environ 22 Ko, soit presque rien.
Pourquoi USER nobody échoue-t-il dans scratch ?
Section intitulée « Pourquoi USER nobody échoue-t-il dans scratch ? »Le conseil classique est de ne pas laisser l'application tourner en root : on ajoute USER nobody. Dans scratch, il n'existe aucun fichier /etc/passwd, donc aucun utilisateur nommé nobody. Le Dockerfile se construit pourtant sans erreur ; c'est au lancement que cela casse.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM scratchCOPY --from=build /pointage /pointageUSER nobodyENTRYPOINT ["/pointage"]docker build -q -f usern.Dockerfile -t pointage:usern .docker run --rm pointage:usern verifier http://xecho "code=$?"sha256:7bf0241777a2790c7ecb8ce9fd3501964cd81a7ffcb3d15ba1b4a68ee85939a9docker: Error response from daemon: unable to find user nobody: no matching entries in passwd file
Run 'docker run --help' for more informationcode=125La construction réussit, rien ne signale le problème. L'erreur arrive au docker run, avec le code 125 : Docker lui-même n'a pas pu démarrer le conteneur (ce n'est pas le programme qui a échoué). Le correctif est d'indiquer un identifiant numérique, qui n'a pas besoin de /etc/passwd : USER 65532:65532 (l'identifiant de l'utilisateur et celui de son groupe, la valeur que les images distroless nonroot, présentées plus bas, utilisent aussi).
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM scratchCOPY --from=build /pointage /pointageUSER 65532:65532ENTRYPOINT ["/pointage"]docker build -q -f usernum.Dockerfile -t pointage:usernum .docker run -d --name pointage-test -p 8080:8080 pointage:usernum >/dev/nullsleep 1curl -s http://127.0.0.1:8080/uiddocker inspect --format '{{.Config.User}}' pointage-testdocker rm -f pointage-test >/dev/nullsha256:b5dceedcc0c588f02e4e64dae49f8e65a934212213e35260e74461321c93ae846553265532:65532Le programme répond lui-même 65532 sur /uid, et docker inspect confirme le champ Config.User de l'image. La leçon Docker USER explique pourquoi un uid numérique est plus sûr qu'un nom, et ce que USER ne change pas dans les droits des fichiers.
Que donne l'image qui reçoit tout cela ?
Section intitulée « Que donne l'image qui reçoit tout cela ? »Rassemblez les quatre corrections dans un seul Dockerfile : binaire statique, certificats, fuseaux, utilisateur numérique.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .RUN cat /etc/ssl/certs/ca-certificates.crt ca.pem > /bundle.crt
FROM scratchCOPY --from=build /pointage /pointageCOPY --from=build /bundle.crt /etc/ssl/certs/ca-certificates.crtCOPY --from=build /usr/local/go/lib/time/zoneinfo.zip /zoneinfo.zipENV ZONEINFO=/zoneinfo.zipUSER 65532:65532ENTRYPOINT ["/pointage"]docker build -q -f tout.Dockerfile -t pointage:tout .docker run --rm pointage:tout verifier https://172.17.0.1:8443/docker run -d --name pointage-test -p 8080:8080 pointage:tout >/dev/nullsleep 1curl -s http://127.0.0.1:8080/heurecurl -s http://127.0.0.1:8080/uiddocker image inspect --format '{{.Size}}' pointage:toutsha256:c5f72f3b45d44f51a16e9791eb7f61ad10945ea9cadc53a29bdd3324afbc243bstatut: 200 ok14:00 CEST6553215591377Les trois comportements (statut: 200 ok, 14:00 CEST, 65532) se retrouvent, et la dernière ligne donne le poids de l'image : 15 591 377 octets, soit 15,6 Mo. C'est 0,9 Mo de plus que l'image multi, le prix du bundle de certificats et de l'archive des fuseaux.
Le bundle de l'étape de build est ici concaténé avec l'autorité privée : l'instruction cat de l'étape build produit un fichier qui contient les autorités publiques et la privée. En production, on livre le bundle public, complété si besoin par les autorités privées de l'entreprise. Copier l'autorité privée seule à cet emplacement remplacerait le bundle public et casserait les connexions vers les sites publics.
Le conteneur pointage-test reste lancé : la section sur la distroless le supprime avant de repartir. Le tableau suivant rassemble les quatre pannes, leur cause et leur correctif.
| Symptôme | Cause dans une image vide | Correctif |
|---|---|---|
exec /pointage: no such file or directory, code 255, alors que le fichier existe | Binaire dynamique, chargeur absent | CGO_ENABLED=0, ou une image qui apporte la libc |
x509: certificate signed by unknown authority | Aucun certificat d'autorité dans l'image | Copier un bundle dans /etc/ssl/certs/ca-certificates.crt |
unknown time zone Europe/Paris (HTTP 500) | Aucune base de fuseaux | ZONEINFO avec l'archive de Go, ou -tags timetzdata |
unable to find user nobody, code 125, au docker run | Aucun /etc/passwd | USER avec un uid:gid numérique |
Ces quatre pannes sont celles de cet exemple, pas une liste complète : un autre programme en rencontrerait d'autres, par exemple une écriture dans /tmp, dossier que scratch ne contient pas non plus.
Et si on partait d'une image distroless plutôt que de scratch ?
Section intitulée « Et si on partait d'une image distroless plutôt que de scratch ? »Copier certificats et fuseaux à la main devient vite répétitif. Les images distroless de Google sont des images minimales sans shell ni gestionnaire de paquets, mais qui apportent déjà le nécessaire pour exécuter un programme. La variante static-debian13 convient aux binaires statiques ; pour l'ensemble de la famille (Wolfi, Chainguard, Alpine), voir Choisir et maintenir une image de base.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM gcr.io/distroless/static-debian13@sha256:448810357cc8e67b7208988abdba0f5ec3d3aebee0a5b13eb624979792cffc0fCOPY --from=build /pointage /pointageENTRYPOINT ["/pointage"]docker rm -f pointage-testdocker build -q -f distroless.Dockerfile -t pointage:distroless .docker image inspect --format '{{.Size}} octets couches={{len .RootFS.Layers}}' pointage:distrolessdocker run -d --name pointage-test -p 8080:8080 pointage:distroless >/dev/nullsleep 1curl -s http://127.0.0.1:8080/heurecurl -s http://127.0.0.1:8080/uiddocker rm -f pointage-test >/dev/nulldocker run --rm pointage:distroless verifier https://172.17.0.1:8443/pointage-testsha256:5f076b3534ec043e6ac70f4f7ac8c68f76fc23f3c6da859cbf68d6eebb5a5c3321361649 octets couches=1414:00 CEST0erreur: Get "https://172.17.0.1:8443/": tls: failed to verify certificate: x509: certificate signed by unknown authorityTrois observations. Les fuseaux fonctionnent sans rien copier (14:00 CEST) : l'image les apporte. L'image pèse 21,4 Mo en 14 couches, contre 14,7 Mo en une couche pour scratch : c'est le prix de ce qu'elle apporte. Le programme tourne toujours en root (0), car l'image par défaut ne change pas l'utilisateur.
La dernière ligne ne contredit pas l'image : elle embarque les autorités publiques, et l'autorité privée de la démonstration n'en fait pas partie. Pour l'ajouter, ne copiez pas ca.pem sur /etc/ssl/certs/ca-certificates.crt, car cela remplacerait le bundle public et supprimerait les autorités publiques. Concaténez les deux dans l'étape de build, puis copiez le résultat. Dans le Dockerfile complet qui suit, les deux lignes utiles sont celles qui touchent à /bundle.crt.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .RUN cat /etc/ssl/certs/ca-certificates.crt ca.pem > /bundle.crt
FROM gcr.io/distroless/static-debian13@sha256:448810357cc8e67b7208988abdba0f5ec3d3aebee0a5b13eb624979792cffc0fCOPY --from=build /pointage /pointageCOPY --from=build /bundle.crt /etc/ssl/certs/ca-certificates.crtENTRYPOINT ["/pointage"]docker build -q -f distroless-ca.Dockerfile -t pointage:distroless-ca .Avec ce Dockerfile, l'image distroless accepte l'autorité privée : docker run --rm pointage:distroless-ca verifier https://172.17.0.1:8443/ affiche statut: 200 ok. Une requête vers un site HTTPS public passe aussi la vérification, sans erreur x509, puisque les autorités publiques sont restées dans le bundle.
Le digest est ici la seule référence : le dépôt ne publie pas de tag de version pour cette variante, et un tag mouvant comme latest ne désigne pas toujours la même image. Pour l'utilisateur, distroless publie une variante nonroot, qui fixe l'utilisateur dans l'image : gcr.io/distroless/static-debian13:nonroot@sha256:25ac8f0c3b2334a9eb05452266bd4697864a8c8ef0ef71e5e6480d7f92868329. Une troisième variante, debug, ajoute un shell (busybox) et sert au débogage, plus bas. Un digest désigne une seule image : quand vous changez de variante, changez le tag et le digest ensemble.
Comment faire échouer la construction quand un test échoue ?
Section intitulée « Comment faire échouer la construction quand un test échoue ? »Les tests unitaires ont besoin du compilateur et des sources, exactement ce que l'image finale n'a pas. Le multi-étapes leur donne une place : une étape test qui repart de l'étape build (FROM build AS test) et lance go test. Si le test échoue, la commande RUN échoue, donc la construction aussi. Du moins en théorie.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM build AS testRUN go test ./...
FROM scratch AS finalCOPY --from=build /pointage /pointageENTRYPOINT ["/pointage"]Demandez d'abord explicitement l'étape test avec --target, qui arrête la construction à l'étape nommée. L'option --progress=plain affiche chaque instruction et sa sortie.
docker build --target test -f test.Dockerfile -t pointage:test --progress=plain .#8 [build 4/4] RUN CGO_ENABLED=0 go build -o /pointage .#8 CACHED
#9 [test 1/1] RUN go test ./...#9 6.717 ok pointage 0.002s#9 DONE 6.8sLa sortie est abrégée aux étapes utiles. Remarquez d'abord #8 CACHED : l'étape build de ce Dockerfile est identique à celle de multi.Dockerfile, déjà construit plus haut, donc Docker réutilise son résultat au lieu de recompiler. Lisez ensuite la ligne #9 6.717 ok pointage 0.002s : le test a tourné et réussi. Lancez maintenant la construction normale, sans --target, comme le fera votre intégration continue.
docker build -f test.Dockerfile -t pointage:final --progress=plain .#8 [build 4/4] RUN CGO_ENABLED=0 go build -o /pointage .#8 CACHED
#9 [final 1/1] COPY --from=build /pointage /pointage#9 CACHEDCherchez la ligne [test 1/1] : elle n'existe pas. L'étape de test n'a pas été exécutée. BuildKit, le moteur de construction, ne construit que les étapes dont l'étape finale dépend, et final ne copie que depuis build. Aucune étape ne lui demande test, donc elle est sautée. Pour mesurer le danger, cassez volontairement le test (le programme reste inchangé) et reconstruisez.
sed -i 's/1+1 != 2/1+1 != 3/' main_test.godocker build -f test.Dockerfile -t pointage:final2 --progress=plain .echo "code=$?"docker image ls pointage:final2#10 DONE 0.1scode=0IMAGE ID DISK USAGE CONTENT SIZE EXTRApointage:final2 afcfc4947f37 14.7MB 5.34MBLa construction réussit (code=0) et produit une image de 14,7 Mo, alors que le test est cassé. C'est le piège : un go test placé dans une étape dont rien ne dépend ne protège de rien. La correction est de faire dépendre l'étape finale de l'étape de test. Le plus simple est de copier le binaire depuis l'étape test au lieu de l'étape build : le binaire y est, puisque test repart de build.
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -o /pointage .
FROM build AS testRUN go test ./...
FROM scratch AS finalCOPY --from=test /pointage /pointageENTRYPOINT ["/pointage"]docker build -f dep.Dockerfile -t pointage:dep-ko --progress=plain .echo "code=$?"docker image ls pointage:dep-ko#9 [test 1/1] RUN go test ./...#9 6.783 --- FAIL: TestAddition (0.00s)#9 6.783 main_test.go:7: arithmetique#9 6.783 FAIL#9 6.783 FAIL pointage 0.002s#9 6.783 FAIL#9 ERROR: process "/bin/sh -c go test ./..." did not complete successfully: exit code: 1ERROR: failed to build: failed to solve: process "/bin/sh -c go test ./..." did not complete successfully: exit code: 1code=1IMAGE ID DISK USAGE CONTENT SIZE EXTRACette fois l'étape test s'exécute, le test échoue, la construction s'arrête avec le code 1 et aucune image n'est créée (la liste est vide). Remettez le test en état et reconstruisez.
sed -i 's/1+1 != 3/1+1 != 2/' main_test.godocker build -q -f dep.Dockerfile -t pointage:dep .docker image inspect --format '{{.Size}} couches={{len .RootFS.Layers}}' pointage:depsha256:ebaa00002ac6a9ec3ede3289ad0071d8931c4b85aad0d386f4fb303a9f21f3a414708917 couches=1L'image est identique à pointage:multi : 14 708 917 octets, une couche. L'étape de test a laissé le résultat intact, elle ne change que la condition de la construction. Une autre façon de tenir la même garantie est de lancer docker build --target test comme étape distincte dans votre pipeline avant la construction finale, mais elle suppose qu'on pense toujours à l'ajouter. La dépendance dans le Dockerfile n'a pas ce défaut : elle protège aussi un docker build tapé à la main.
Le schéma résume le parcours. Suivez les flèches pleines : les sources donnent le binaire dans l'étape build, qui le passe à l'étape test, puis test le passe à l'étape finale par COPY --from=test. Le compilateur et les autres outils, en gris, ne servent qu'à compiler : ils restent dans build et ne traversent pas.
Comment tester l'image finale elle-même ?
Section intitulée « Comment tester l'image finale elle-même ? »Le test de l'étape test vérifie le code, pas l'image livrée : il ne dit rien du chargeur, des certificats, des fuseaux ni de l'utilisateur, les quatre pannes de l'image vide. Le test qui les attrape consiste à lancer l'image finale et lui poser des questions, comme le ferait un client.
Le test se range dans un script, verifier-image.sh, qui prend l'image en argument et gère lui-même le conteneur pointage-test : il supprime l'ancien s'il existe, puis lance le nouveau. Quatre précautions le rendent fiable.
set -earrête le script à la première commande qui échoue.curl -fsSrend un code non nul sur une réponse HTTP 500, ce quecurl -sseul ne fait pas.- Chaque sortie est comparée à sa valeur attendue avec
test, car une route peut répondre avec le code 0 et la mauvaise valeur :/uidrépond toujours avec le code 0, y compris quand le programme tourne en root. - Une boucle courte attend que
/santeréponde, au plus cinq secondes, au lieu d'unsleep 1qui crée une course entre le démarrage et la requête.
#!/usr/bin/env bashset -eimage="${1:-pointage:tout}"docker rm -f pointage-test >/dev/null 2>&1 || truedocker run -d --name pointage-test -p 8080:8080 "$image" >/dev/nullfor _ in $(seq 1 10); do curl -fsS http://127.0.0.1:8080/sante >/dev/null 2>&1 && break sleep 0.5donesante=$(curl -fsS http://127.0.0.1:8080/sante)test "$sante" = "ok"heure=$(curl -fsS http://127.0.0.1:8080/heure)test "$heure" = "14:00 CEST"uid=$(curl -fsS http://127.0.0.1:8080/uid)test "$uid" != 0https=$(docker run --rm "$image" verifier https://172.17.0.1:8443/)test "$https" = "statut: 200 ok"echo "image conforme"Lancez-le d'abord sur l'image sans fuseaux, pointage:multi, pour vérifier qu'il bloque vraiment.
bash verifier-image.sh pointage:multiecho "code=$?"curl: (22) The requested URL returned error: 500code=22Le script s'arrête sur la route /heure avec le code 22 : curl -fsS signale l'erreur 500 que renvoie l'image sans fuseaux, 22 étant le code que curl rend pour une réponse HTTP en erreur. La ligne image conforme n'apparaît pas.
Ce qui arrête le script, c'est le code de sortie de curl, que l'affectation heure=$(curl …) transmet à set -e. C'est pourquoi chaque contrôle se fait en deux lignes, l'affectation puis le test. Écrit test "$(curl …/uid)" != 0, le contrôle de /uid ne suffirait pas : set -e ignore le code d'une substitution placée en argument d'une commande, et seul le code de test compte. Un curl en échec rendrait une chaîne vide, différente de 0, et le test passerait sur une route en panne.
Lancez maintenant le script sur l'image corrigée.
bash verifier-image.sh pointage:toutecho "code=$?"image conformecode=0Chaque comparaison répond à une panne : ok prouve que le binaire démarre (chargeur), 14:00 CEST que les fuseaux sont là, un uid différent de 0 que l'utilisateur n'est pas root, statut: 200 ok que les certificats sont lus. Dans un pipeline, on appelle ce script après la construction : si une comparaison échoue, le script rend un code non nul et l'image ne part pas. Le conteneur pointage-test reste lancé pour la section sur le débogage.
Comment contrôler qu'aucun outil de build ne reste dans l'image ?
Section intitulée « Comment contrôler qu'aucun outil de build ne reste dans l'image ? »L'image est plus petite, mais il reste à prouver qu'elle ne contient ni go, ni gcc, ni git. Le réflexe est d'entrer dans le conteneur et de demander. Ce réflexe a un piège avec la commande command -v, qui affiche le chemin d'un programme s'il existe. Essayez-la sur l'image construite en une étape, où les trois outils sont présents.
echo 'command -v go gcc git; echo code=$?' | docker run --rm -i pointage:mono sh/usr/local/go/bin/gocode=0Un seul chemin s'affiche, alors que gcc et git sont dans l'image. La raison : sh est ici dash, qui ne regarde que le premier nom donné à command -v. Sur une image qui contiendrait gcc sans go, cette commande n'afficherait rien, et vous conclueriez à tort qu'aucun outil n'est présent. La parade est une boucle, qui pose la question nom par nom. Elle se lance sur une image qui contient les outils pour voir la bonne sortie ; l'image de l'étape build s'obtient avec --target build.
docker build --target build -f test.Dockerfile -t pointage:build .echo 'for t in go gcc git; do command -v $t; done' | docker run --rm -i pointage:build bash/usr/local/go/bin/go/usr/bin/gcc/usr/bin/gitLa boucle trouve les trois outils. Mais ce contrôle suppose un shell dans l'image, et l'image finale n'en a pas. Tentez de l'ouvrir par --entrypoint, qui remplace le programme lancé.
docker run --rm --entrypoint sh pointage:multiecho "code=$?"docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "sh": executable file not found in $PATH
Run 'docker run --help' for more informationcode=127L'erreur executable file not found in $PATH avec le code 127 dit qu'il n'y a pas de sh, ce qui est justement ce qu'on attendait d'une image sans outils. Ne confondez pas avec une commande qui paraît plus simple : docker run pointage:multi sh ne cherche pas un shell. Tout ce qui suit le nom de l'image est passé en argument à l'ENTRYPOINT, donc sh devient un argument de /pointage.
docker run --rm --name pointage-sh pointage:multi shecoute sur :8080Le programme démarre, affiche ecoute sur :8080 et ne rend pas la main. Le terminal reste occupé sans erreur : c'est un piège, car rien n'indique que sh a été ignoré. Depuis un autre terminal, docker stop pointage-sh arrête le conteneur, qui sort avec le code 143, soit 128 plus 15, le numéro du signal SIGTERM que docker stop envoie (mesuré sur cet exemple).
Pour contrôler une image sans shell, on ne lui demande rien : on lit son contenu. docker create prépare un conteneur sans le démarrer, docker export en sort le système de fichiers sous forme d'archive, et tar -t en liste le contenu.
docker create --name pointage-export pointage:multi >/dev/nulldocker export pointage-export | tar -tdocker rm pointage-export.dockerenvdev/dev/consoledev/pts/dev/shm/etc/etc/hostnameetc/hostsetc/mtabetc/resolv.confpointageproc/sys/pointage-exportUne seule entrée vient de l'application, pointage ; le reste est ce que Docker crée dans chaque conteneur (.dockerenv, dev, etc, proc, sys). Aucun go, aucun gcc, aucun git, aucun fichier source. Pour comparer, la même commande sur l'image en une étape, filtrée sur les trois outils, les retrouve tous :
docker create --name pointage-export pointage:mono >/dev/nulldocker export pointage-export | tar -t | grep -E 'bin/(go|gcc|git)$'docker export pointage-export | tar -t | wc -ldocker rm pointage-exportusr/bin/gccusr/bin/gitusr/local/go/bin/go38248Trois outils présents sur 38 248 entrées, contre une seule entrée applicative dans l'image multi-étapes. L'absence se prouve donc par la lecture du contenu, et non par un programme lancé dans l'image. Pour une image distroless, docker export donne 1 403 entrées (mesuré sur l'image construite plus haut), dont etc/passwd et etc/os-release, que scratch n'a pas.
Comment déboguer une image qui n'a ni shell ni outils ?
Section intitulée « Comment déboguer une image qui n'a ni shell ni outils ? »Quand un conteneur sans shell se comporte mal, docker exec ne peut rien : l'image n'a aucun programme à lancer. Les journaux restent disponibles, ils ne dépendent pas de l'image. Les commandes de cette section visent le conteneur pointage-test, lancé sur pointage:tout par verifier-image.sh, dans la section « Comment tester l'image finale elle-même ? ».
docker logs pointage-testecoute sur :8080La commande docker debug existe dans certaines éditions de Docker, mais pas dans le moteur utilisé ici.
docker debug pointage:depecho "code=$?"docker: unknown command: docker debug
Run 'docker --help' for more informationcode=1Le moteur répond unknown command : cette leçon ne s'appuie pas sur docker debug. Les gestes qui suivent ne demandent que Docker 29.9.2. Essayez d'abord l'évidence, docker exec.
docker exec pointage-test shecho "code=$?"OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATHcode=127La solution la plus simple est de lancer à côté un conteneur d'outils qui partage ceux du conteneur à étudier. Un conteneur voit normalement ses propres processus et son propre réseau. --pid=container:pointage-test lui fait voir les processus du conteneur étudié, --network=container:pointage-test son réseau ; l'image busybox apporte un shell et les commandes de base. Elle n'est pas dans l'image de l'application : elle vit le temps du diagnostic.
docker run --rm --pid=container:pointage-test --network=container:pointage-test \ busybox:1.37@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec \ sh -c 'ps; echo ---; netstat -tln; echo ---; wget -qO- http://127.0.0.1:8080/sante; echo ---; ls /proc/1/root/; echo ---; ls -l /proc/1/exe'PID USER TIME COMMAND 1 65532 0:00 /pointage 16 root 0:00 sh -c ps; echo ---; netstat -tln; echo ---; wget -qO- http://127.0.0.1:8080/sante; echo ---; ls /proc/1/root/; echo ---; ls -l /proc/1/exe 23 root 0:00 ps---Active Internet connections (only servers)Proto Recv-Q Send-Q Local Address Foreign Address Statetcp 0 0 :::8080 :::* LISTEN---ok---ls: /proc/1/root/: Permission denied---lrwxrwxrwx 1 65532 65532 0 Nov 3 14:02 /proc/1/exels: /proc/1/exe: cannot read link: Permission deniedLisez ce bloc par morceaux. ps montre le processus 1, /pointage, tournant sous l'uid 65532, à côté des processus de l'outil. netstat confirme que le port 8080 écoute. wget joint l'application depuis l'intérieur du réseau du conteneur et reçoit ok. En revanche, les deux dernières commandes, qui essaient de lire le système de fichiers du processus 1, répondent Permission denied.
Le conteneur d'outils tourne en root, mais Docker ne lui donne pas toutes les capacités de root, ces droits précis du noyau : il n'a pas celle de tracer un autre processus. L'option --cap-add=SYS_PTRACE lui ajoute cette capacité, le temps du diagnostic. Elle est nécessaire ici parce que le processus étudié appartient à un autre utilisateur (65532) : ce n'est pas une règle générale, une cible qui tournerait sous le même uid que le conteneur d'outils ne l'exigerait pas forcément.
docker run --rm --pid=container:pointage-test --network=container:pointage-test --cap-add=SYS_PTRACE \ busybox:1.37@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec \ sh -c 'ls /proc/1/root/; ls -l /proc/1/exe; cat /proc/1/cmdline | tr "\0" " "; echo'devetcpointageprocsyszoneinfo.ziplrwxrwxrwx 1 65532 65532 0 Nov 3 14:02 /proc/1/exe -> /pointage/pointageCette fois /proc/1/root/ liste le système de fichiers du conteneur étudié (le binaire pointage et zoneinfo.zip), et /proc/1/exe désigne bien /pointage. Vous voyez ce que l'application voit, sans rien avoir ajouté à son image. Le même principe, côté orchestrateur, s'appelle un conteneur éphémère ; il est présenté dans Débugger une application Kubernetes.
Deux autres gestes ne demandent aucun shell. docker cp copie un fichier hors du conteneur, même s'il est basé sur scratch : vous récupérez le binaire pour l'examiner sur votre machine.
docker cp pointage-test:/pointage /tmp/pointage-copiels -l /tmp/pointage-copierm -f /tmp/pointage-copie-rwxr-xr-x 1 ansible ansible 9366534 Nov 3 13:55 /tmp/pointage-copieQuand il vous faut un shell dans l'image même, deux solutions évitent de dégrader l'image livrée. La première est l'image distroless debug, qui ajoute busybox à l'image de base ; utilisée comme --entrypoint sh, elle donne un shell et les commandes de /busybox.
docker run --rm --entrypoint sh \ gcr.io/distroless/static-debian13:debug@sha256:63b4c0065ff7bd68753510400b2d5cedd5d5571d15eac4be0b6b0dd236a2d148 \ -c 'echo je suis le shell busybox; ls /busybox | head -3'je suis le shell busybox[[[acpidLa seconde est l'étape de construction elle-même. Avec --target build, vous gardez une image de débogage : même compilateur, mêmes sources, utilisable pour reproduire un problème de code ou de compilation, sans jamais la publier. Elle ne reproduit pas les pannes d'environnement de l'image finale : l'image golang a des certificats, une base de fuseaux et tourne en root. Pour l'environnement d'exécution, utilisez le conteneur d'outils voisin décrit plus haut, branché sur l'image finale. Vous avez déjà construit pointage:build dans la section précédente ; docker run --rm -it --entrypoint bash pointage:build ouvre un shell dedans.
Comment nettoyer après les mesures ?
Section intitulée « Comment nettoyer après les mesures ? »La démonstration a laissé un serveur HTTPS de test, un conteneur, des images pointage:* et le dossier de travail. Arrêtez le serveur, supprimez le conteneur, les images, puis le dossier. Le || true évite qu'une image déjà absente ne fasse échouer la suite des commandes.
pkill -f 's_server -accept 8443'docker rm -f pointage-test pointage-export 2>/dev/null || truedocker image ls --format '{{.Repository}}:{{.Tag}}' | grep '^pointage:' | xargs -r docker rmicd ~ && rm -f ~/pointage/* && rmdir ~/pointageLes images de base utilisées par FROM (golang et gcr.io/distroless/static-debian13) vivent dans le cache de construction, pas dans la liste de docker image ls ; busybox et l'image distroless debug, lancées par docker run, y figurent. Retirez ces deux-là avec docker rmi et leur référence relevée par docker image ls, puis videz le cache avec docker builder prune -af. Cette dernière commande efface tout le cache de construction de la machine, pas seulement celui de cette leçon.
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab part d'une application construite en une seule étape, dont l'image embarque ses outils de fabrication. Vous produisez une image finale qui la fait tourner sans ces outils, ni dans ses fichiers ni dans son poids, sur une image de base prévue pour l'exécution, et dont le produit est fabriqué à chaque construction au lieu d'être recopié. Deux contraintes s'y ajoutent : l'outil de fabrication refuse de tourner sans ses outils, et seul le produit doit passer d'une étape à l'autre. Seul l'état du système est contrôlé, avec dsoxlab check, jamais les commandes tapées.
À retenir
Section intitulée « À retenir »-
Un build multi-étapes livre seulement la dernière étape : sur cet exemple, 1,43 Go en 10 couches avec une seule étape, 14,7 Mo en une couche avec deux ; ce qui n'est pas copié par
COPY --fromne part pas. -
Une image
scratchne reçoit que ce que vous copiez : un binaire statique (CGO_ENABLED=0pour Go), des certificats dans/etc/ssl/certs/ca-certificates.crt, des fuseaux horaires, un utilisateur numérique. -
Chaque oubli a son message :
exec … no such file or directorypour un binaire dynamique,x509: certificate signed by unknown authoritysans certificats,unknown time zonesans fuseaux,unable to find user nobodyaudocker run(code 125) pour unUSERpar nom. -
Distroless apporte fuseaux et certificats publics, pèse plus que
scratch(21,4 Mo contre 14,7 Mo ici), tourne en root par défaut, et se décline ennonrootet endebugavec un shell. -
Sans
--target, BuildKit saute une étape dont rien ne dépend : un test cassé laisse passer la construction. Pour quego testbloque, faites copier le binaire depuis l'étape de test (COPY --from=test). -
Pour contrôler une image sans outils, ne lancez pas un shell qui n'y est pas. Sous
dash,command -v go gcc gitne regarde que le premier nom : posez la question par une boucle.docker run image shpasseshen argument à l'ENTRYPOINTsans ouvrir de shell, alors que--entrypoint shen cherche un et répond 127 s'il n'y en a pas. Pour lire le contenu, utilisezdocker create,docker exportettar -t. -
Testez l'image finale par un script, pas seulement le code :
verifier-image.shlance l'image, interroge/sante,/heure,/uidet la connexion HTTPS, et s'arrête avec un code non nul dès qu'une réponse diffère. Chaquecurl -fsSpasse par une affectation (heure=$(curl …)) avant letest, sinonset -ene voit pas son échec. -
Déboguer sans alourdir l'image : un conteneur d'outils avec
--pid=container:et--network=container:(plus--cap-add=SYS_PTRACEpour lire/proc/1/root),docker cp, l'image distrolessdebug, ou l'étape--target build.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : BuildKit : secrets de build et accès privé
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications