Déboguer des builds Docker en échec : lire et corriger les erreurs de Dockerfile
Construire une image Docker paraît souvent simple… jusqu’au moment où docker build échoue avec un message obscur, une étape qui “reste bloquée”, ou un échec qui n’arrive que sur la CI. Ce tutoriel explique comment lire les erreurs de build, où chercher la cause, et comment corriger durablement un Dockerfile. Il inclut des commandes réelles, des techniques de diagnostic avancées (BuildKit, logs, cache, inspection), et des exemples concrets d’erreurs fréquentes.
Sommaire
- Comprendre ce que fait réellement
docker build - Activer des logs utiles : BuildKit, progression, détails
- Lire un échec : identifier l’étape, le contexte, la commande
- Débogage interactif : entrer dans une étape qui échoue
- Problèmes de contexte et
.dockerignore - Erreurs de syntaxe Dockerfile et pièges de parsing
- Échecs liés aux commandes
RUN(apt, apk, pip, npm…) - Problèmes réseau, DNS, proxy, certificats
- Cache : faux positifs, incohérences, invalidation
- Erreurs multi-stage :
COPY --from, chemins, utilisateurs - Permissions, utilisateurs, UID/GID, fichiers non accessibles
- Architecture/plateforme : amd64 vs arm64
- Diagnostiquer une image produite : inspection et tests
- Checklist de dépannage rapide
Comprendre ce que fait réellement docker build
Un build Docker n’exécute pas “un script” : il exécute une suite d’instructions (les couches) décrites dans un Dockerfile. Chaque instruction (FROM, COPY, RUN, ENV, etc.) crée une nouvelle couche (ou modifie la configuration) et peut être mise en cache.
Points clés :
- Le build a un contexte : le répertoire envoyé au daemon Docker (ou à BuildKit).
COPYetADDne peuvent lire que dans ce contexte (saufCOPY --from=d’un stage).RUNs’exécute dans un conteneur temporaire basé sur la couche précédente.- Les erreurs se produisent souvent parce qu’un fichier n’est pas dans le contexte, qu’une commande
RUNéchoue, ou qu’un cache masque un problème.
Commande de base :
docker build -t monimage:debug .
Si vous utilisez docker buildx (souvent le cas sur CI modernes) :
docker buildx build -t monimage:debug .
Activer des logs utiles : BuildKit, progression, détails
1) Forcer BuildKit et un affichage lisible
BuildKit fournit de meilleurs logs, des étapes parallélisées, et des fonctionnalités de debug.
Sous Linux/macOS :
DOCKER_BUILDKIT=1 docker build --progress=plain -t monimage:debug .
--progress=plainaffiche des logs “linéaires” (idéal en CI).- Sans ça, l’affichage “TTY” masque parfois des lignes.
Avec buildx :
docker buildx build --progress=plain -t monimage:debug .
2) Désactiver le cache pour reproduire un problème
Un build qui “marche chez moi” peut être dû au cache local.
docker build --no-cache --progress=plain -t monimage:nocache .
3) Cibler une étape précise (multi-stage)
Pour isoler un problème dans un stage :
docker build --target builder --progress=plain -t monimage:builder .
4) Augmenter la verbosité d’un outil, pas de Docker
Souvent, Docker n’a pas “plus de logs” à donner : c’est la commande dans RUN qui doit être plus verbeuse.
Exemples :
apt-get: ajouter-o Debug::pkgProblemResolver=yes(ponctuellement)pip:pip -v ...npm:npm ci --verbose
Lire un échec : identifier l’étape, le contexte, la commande
Un échec typique ressemble à :
=> ERROR [4/7] RUN apt-get update && apt-get install -y curl
------
> [4/7] RUN apt-get update && apt-get install -y curl:
#0 0.332 Err:1 http://deb.debian.org/debian bookworm InRelease
#0 0.332 Temporary failure resolving 'deb.debian.org'
...
------
Dockerfile:12
--------------------
10 | FROM debian:bookworm
11 |
12 | >>> RUN apt-get update && apt-get install -y curl
13 |
--------------------
ERROR: failed to solve: process "/bin/sh -c apt-get update && apt-get install -y curl" did not complete successfully: exit code: 100
Méthode de lecture :
- Repérer l’instruction fautive (ici
RUN apt-get update...). - Lire la sortie de la commande (DNS, 404, permission, etc.).
- Noter le code de sortie (100 pour apt, 1 générique, 127 commande introuvable…).
- Repérer la ligne du Dockerfile indiquée par BuildKit (super utile).
Débogage interactif : entrer dans une étape qui échoue
Quand une instruction RUN échoue, vous voulez souvent “entrer” dans l’environnement juste avant l’échec.
Technique A : construire jusqu’à l’étape précédente puis lancer un shell
- Construire jusqu’au stage (ou jusqu’à une étape en réorganisant temporairement le Dockerfile).
Si c’est un multi-stage, ciblez le stage :
docker build --target builder -t monimage:builder --progress=plain .
- Démarrer un conteneur interactif :
docker run --rm -it --entrypoint /bin/bash monimage:builder
Si l’image n’a pas bash (Alpine), utilisez sh :
docker run --rm -it --entrypoint /bin/sh monimage:builder
Ensuite, exécutez manuellement la commande qui échoue, en ajoutant de la verbosité.
Technique B : insérer un “breakpoint” dans le Dockerfile
Ajoutez temporairement :
RUN echo "Breakpoint" && sleep 3600
Puis construisez et, dans un autre terminal, trouvez le conteneur de build (avec BuildKit c’est moins direct), mais vous pouvez surtout reproduire la commande dans un conteneur basé sur l’image intermédiaire si vous taguez le stage.
Technique C : utiliser --output et --target (BuildKit)
Pour extraire des artefacts d’un stage et vérifier ce qui a été produit :
docker buildx build --target builder --output type=local,dest=./out .
ls -la ./out
Très utile pour vérifier qu’un binaire a bien été généré, qu’un répertoire contient ce que vous pensez, etc.
Problèmes de contexte et .dockerignore
Symptôme : COPY failed: file not found in build context
Exemple :
COPY failed: file not found in build context or excluded by .dockerignore: stat secret.txt: file does not exist
Causes fréquentes :
- Le fichier n’existe pas dans le répertoire de build (
.). - Le fichier est ignoré par
.dockerignore. - Vous lancez
docker builddepuis le mauvais dossier. - Chemin relatif incorrect.
Commandes de diagnostic :
pwd
ls -la
cat .dockerignore
Test rapide : construire en précisant explicitement le Dockerfile et le contexte :
docker build -f docker/Dockerfile -t monimage:debug .
Ici, le contexte est . (le dernier argument). Si votre Dockerfile est dans docker/ mais que vous mettez le contexte docker/, alors COPY ne verra pas les fichiers au-dessus.
Exemple correct si vos sources sont à la racine :
docker build -f docker/Dockerfile -t monimage:debug .
Exemple problématique :
docker build -f docker/Dockerfile -t monimage:debug docker/
Dans ce cas, COPY ../src /app/src est impossible : Docker interdit de sortir du contexte.
Piège : .dockerignore trop agressif
Un .dockerignore peut exclure node_modules, dist, mais aussi par erreur package.json.
Vérifiez les patterns. Exemple :
*
!src/
!Dockerfile
Ici, tout est exclu sauf src/ et Dockerfile. Si vous avez besoin de package.json, il faut l’autoriser :
!package.json
!package-lock.json
Erreurs de syntaxe Dockerfile et pièges de parsing
1) Erreurs de continuation de ligne
Exemple :
RUN apt-get update && \
apt-get install -y curl \
git
Ici, git est sur une nouvelle ligne sans \ à la fin de la ligne précédente : selon le shell, ça peut devenir une commande séparée ou provoquer une erreur.
Bon exemple :
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git
2) Confusion entre forme shell et forme exec
- Forme shell :
CMD echo "hello"
- Forme exec (recommandée pour éviter des surprises) :
CMD ["echo", "hello"]
Pour RUN, la forme shell passe par /bin/sh -c. Si vous utilisez des fonctionnalités bash ([[ ... ]], pipefail), ça casse.
Symptôme : sh: 1: [[: not found ou pipefail: not found.
Solution : soit rester POSIX, soit forcer bash :
SHELL ["/bin/bash", "-lc"]
RUN set -euo pipefail; ...
Attention : bash doit exister dans l’image (Debian/Ubuntu oui, Alpine non par défaut).
3) Variables non expansées comme attendu
Dans RUN, l’expansion dépend du shell. Dans COPY, ce n’est pas un shell : les variables d’environnement ne s’expansent pas comme dans bash (sauf cas spécifiques BuildKit). Évitez de compter dessus.
Préférez :
ARG APP_DIR=/app
WORKDIR ${APP_DIR}
COPY . ${APP_DIR}
Échecs liés aux commandes RUN (apt, apk, pip, npm…)
APT (Debian/Ubuntu) : erreurs courantes et corrections
1) apt-get update échoue (réseau, DNS, proxy)
Voir section réseau plus bas.
2) Paquets introuvables / dépôts obsolètes
Symptôme :
E: Unable to locate package xyz
Causes :
- Nom de paquet incorrect
apt-get updateoublié- Distribution incompatible
Bon pattern :
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates curl && \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/*réduit la taille de l’image.--no-install-recommendsévite des dépendances inutiles.
3) debconf: unable to initialize frontend
Sur des images minimalistes, apt peut vouloir un frontend interactif.
Solution :
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y tzdata
APK (Alpine) : dépôts et certificats
Symptôme :
ERROR: unable to select packages:
xyz (no such package)
Ou erreurs TLS si ca-certificates manque.
Pattern :
RUN apk add --no-cache ca-certificates curl
pip (Python) : compilation, wheels, headers manquants
Symptôme :
error: command 'gcc' failedfatal error: Python.h: No such file or directory
Cause : vous installez un package qui compile une extension C sans toolchain.
Exemple de correction (Debian) :
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-dev gcc build-essential && \
rm -rf /var/lib/apt/lists/*
Ou mieux : multi-stage pour ne pas garder les outils de build dans l’image finale.
npm/yarn : lockfile, scripts, permissions
Symptômes fréquents :
npm ERR! code EACCES(permissions)npm ciéchoue car pas depackage-lock.json- build natif (node-gyp) échoue faute de
python3,make,g++
Bon pattern Node (exemple) :
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY . .
RUN npm run build
Séparer COPY package*.json du reste permet un cache efficace.
Problèmes réseau, DNS, proxy, certificats
1) DNS : Temporary failure resolving
Si apt-get update affiche une erreur de résolution, le problème est souvent :
- DNS du host / VPN / entreprise
- configuration Docker Desktop / daemon
- réseau de CI
Tests :
docker run --rm alpine:3.19 nslookup deb.debian.org
docker run --rm alpine:3.19 wget -S -O- https://deb.debian.org/ 2>&1 | head
Si nslookup échoue, ce n’est pas votre Dockerfile, mais l’environnement.
Solutions possibles :
- Configurer le DNS du daemon Docker (ex:
/etc/docker/daemon.json), puis redémarrer Docker. - Sur CI, utiliser un runner avec accès internet correct.
- Éviter des miroirs spécifiques, ou utiliser des dépôts internes.
2) Proxy d’entreprise
Si vous avez un proxy HTTP(S), vous devez le passer au build.
Exemple :
docker build \
--build-arg http_proxy=http://proxy.exemple:3128 \
--build-arg https_proxy=http://proxy.exemple:3128 \
--build-arg no_proxy=localhost,127.0.0.1,.exemple \
-t monimage:debug .
Et dans le Dockerfile (si nécessaire) :
ARG http_proxy
ARG https_proxy
ARG no_proxy
ENV http_proxy=$http_proxy https_proxy=$https_proxy no_proxy=$no_proxy
3) Certificats : x509: certificate signed by unknown authority
Symptôme fréquent derrière un proxy TLS interceptant.
Solutions :
- Installer
ca-certificates(si manquant) - Ajouter le certificat interne (CA) dans l’image
Exemple Debian :
COPY entreprise-ca.crt /usr/local/share/ca-certificates/entreprise-ca.crt
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
update-ca-certificates && \
rm -rf /var/lib/apt/lists/*
Cache : faux positifs, incohérences, invalidation
Le cache peut :
- Masquer un problème (une étape n’est pas rejouée).
- Créer un problème (artefacts obsolètes copiés).
1) Comprendre l’invalidation
Une instruction est rejouée si :
- l’instruction change
- un fichier utilisé par
COPY/ADDchange - un
ARGutilisé change - le cache est désactivé
2) Forcer un “cache bust” propre
Exemple :
ARG CACHE_BUST=1
RUN echo "cache bust: ${CACHE_BUST}" && apt-get update
Build :
docker build --build-arg CACHE_BUST=$(date +%s) -t monimage:debug .
3) Nettoyer les caches locaux (avec prudence)
Voir l’espace disque :
docker system df
Nettoyer :
docker builder prune
docker system prune -f
Attention : system prune supprime des conteneurs arrêtés, réseaux, images non utilisées.
Erreurs multi-stage : COPY --from, chemins, utilisateurs
Le multi-stage réduit la taille et isole les dépendances de build, mais introduit des erreurs typiques.
1) COPY --from=builder échoue : mauvais chemin
Exemple :
FROM golang:1.22 AS builder
WORKDIR /src
RUN go build -o /bin/app ./cmd/app
FROM debian:bookworm-slim
COPY --from=builder /src/app /usr/local/bin/app
Ici, le binaire est /bin/app, pas /src/app.
Correction :
COPY --from=builder /bin/app /usr/local/bin/app
2) Stage name incorrect
Si vous renommez AS builder en AS build, mais gardez --from=builder, vous aurez une erreur “unknown stage”.
3) Fichiers non exécutables / mauvais droits
Après COPY, un binaire peut perdre des droits (rare) ou être copié sans chmod attendu (plus fréquent pour des scripts).
Correction :
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
Permissions, utilisateurs, UID/GID, fichiers non accessibles
Symptôme : Permission denied pendant un RUN
Exemples :
- écrire dans
/rootalors que vous êtesUSER app - écrire dans
/appsans droits
Bon pattern :
RUN useradd -m -u 10001 app
WORKDIR /app
RUN chown -R app:app /app
USER app
Si vous copiez des fichiers après avoir changé d’utilisateur, pensez à --chown :
COPY --chown=app:app . /app
Problème fréquent : npm ci en user non root
npm écrit dans des caches. Il faut configurer un cache accessible :
USER app
ENV npm_config_cache=/home/app/.npm
RUN npm ci
Architecture/plateforme : amd64 vs arm64
Sur Mac Apple Silicon (arm64), vous pouvez builder une image amd64 par erreur (ou l’inverse), ou télécharger des binaires incompatibles.
Diagnostiquer
Voir l’architecture :
docker version --format '{{.Server.Arch}}'
uname -m
Builder pour une plateforme :
docker buildx build --platform linux/amd64 -t monimage:amd64 .
docker buildx build --platform linux/arm64 -t monimage:arm64 .
Symptôme typique
exec format errorau runtime- binaire téléchargé (curl) non compatible
Solution : utiliser des URLs conditionnelles, ou des images de base multi-arch, ou compiler dans le stage builder pour la bonne plateforme.
Diagnostiquer une image produite : inspection et tests
Même si le build passe, l’image peut être incorrecte. Vérifiez :
1) Historique des couches
docker history monimage:debug
Utile pour repérer une couche énorme (ex: cache apt non nettoyé) ou une commande suspecte.
2) Inspecter la config
docker inspect monimage:debug | less
Regardez :
Config.EnvConfig.Cmd,Config.EntrypointConfig.WorkingDirRootFS.Layers
3) Lancer un shell et vérifier les fichiers
docker run --rm -it --entrypoint /bin/sh monimage:debug
Puis :
ls -la
whoami
env | sort | head
4) Vérifier la taille et les fichiers
Taille :
docker images monimage:debug
Lister les fichiers (approche simple) :
docker run --rm monimage:debug sh -lc 'find / -maxdepth 2 -type f 2>/dev/null | head -n 50'
Exemples d’erreurs fréquentes et corrections
Exemple 1 : COPY échoue à cause du contexte
Dockerfile (problématique) :
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Erreur :
COPY failed: file not found in build context: stat requirements.txt: file does not exist
Cause : requirements.txt est dans backend/requirements.txt et vous construisez depuis la racine.
Solutions :
- Construire avec le bon contexte :
docker build -t monapp:debug backend/
- Ou corriger les chemins :
COPY backend/requirements.txt .
Mais attention : cela couple le Dockerfile à un contexte racine. Souvent, le plus propre est de mettre le Dockerfile dans backend/ et builder depuis backend/.
Exemple 2 : RUN échoue car bash requis
Dockerfile :
FROM alpine:3.19
RUN set -euo pipefail; echo ok
Erreur : pipefail: not found.
Correction POSIX :
FROM alpine:3.19
RUN set -eu; echo ok
Ou installer bash et changer le shell :
FROM alpine:3.19
RUN apk add --no-cache bash
SHELL ["/bin/bash", "-lc"]
RUN set -euo pipefail; echo ok
Exemple 3 : apt-get install bloqué (interaction)
Symptôme : build “bloqué” sur tzdata (choix de timezone).
Correction :
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends tzdata && \
rm -rf /var/lib/apt/lists/*
Exemple 4 : dépendances de build laissées dans l’image finale
Problème : image énorme, contient gcc, make, etc.
Solution multi-stage (Python + wheels) :
FROM python:3.12-slim AS builder
WORKDIR /w
RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
CMD ["python", "app.py"]
Techniques avancées BuildKit utiles en debug
1) Monter un cache pour accélérer et stabiliser (et diagnostiquer)
Exemple avec cache pip :
# syntax=docker/dockerfile:1.6
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
Si un build échoue “aléatoirement” à cause de téléchargements, ce cache peut réduire la variabilité (mais attention à la reproductibilité : pinnez vos versions).
2) Monter un secret (éviter de copier des tokens)
Pour diagnostiquer des accès privés (registry, git privé) sans mettre le secret dans l’image :
# syntax=docker/dockerfile:1.6
RUN --mount=type=secret,id=git_token \
sh -lc 'cat /run/secrets/git_token | wc -c'
Build :
docker build \
--secret id=git_token,src=./git_token.txt \
--progress=plain -t monimage:debug .
En debug, c’est très utile pour confirmer que le secret est bien accessible uniquement pendant le build.
Checklist de dépannage rapide
Quand un build échoue, suivez cet ordre :
-
Relancer avec logs clairs
DOCKER_BUILDKIT=1 docker build --progress=plain -t monimage:debug . -
Identifier l’instruction fautive (ligne Dockerfile + étape
[x/y]). -
Reproduire sans cache
docker build --no-cache --progress=plain -t monimage:nocache . -
Vérifier le contexte et
.dockerignore- le fichier est-il présent ?
- est-il ignoré ?
-
Tester réseau/DNS dans un conteneur simple
docker run --rm alpine:3.19 nslookup github.com docker run --rm alpine:3.19 wget -qO- https://example.com | head -
Isoler le stage (multi-stage)
docker build --target builder --progress=plain -t monimage:builder . docker run --rm -it --entrypoint /bin/sh monimage:builder -
Rendre la commande
RUNplus verbeuse (pip-v, npm--verbose, etc.). -
Vérifier la plateforme si vous téléchargez des binaires
docker buildx build --platform linux/amd64 ...
Conclusion
Déboguer un build Docker en échec consiste à réduire l’incertitude : obtenir des logs lisibles (--progress=plain), isoler l’étape fautive (--target), vérifier le contexte (.dockerignore), reproduire sans cache (--no-cache), et, si nécessaire, entrer dans un conteneur issu d’un stage intermédiaire pour rejouer la commande en direct.
Si vous souhaitez, vous pouvez coller :
- votre Dockerfile,
- la commande exacte de build,
- et le log complet (avec
--progress=plain),
et je vous proposerai un diagnostic ciblé et une version corrigée du Dockerfile.