Rien ne dit « chaîne de livraison saine et calme » comme une construction Docker qui explose avec failed to solve cinq minutes avant une livraison. Votre journal CI se transforme en roman policier écrit par un compilateur. Quelqu’un suggère « relancez juste », comme si les systèmes de production étaient alimentés par l’optimisme.
Ce guide est la version pragmatique : ce que failed to solve signifie réellement (c’est BuildKit qui se plaint, pas Docker), comment trouver rapidement la vraie erreur, et les corrections qui fonctionnent de manière fiable dans des contraintes réelles : réseaux instables, runners verrouillés, dépôts surdimensionnés et contrôles « sécurité » qui cassent les builds.
Ce que signifie réellement « failed to solve »
Lorsque vous voyez failed to solve, vous regardez presque toujours une remontée d’erreur de BuildKit. BuildKit est le moteur de construction moderne de Docker : il construit un graphe de dépendances (couches, sources, montages), puis le « résout » comme un plan de construction. Si quelque chose casse n’importe où dans ce graphe — récupération d’une image de base, lecture d’un fichier dans le contexte de build, exécution d’une commande RUN, calcul d’une somme de contrôle, décompression d’une archive — BuildKit rapporte l’échec sous la forme « failed to solve ». Ce n’est pas pour être mystérieux. C’est juste qu’il n’est pas très loquace non plus.
Il y a trois implications clés :
- La vraie erreur se trouve presque toujours quelques lignes plus haut. « failed to solve » est le générique, pas le film.
- L’étape en échec est un nœud dans un graphe. Le numéro d’étape dans les logs peut être trompeur si des étapes tournent en parallèle ou sont mises en cache.
- Le contexte compte plus que la pureté du Dockerfile. Beaucoup d’échecs ne sont pas des « bugs de Dockerfile » ; ce sont des problèmes d’environnement, de réseau, de permissions ou de taille du contexte.
Si vous voulez un modèle mental utile opérationnellement : la construction Docker est un pipeline d’entrées (fichiers de contexte, images de base, secrets, réseau) vers des transformations déterministes (couches). « Failed to solve » signifie qu’une entrée est manquante, illisible, non approuvée ou trop lente pour être considérée comme vivante.
Une citation, parce que c’est toujours douloureusement vrai dans les systèmes de build et ailleurs : « L’espoir n’est pas une stratégie. »
— Gene Kranz
Playbook de diagnostic rapide
Ceci est l’ordre qui fait gagner le plus de temps dans la plupart des environnements. Ne faites pas de freestyle. Suivez les étapes jusqu’à ce que vous trouviez le goulot d’étranglement.
1) Localisez la première vraie erreur, pas le dernier emballage
- Remontez au premier message d’échec non bruyant. BuildKit imprime souvent plusieurs erreurs en cascade.
- Identifiez à quelle catégorie elle appartient : contexte, syntaxe, réseau, authentification, permissions, plateforme, cache ou runtime.
2) Confirmez le builder et ses paramètres
- BuildKit est-il activé ? Utilisez-vous buildx ? Quel driver (docker, docker-container, kubernetes) ?
- L’échec se produit-il sur un builder distant où le système de fichiers/le réseau est différent de votre poste ?
3) Vérifiez d’abord l’intégrité du contexte de build
- La plupart des « corrections instantanées » sont des problèmes de contexte : fichiers manquants, .dockerignore incorrect, contexte énorme, permissions.
- Vérifiez les chemins référencés dans COPY/ADD et confirmez qu’ils sont à l’intérieur du contexte de build.
4) Si c’est réseau/auth, reproduisez avec une seule requête
- Essayez de tirer l’image de base avec les mêmes identifiants et la même configuration réseau.
- Tentez de faire un curl vers le dépôt de paquets depuis le runner (pas depuis votre poste).
5) Si c’est une étape RUN, réduisez-la
- Rendez la commande RUN en échec visible (pas de mega-lignes « && … && … » pour le débogage).
- Supprimez temporairement les flags de parallélisme et ajoutez de la sortie verbeuse.
6) Si c’est lié au cache, prouvez-le
- Relancez avec
--no-cacheou nettoyez les caches pour confirmer que vous ne pourchassez pas un état obsolète. - Puis corrigez les clés de cache, pas les symptômes.
Blague #1 : Traitez « failed to solve » comme une crise de colère d’un enfant — quelque chose de réel s’est passé plus tôt, et maintenant tout le monde crie à propos de ça.
Catégories de corrections instantanées (et pourquoi elles arrivent)
Catégorie A : erreurs de syntaxe Dockerfile et échecs du parseur
Ceux-là sont agréables. Ils échouent vite et de manière cohérente. Vous verrez des erreurs comme :
failed to parse Dockerfileunknown instructioninvalid reference format
La correction est presque toujours une faute de frappe, un \ manquant, un mauvais quoting, ou l’utilisation d’une fonctionnalité non supportée par votre builder. Si vous copiez des snippets depuis des blogs, supposez qu’ils sont faux jusqu’à preuve du contraire.
Catégorie B : problèmes de contexte de build (fichiers manquants, mauvais chemins, .dockerignore)
C’est la « correction instantanée » la plus fréquente, et la plus facile à mal diagnostiquer car les gens confondent la structure du dépôt avec le contexte de build. Le contexte est le répertoire que vous passez à docker build. BuildKit ne peut voir que les fichiers sous ce répertoire (moins les motifs ignorés). Si votre Dockerfile dit :
COPY ../secrets /secrets(non)COPY build/output/app /app(peut-être, si ça existe au moment du build)COPY . /src(fonctionne, mais souvent stupide)
Alors la correction du contexte décide de votre destin. Une petite erreur dans .dockerignore peut devenir « checksum of ref … not found », « failed to compute cache key », ou le classique « failed to walk … no such file or directory ».
Catégorie C : échecs de permissions et propriété
BuildKit exécute des étapes avec des utilisateurs, des montages et des comportements en lecture seule spécifiques. Entre Docker rootless, des runners CI durcis et des répertoires NFS d’entreprise, vous pouvez rencontrer :
permission deniedlors de la lecture des fichiers de contexteoperation not permittedsur chmod/chownfailed to create shim tasklorsque les namespaces utilisateur entrent en collision
Les corrections vont du banal (chmod d’un fichier) au structurel (arrêtez d’essayer de chown 200k fichiers pendant le build).
Catégorie D : réseau, DNS, proxies et MITM d’entreprise
Les builds Docker téléchargent des images de base et des paquets. Cela implique DNS, TLS, proxies et timeouts. BuildKit effectue aussi des récupérations en parallèle, ce qui peut aggraver un réseau instable. Vous verrez :
failed to fetch oauth tokeni/o timeoutx509: certificate signed by unknown authoritytemporary failure in name resolution
Cela se corrige souvent instantanément en configurant correctement les variables de proxy, en important une CA d’entreprise, ou en utilisant un miroir de registre réellement accessible depuis le runner.
Catégorie E : authentification au registre et limites de taux
Les pulls non authentifiés subissent des limites de débit. Des identifiants erronés renvoient des 401/403. Les registres privés avec tokens expirés vous frappent au pire moment. BuildKit enveloppe parfois cela dans un terne « failed to solve », mais l’erreur sous-jacente est généralement explicite si vous remontez le log.
Catégorie F : incompatibilité de plateforme et d’architecture
Les builds multi-arch sont formidables jusqu’à ce qu’ils ne le soient plus. Si votre image de base ne supporte pas la plateforme cible, vous verrez des erreurs comme :
no match for platform in manifestexec format error
La correction : spécifier --platform, choisir une base multi-arch, ou arrêter de construire des images amd64 sur un runner arm64 sans émulation configurée.
Catégorie G : bizarreries de cache/solver (checksum, clé de cache, invalidation)
BuildKit est agressif sur le caching, ce qui est bien jusqu’à ce que votre build dépende de quelque chose qui ne devrait pas être caché. Les erreurs peuvent inclure :
failed to compute cache keyfailed to calculate checksum of refrpc error: code = Unknown desc = …
Cela signifie souvent « le fichier que BuildKit attend n’est pas dans le contexte » ou « votre état de cache est corrompu » ou « votre builder distant a perdu un montage ». Ce n’est pas mystique ; c’est juste de l’état.
Catégorie H : secrets, montages SSH et identifiants au build
Les builds modernes utilisent RUN --mount=type=secret et RUN --mount=type=ssh. Très bien. Mais si le CI ne transmet pas le secret/l’agent ssh, BuildKit peut échouer avec :
secret not foundssh agent requested but no SSH_AUTH_SOCK
Correction : passer le secret ou arrêter de prétendre que votre build a accès à des choses privées sans les brancher correctement.
Tâches pratiques : commandes, sorties, décisions
Voici des vérifications réelles que vous pouvez exécuter sur une machine dev ou un runner CI. Chacune inclut : la commande, ce que signifie la sortie, et la décision suivante à prendre. Exécutez-les dans l’ordre qui correspond à votre catégorie d’échec.
Task 1: Confirm whether BuildKit is in play
cr0x@server:~$ docker build --help | head -n 5
Usage: docker build [OPTIONS] PATH | URL | -
Build an image from a Dockerfile
Ce que cela signifie : L’affichage d’aide seul ne vous dira pas BuildKit, mais confirme que vous utilisez le point d’entrée CLI classique.
Décision : Ensuite, vérifiez explicitement l’environnement et l’état du builder.
cr0x@server:~$ echo $DOCKER_BUILDKIT
Ce que cela signifie : Vide signifie généralement « défaut ». Sur beaucoup d’installations modernes, le défaut est BuildKit de toute façon.
Décision : Inspectez les builders buildx pour voir quel backend vous utilisez réellement.
Task 2: Inspect buildx and active builder
cr0x@server:~$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default docker running v0.12.5 linux/amd64,linux/arm64
ci-builder * docker-container running v0.12.5 linux/amd64
Ce que cela signifie : Vous utilisez un builder containerisé (docker-container) nommé ci-builder. Ce builder a son propre réseau, DNS et stockage de cache.
Décision : Si des échecs ressemblent à DNS/timeouts ou fichiers manquants, reproduisez en utilisant le même builder, pas le défaut.
Task 3: Re-run with plain progress to find the real failing line
cr0x@server:~$ docker buildx build --progress=plain -t testimg:debug .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 1.27kB done
#2 [internal] load metadata for docker.io/library/alpine:3.19
#2 ERROR: failed to do request: Head "https://registry-1.docker.io/v2/library/alpine/manifests/3.19": dial tcp: lookup registry-1.docker.io: temporary failure in name resolution
------
> [internal] load metadata for docker.io/library/alpine:3.19:
------
failed to solve: failed to do request: Head "https://registry-1.docker.io/v2/library/alpine/manifests/3.19": dial tcp: lookup registry-1.docker.io: temporary failure in name resolution
Ce que cela signifie : Ce n’est pas un problème de Dockerfile. Le DNS depuis le builder est cassé.
Décision : Passez aux vérifications réseau/DNS. Ne touchez pas encore au Dockerfile ; vous créeriez juste de nouveaux bugs.
Task 4: Verify the build context size (big contexts cause slow “failed to solve”)
cr0x@server:~$ docker buildx build --progress=plain --no-cache -t testimg:ctx .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 2.10kB done
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#3 [internal] load build context
#3 transferring context: 1.48GB 34.2s done
Ce que cela signifie : Vous envoyez 1,48Go de contexte au builder. Ce n’est pas « un peu grand ». C’est « pourquoi votre repo git sert-il aussi de serveur de fichiers ? »
Décision : Corrigez .dockerignore et réduisez la portée de COPY. Les contextes volumineux causent des timeouts, du thrash de cache et ralentissent le CI.
Task 5: Show what’s being ignored by .dockerignore (practical sanity check)
cr0x@server:~$ sed -n '1,120p' .dockerignore
.git
node_modules
dist
*.log
Ce que cela signifie : Ça semble raisonnable, mais peut-être avez-vous oublié build/, target/, .venv/ ou des gros jeux de données de test.
Décision : Ajoutez des patterns pour ignorer les artefacts générés et les caches locaux ; assurez-vous que le build conserve bien ce dont il a besoin.
Task 6: Catch “COPY failed: file not found” before you waste time
cr0x@server:~$ grep -nE '^(COPY|ADD) ' Dockerfile
12:COPY build/output/app /app/app
13:COPY configs/prod.yaml /app/config.yaml
Ce que cela signifie : Le Dockerfile attend que build/output/app existe dans le contexte de build au moment du build.
Décision : Si le CI ne génère pas ce chemin avant docker build, le build échouera. Soit produisez l’artefact dans une étape CI préalable, soit compilez dans le Dockerfile (multi-stage).
Task 7: Confirm the referenced files exist in the context
cr0x@server:~$ ls -la build/output/app configs/prod.yaml
ls: cannot access 'build/output/app': No such file or directory
-rw-r--r-- 1 cr0x cr0x 842 Jan 2 09:41 configs/prod.yaml
Ce que cela signifie : Un des COPY échouera immédiatement.
Décision : Décidez si (a) produire l’artefact avant le docker build, ou (b) convertir en build multi-stage qui compile à l’intérieur de l’image.
Task 8: Validate base image pull and authentication (separate from build)
cr0x@server:~$ docker pull alpine:3.19
3.19: Pulling from library/alpine
Digest: sha256:de0eb0b3f2a47ba1b5d1c61c87a1f1d9b5a4f34b3a4a1d7f3d6c2c6a7f1d2d3c
Status: Image is up to date for alpine:3.19
docker.io/library/alpine:3.19
Ce que cela signifie : L’accès au registre est OK depuis l’hôte Docker. Si buildx utilise un builder distant, ce builder peut toujours être cassé — ne déclarez pas la victoire trop vite.
Décision : Si le build échoue encore en tirant la même image, inspectez le réseau ou les paramètres proxy du builder.
Task 9: Check DNS and outbound connectivity from the builder container
cr0x@server:~$ docker ps --filter name=buildx_buildkit --format '{{.ID}} {{.Names}}'
a1b2c3d4e5f6 buildx_buildkit_ci-builder0
cr0x@server:~$ docker exec -it buildx_buildkit_ci-builder0 sh -lc 'cat /etc/resolv.conf && nslookup registry-1.docker.io 2>/dev/null | head -n 5'
nameserver 127.0.0.11
options ndots:0
Server: 127.0.0.11
Address: 127.0.0.11:53
Ce que cela signifie : Le builder utilise le DNS embarqué de Docker (127.0.0.11). Si nslookup bloque ou échoue, vous avez probablement un problème de configuration DNS du daemon ou un blocage des sorties UDP/TCP 53.
Décision : Corrigez la configuration DNS du daemon Docker ou fournissez des serveurs DNS explicites pour l’environnement du builder.
Task 10: Identify proxy settings mismatch (classic corporate failure mode)
cr0x@server:~$ env | grep -iE 'http_proxy|https_proxy|no_proxy'
HTTP_PROXY=http://proxy.corp:8080
HTTPS_PROXY=http://proxy.corp:8080
NO_PROXY=localhost,127.0.0.1,.corp
Ce que cela signifie : Le proxy est défini dans votre shell. Le container builder peut ne pas l’hériter automatiquement selon la façon dont il est créé.
Décision : Passez les arguments de build pour le proxy ou configurez le builder/daemon pour utiliser le proxy de manière cohérente. Si le TLS est intercepté, vous devrez aussi ajouter la CA d’entreprise dans l’image de build.
Task 11: Catch platform mismatch quickly
cr0x@server:~$ docker buildx imagetools inspect alpine:3.19 | sed -n '1,40p'
Name: docker.io/library/alpine:3.19
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Manifests:
Name: docker.io/library/alpine:3.19@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Platform: linux/amd64
Name: docker.io/library/alpine:3.19@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
Platform: linux/arm64
Ce que cela signifie : Cette image de base est multi-arch. Bien. Si votre image de base montre une seule plateforme et que vous construisez pour l’autre, vous avez trouvé la cause racine.
Décision : Choisissez une image de base avec la plateforme cible ou définissez --platform explicitement et assurez-vous que l’émulation est configurée si nécessaire.
Task 12: Force a clean build to rule out cache corruption
cr0x@server:~$ docker buildx build --no-cache --progress=plain -t testimg:nocache .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 1.27kB done
#2 [internal] load metadata for docker.io/library/alpine:3.19
#2 DONE 0.9s
#3 [internal] load build context
#3 transferring context: 52.4MB 1.1s done
#4 [1/6] FROM docker.io/library/alpine:3.19@sha256:bbbb...
#4 DONE 0.0s
#5 [2/6] RUN apk add --no-cache ca-certificates
#5 DONE 2.8s
Ce que cela signifie : Si un build propre réussit mais que les builds cachés échouent de façon intermittente, vous avez des problèmes d’état/cache (souvent pression disque du builder distant ou enregistrements de cache corrompus).
Décision : Nettoyez le stockage du builder et resserrez la stratégie d’export/import de cache plutôt que « désactiver le cache pour toujours ».
Task 13: Check builder disk pressure (the silent killer)
cr0x@server:~$ docker exec -it buildx_buildkit_ci-builder0 sh -lc 'df -h /var/lib/buildkit | tail -n 1'
overlay 20G 19G 1.0G 95% /
Ce que cela signifie : 95% d’occupation sur le système de fichiers du builder. Attendez-vous à des erreurs étranges : échecs de décompression, « no space left », problèmes aléatoires de cache et ralentissements spectaculaires.
Décision : Émondez le cache de build, augmentez le stockage ou arrêtez d’exporter des caches énormes que vous ne réutilisez jamais.
Task 14: Prune BuildKit cache safely (and interpret the output)
cr0x@server:~$ docker buildx prune -f --verbose
ID RECLAIMABLE SIZE LAST ACCESSED
v1:9n8m7l6k5j4h3g2f1d0s true 1.2GB 2 days ago
v1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa true 650MB 7 days ago
Total: 1.85GB reclaimed
Ce que cela signifie : Vous avez récupéré 1,85Go. Si votre builder était à 95%, cela peut transformer un build échoué en build fonctionnel immédiatement.
Décision : Si le prune corrige le problème, mettez en place des politiques de rétention automatiques du cache et surveillez l’utilisation disque du builder. Si cela ne suffit pas, continuez l’investigation.
Task 15: Diagnose “secret not found” and fix wiring
cr0x@server:~$ docker buildx build --progress=plain --secret id=npmrc,src=$HOME/.npmrc -t testimg:secrets .
#7 [4/6] RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
#7 DONE 18.9s
Ce que cela signifie : Votre montage de secret est présent et l’étape a réussi. Si vous omettez --secret, BuildKit échoue l’étape à l’exécution.
Décision : En CI, assurez-vous que le secret existe, est correctement scopié et est passé par la commande build. Ne l’inscrivez jamais dans l’image. Jamais.
Task 16: Make a failing RUN step debuggable
cr0x@server:~$ docker buildx build --progress=plain --build-arg DEBUG=1 -t testimg:debugrun .
#9 [5/6] RUN set -eux; apk add --no-cache git; git --version
+ apk add --no-cache git
(1/4) Installing ca-certificates (20241121-r0)
(2/4) Installing libcurl (8.6.0-r0)
(3/4) Installing pcre2 (10.42-r2)
(4/4) Installing git (2.45.2-r0)
OK: 26 MiB in 24 packages
+ git --version
git version 2.45.2
Ce que cela signifie : set -eux affiche la commande exacte qui échoue et s’arrête à la première erreur. C’est ainsi que vous arrêtez de deviner.
Décision : Gardez le modèle « mode debug » dans votre Dockerfile via un build arg pour pouvoir l’activer quand le CI casse.
Erreurs courantes : symptômes → cause racine → correction
1) « failed to compute cache key » près d’un COPY/ADD
Symptômes : Le build échoue à une ligne COPY avec un langage de cache-key/checksum ; mentionne parfois « not found ».
Cause racine : Le chemin source n’existe pas dans le contexte de build (ou est ignoré par .dockerignore), donc BuildKit ne peut pas le hacher pour calculer la clé de cache.
Correction : Assurez-vous que le fichier existe avant le build, ajustez le répertoire de contexte, ou mettez à jour .dockerignore. Préférez copier seulement les fichiers nécessaires, pas COPY . ..
2) « COPY failed: stat … no such file or directory »
Symptômes : Erreur simple de fichier manquant.
Cause racine : Mauvais chemin relatif, mauvais contexte, ou CI qui build depuis un répertoire de travail différent de celui que vous imaginiez.
Correction : Soyez explicite : exécutez docker build -f path/to/Dockerfile path/to/context. En CI, affichez pwd et ls avant le build. Rendre les chemins ennuyeux.
3) « failed to do request: Head … temporary failure in name resolution »
Symptômes : La récupération des métadonnées de l’image de base échoue.
Cause racine : DNS cassé dans le builder, ou sorties bloquées.
Correction : Configurez le DNS du daemon, corrigez la politique réseau du runner, ou lancez le builder avec des serveurs DNS connus. Validez depuis l’intérieur du container builder.
4) « x509: certificate signed by unknown authority » pendant l’installation de paquets ou le pull d’une image
Symptômes : Échecs TLS en touchant des registres ou miroirs de paquets, spécialement en entreprise.
Cause racine : Interception TLS / CA d’entreprise non approuvée dans l’image de base ou le builder.
Correction : Installez la CA d’entreprise dans l’étape de build (et idéalement dans une image de base partagée). Ne désactivez pas la vérification TLS comme « correction » sauf si vous aimez la gestion d’incidents.
5) « failed to fetch oauth token » ou 401/403 lors du pull d’images de base
Symptômes : Erreurs d’auth au registre, souvent intermittentes quand les tokens expirent.
Cause racine : docker login manquant en CI, helper d’identifiants incorrect, tokens expirés, ou pull depuis un registre privé sans passer les creds au builder distant.
Correction : Connectez-vous avant le build ; pour les builders buildx distants, assurez-vous que les credentials sont disponibles dans ce contexte de builder. Confirmez avec un docker pull direct depuis le même environnement.
6) « no match for platform in manifest »
Symptômes : Le build échoue tôt lors de la résolution de l’image de base.
Cause racine : L’image de base n’est pas publiée pour votre architecture cible.
Correction : Utilisez une image de base multi-arch, épinglez à la plateforme correcte, ou ajustez vos runners. Si vous buildez sur arm64 mais délivrez amd64, soyez explicite.
7) « exec format error » pendant RUN
Symptômes : L’image de base est tirée, mais l’exécution des binaires échoue.
Cause racine : Incompatibilité d’architecture à l’exécution (ex. binaire amd64 sur image arm64) ou émulation QEMU non configurée.
Correction : Alignez image de base + binaires + plateforme. Si vous utilisez l’émulation, assurez-vous que binfmt/qemu est configuré sur l’hôte qui exécute le builder.
8) « no space left on device » (parfois déguisé en erreurs de décompression)
Symptômes : Échecs aléatoires lors de la décompression de couches, écriture de cache ou export d’images.
Cause racine : Stockage du builder plein (commun avec le driver docker-container buildx), exhaustion d’inodes, ou limites overlayfs.
Correction : Émondez les caches, augmentez le disque, et arrêtez de copier des répertoires énormes tôt dans le Dockerfile (ça multiplie les couches stockées).
9) « failed to create LLB definition » ou erreurs RPC étranges
Symptômes : BuildKit rapporte des erreurs gRPC/rpc avec « unknown desc ».
Cause racine : Instabilité du builder, mismatch de versions, état corrompu, ou pression sur les ressources (disque, mémoire).
Correction : Redémarrez le builder, mettez à jour BuildKit/buildx, vérifiez les ressources, et reproduisez avec --progress=plain. Si ça disparaît après un prune, c’était de la pression d’état.
10) « secret not found » / échecs de montage SSH
Symptômes : Une étape RUN qui attend un secret/agent ssh échoue immédiatement.
Cause racine : La commande build n’a pas passé --secret ou --ssh, ou le CI n’a pas exposé le secret.
Correction : Branchez-le correctement. Si vous ne pouvez pas, changez le design : récupérez les dépendances dans une autre étape ou conservez-les localement. Ne commettez pas de secrets par frustration.
Trois mini-histoires d’entreprise issues du terrain
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Une équipe a migré ses builds d’un runner GitLab auto-hébergé vers une flotte de runners managés. Même repo, même Dockerfile, mêmes commandes. Le premier build a échoué avec failed to solve sur une étape COPY. Les développeurs ont hausser les épaules en disant « Le fichier est dans le repo ; il ne peut pas manquer. »
Le fichier était généré. Localement, tout le monde l’avait car leur workflow dev exécutait un outil de build qui produisait build/output/app. L’ancien runner CI avait aussi une pré-étape qui lançait le même outil — mais elle vivait dans un template partagé que personne n’avait retenu. La nouvelle pipeline CI avait été « simplifiée », ce qui est le jargon corporate pour « nous avons supprimé les parties ennuyeuses qui faisaient que ça marchait ».
L’hypothèse erronée était subtile : ils supposaient que Docker build pouvait voir l’état complet du repo, y compris des choses qui existeraient « au moment où le build s’exécute ». Mais le contexte de Docker build est un instantané du système de fichiers au moment où vous lancez la commande. Si l’artefact n’est pas là, peu importe à quel point il semble présent spirituellement.
La correction a été de formaliser la création de l’artefact : soit une étape CI dédiée qui produit l’artefact avant de construire l’image, soit un build Docker multi-stage qui compile dans une étape builder. Ils ont choisi le multi-stage. C’était plus lent au début, puis plus rapide après ajustement du cache, et surtout : déterministe.
Le postmortem : quand les builds cassent après des changements de runner, supposez que votre dépendance d’environnement était non documentée. Elle l’était généralement.
Mini-histoire 2 : L’« optimisation » qui s’est retournée contre eux
Une autre organisation avait pour objectif de réduire le temps de CI. Quelqu’un a remarqué que les builds Docker étaient « lents parce qu’ils envoient trop de contexte », et a voulu être astucieux. Ils ont ajouté des patterns agressifs dans .dockerignore pour exclure pratiquement tout sauf le Dockerfile et quelques répertoires. Le CI est devenu plus rapide. Applaudissements.
Puis un candidat release a échoué avec failed to compute cache key: failed to calculate checksum pointant sur un fichier dans une ligne COPY — un fichier qui existait, mais qui était maintenant ignoré. L’équipe l’a réparé en dé-ignorant ce fichier. Le build suivant a échoué pour un autre fichier ignoré. Un autre ajustement. Encore des échecs. Ça a tourné au jeu du whack-a-mole parce que les règles d’ignore avaient été écrites sans les relier au graphe réel de dépendances du Dockerfile.
Le retour de bâton n’était pas seulement les erreurs. C’était le temps passé à redécouvrir ce dont l’image avait réellement besoin, et le risque de sécurité subtil : les développeurs ont commencé à « réparer » temporairement en copiant . pour faire passer les builds, envoyant par inadvertance des identifiants et des fichiers de dev dans les images. C’est comme ça qu’on se retrouve avec .env dans des conteneurs de production et une équipe conformité dans votre calendrier.
La résolution finale a été ennuyeuse et correcte : ils ont reconstruit .dockerignore depuis les principes de base. Ils ont listé chaque fichier copié dans l’image, n’ont ajouté que ces répertoires au contexte, et ont rendu les COPY du Dockerfile spécifiques et stables. Le contexte de build a diminué sans casser les dépendances.
Leçon : optimiser le contexte de build sans comprendre les dépendances de COPY, c’est comme retirer des boulons pour alléger un pont.
Mini-histoire 3 : La pratique ennuyeuse qui a sauvé la journée
Une équipe plateforme exploitait un cluster de builders buildx dédié. Rien de fancy : driver container, stockage persistant, et une politique stricte. Chaque nœud builder exportait des métriques pour l’utilisation disque, l’utilisation d’inodes et la taille des caches. Ils émondaient aussi les caches de build selon un calendrier avec une fenêtre de rétention adaptée à leur charge.
Un vendredi, le build d’une équipe produit a commencé à échouer avec des « failed to solve » sporadiques : parfois lors du déballage de couches, parfois pendant l’export du cache. Les développeurs ont suspecté « Docker est flaky » (théorie intemporelle) et ont commencé à relancer les jobs jusqu’à obtenir un build vert.
L’équipe plateforme a regardé les tableaux de bord. Les disques des builders montaient vers le plein. Pas à cause d’un seul projet, mais parce que plusieurs équipes avaient activé l’export de cache et personne n’avait mis de limites. Quand la pression disque est montée, BuildKit a commencé à échouer de manière désordonnée — parce que les défaillances de stockage sont rarement poliées.
Ils ont agrandi le volume du builder et resserré la politique de prune. Les builds se sont stabilisés immédiatement. Pas d’héroïsme dramatique, juste quelqu’un qui s’est occupé des signaux « ennuyeux » comme l’utilisation disque. L’équipe produit a livré. La release a été calme. C’est le meilleur.
Leçon : le disque du builder est une infrastructure de production. Traitez-le comme tel, ou il vous traitera comme un passe-temps.
Checklists / plan étape par étape
Étape par étape : du CI rouge à une cause racine confirmée
- Relancez une fois avec
--progress=plain. Votre objectif est la clarté, pas l’espoir. - Identifiez la phase en échec : internal/context, metadata fetch, COPY/ADD, RUN, export.
- Vérifiez le contexte de build : confirmez que les fichiers référencés existent ; vérifiez
.dockerignore; mesurez la taille du contexte. - Vérifiez la résolution de l’image de base : pull manuel ; inspectez les plateformes ; validez l’auth.
- Vérifiez l’environnement du builder : quel buildx builder, type de driver, DNS/proxy, pression disque.
- Réduisez la commande RUN en échec : ajoutez
set -eux, retirez les commandes enchaînées, relancez. - Écartez la corruption de cache : lancez avec
--no-cache; si ça corrige, réparez la stratégie de cache. - Appliquez la plus petite correction qui la rend déterministe. La détermination bat l’ingéniosité.
Checklist : réduire la probabilité d’échecs Dockerfile la semaine prochaine
- Gardez
.dockerignorepetit, explicite et relu avec les changements de Dockerfile. - Privilégiez les builds multi-stage pour que la création d’artefacts fasse partie du graphe de build, pas d’une hypothèse externe.
- Épinglez les images de base sur des tags que vous contrôlez (et considérez les digests pour les pipelines critiques).
- Arrêtez d’utiliser
COPY . .comme style de vie. Copiez des répertoires spécifiques. - Scindez les RUN risqués. Une étape pour télécharger les paquets, une pour construire, une pour nettoyer.
- Surveillez le disque des builders. Mettez en place des politiques de prune. Documentez la configuration du builder comme si c’était une base de données (parce que ça se comporte comme telle).
- Gérez explicitement les proxies et les CA d’entreprise. Si vous en avez besoin, codez-les.
- Utilisez des montages de secrets pour les identifiants ; vérifiez que le CI les transmet ; ne jamais incorporer de secrets dans les couches.
Blague #2 : « On va juste désactiver le cache pour réparer le build » est l’équivalent CI de débrancher l’alarme incendie pour pouvoir dormir.
Faits intéressants et contexte historique
- BuildKit a commencé comme un projet séparé puis est devenu le builder par défaut pour de nombreuses installations Docker parce que le builder classique ne pouvait pas évoluer en cache et concurrence aussi bien.
- Le concept « LLB » (Low-Level Build) est le format interne de graphe de BuildKit ; « solve » se réfère au calcul et à l’exécution de ce graphe.
- Le builder original de Docker exécutait les étapes du Dockerfile séquentiellement ; BuildKit a introduit plus de parallélisme et un cache plus intelligent, ce qui change aussi la manière dont les échecs apparaissent.
- .dockerignore existe parce que les premiers builds Docker étaient douloureusement lents quand les utilisateurs envoyaient involontairement des dépôts entiers (y compris .git) comme contexte.
- Les limites de taux des registres sont devenues un vrai problème opérationnel à mesure que l’usage CI a explosé ; beaucoup d’organisations l’ont découvert seulement après que les pipelines ont commencé à échouer aux heures de pointe.
- Les images multi-architecture sont devenues courantes avec la montée des serveurs ARM et Apple Silicon ; les erreurs de mismatch de plateforme ont augmenté en conséquence.
- Docker rootless et les runners durcis ont amélioré la sécurité mais ont rendu les hypothèses de permissions dans d’anciens Dockerfiles plus sujettes à l’échec.
- Le support des secrets de build s’est fortement amélioré avec les montages BuildKit, réduisant le besoin de hacks ARG qui fuient dans les couches.
- Les builds reproductibles sont devenus une attente plus forte avec la montée des préoccupations sur la chaîne d’approvisionnement ; « ça marche si vous le relancez » n’est plus acceptable.
FAQ
Pourquoi Docker affiche-t-il « failed to solve » au lieu de la vraie erreur ?
Parce que BuildKit rapporte un échec de haut niveau pour le graphe de build (« solve failed ») et inclut l’erreur sous-jacente au-dessus. Utilisez --progress=plain pour rendre l’erreur sous-jacente évidente.
« Failed to solve » est-ce toujours un problème de Dockerfile ?
Non. C’est fréquemment réseau/DNS, authentification registre, contexte de build, ou pression disque du builder. Traitez le Dockerfile comme coupable seulement après avoir prouvé que l’environnement est sain.
Pourquoi ça marche sur mon laptop mais pas en CI ?
Les runners CI ont des politiques réseau différentes, des proxies, des credentials, des permissions système, et souvent un chemin de contexte de build différent. De plus, le CI peut utiliser un builder buildx distant avec son propre environnement.
Comment voir rapidement quelle étape a réellement échoué ?
Lancez le build avec docker buildx build --progress=plain et remontez au premier bloc ERROR. Ignorez la ligne finale d’emballage tant que vous n’avez pas lu l’erreur réelle.
Quel est le moyen le plus rapide de détecter les problèmes de contexte de build ?
Vérifiez .dockerignore, greppez les lignes COPY/ADD, et confirmez que ces chemins sources existent sous le répertoire de contexte. Si votre transfert de contexte pèse des centaines de Mo ou plus, corrigez cela aussi.
Dois-je épingler les images de base par digest ?
Si vous tenez à la répétabilité et au contrôle de la chaîne d’approvisionnement, oui — surtout pour les pipelines de production. Si vous voulez des mises à jour de sécurité régulières, utilisez un processus contrôlé pour mettre à jour les digests plutôt que des tags flottants.
Comment résoudre rapidement les problèmes de proxy et CA d’entreprise lors des builds ?
Transmettez les paramètres de proxy de manière cohérente au builder et aux étapes de build, et installez la CA d’entreprise dans l’image (et parfois dans l’environnement du builder). Ne désactivez pas la vérification TLS comme solution de contournement.
Quelle est la bonne façon d’utiliser les secrets pendant docker build ?
Utilisez les montages de secrets BuildKit (--secret et RUN --mount=type=secret). Vérifiez que le CI fournit le secret. Évitez ARG/ENV pour les secrets car ils fuient dans l’historique et les couches.
Pourquoi les erreurs liées au cache semblent-elles si étranges ?
Parce que les caches BuildKit sont adressés par contenu et liés au graphe de build. Quand le contenu référencé n’est pas disponible (fichier ignoré, montage manquant, cache corrompu), vous obtenez des erreurs de checksum et de clé de cache.
Quand devrais-je utiliser --no-cache ?
Comme outil de diagnostic. Si ça corrige le build, vous avez prouvé un problème d’état/cache. Ensuite, réparez la stratégie de cache ou la santé du builder — ne renoncez pas au cache à moins d’aimer un CI plus lent.
Conclusion : prochaines étapes qui réduisent réellement la douleur
« Failed to solve » n’est pas un message. C’est une étiquette de catégorie. Votre travail est de l’éplucher jusqu’à la première erreur concrète, puis de décider si vous avez affaire à du contexte, du réseau, de l’auth, de la plateforme, des permissions, du cache ou du runtime.
Faites ces trois choses et votre futur vous enverra une note de remerciement (silencieusement, en ne vous réveillant pas à 2h du matin) :
- Standardisez sur
--progress=plaindans les logs CI (au moins en cas d’échec) pour que la vraie erreur soit visible. - Rendez le contexte déterministe : chemins COPY explicites,
.dockerignorediscipliné, et builds multi-stage pour les artefacts générés. - Exploitez vos builders : surveillez le disque, émondez intelligemment, et traitez l’infrastructure de build comme la production — parce qu’elle est en amont de la production.
La plupart des erreurs Dockerfile « failed to solve » sont réparées instantanément dès que vous arrêtez de fixer la dernière ligne et que vous commencez à traiter les builds comme des systèmes : entrées, état et domaines de défaillance.