← Retour aux tutoriels

Déboguer des builds Docker en échec : lire et corriger les erreurs de Dockerfile

dockerdockerfiledebugci/cddevopsbuildlogscontainers

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

  1. Comprendre ce que fait réellement docker build
  2. Activer des logs utiles : BuildKit, progression, détails
  3. Lire un échec : identifier l’étape, le contexte, la commande
  4. Débogage interactif : entrer dans une étape qui échoue
  5. Problèmes de contexte et .dockerignore
  6. Erreurs de syntaxe Dockerfile et pièges de parsing
  7. Échecs liés aux commandes RUN (apt, apk, pip, npm…)
  8. Problèmes réseau, DNS, proxy, certificats
  9. Cache : faux positifs, incohérences, invalidation
  10. Erreurs multi-stage : COPY --from, chemins, utilisateurs
  11. Permissions, utilisateurs, UID/GID, fichiers non accessibles
  12. Architecture/plateforme : amd64 vs arm64
  13. Diagnostiquer une image produite : inspection et tests
  14. 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 :

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 .

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 :


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 :

  1. Repérer l’instruction fautive (ici RUN apt-get update...).
  2. Lire la sortie de la commande (DNS, 404, permission, etc.).
  3. Noter le code de sortie (100 pour apt, 1 générique, 127 commande introuvable…).
  4. 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

  1. 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 .
  1. 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 :

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

CMD echo "hello"
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 :

Bon pattern :

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      ca-certificates curl && \
    rm -rf /var/lib/apt/lists/*

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 :

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 :

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 :

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 :

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 :

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 :

1) Comprendre l’invalidation

Une instruction est rejouée si :

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 :

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

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 :

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 :

docker build -t monapp:debug backend/
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 :

  1. Relancer avec logs clairs

    DOCKER_BUILDKIT=1 docker build --progress=plain -t monimage:debug .
  2. Identifier l’instruction fautive (ligne Dockerfile + étape [x/y]).

  3. Reproduire sans cache

    docker build --no-cache --progress=plain -t monimage:nocache .
  4. Vérifier le contexte et .dockerignore

    • le fichier est-il présent ?
    • est-il ignoré ?
  5. 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
  6. Isoler le stage (multi-stage)

    docker build --target builder --progress=plain -t monimage:builder .
    docker run --rm -it --entrypoint /bin/sh monimage:builder
  7. Rendre la commande RUN plus verbeuse (pip -v, npm --verbose, etc.).

  8. 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 :

et je vous proposerai un diagnostic ciblé et une version corrigée du Dockerfile.