Falende Docker-builds debuggen: Dockerfile-fouten lezen en oplossen
Docker-builds falen zelden “zomaar”. Bijna altijd vertelt de foutmelding je precies wat er misgaat—alleen staat het verstopt tussen lagen, caching, multi-stage builds en soms cryptische output van package managers. In deze tutorial leer je systematisch Dockerfile-fouten lezen, reproduceren, isoleren en oplossen. We gaan diep in op de meest voorkomende oorzaken (syntax, context, permissies, netwerken, package managers, multi-stage, BuildKit, caching) en je krijgt concrete commando’s om elke stap te debuggen.
Alles in deze gids werkt met de moderne Docker build-engine (BuildKit). Waar relevant laat ik ook zien hoe je gedrag kunt vergelijken met “legacy” builds.
Inhoud
- Basis: hoe Docker een build uitvoert
- Eerste hulp: de build opnieuw draaien met maximale info
- Foutmeldingen leren lezen: laag, instructie, exit code
- Veelvoorkomende Dockerfile-fouten (met fixes)
- BuildKit superpowers: progress, secrets, mounts, targets
- Debuggen door een “shell in de build” te krijgen
- Caching en “het werkt op mijn machine”
- Checklist: van fout naar fix
- Praktische case studies
Basis: hoe Docker een build uitvoert
Een Docker-build is een reeks lagen (layers). Elke Dockerfile-instructie (FROM, RUN, COPY, etc.) creëert een stap. Docker:
- Leest de Dockerfile.
- Stuurt de build context (meestal de huidige map) naar de Docker daemon.
- Voert instructies uit in volgorde.
- Cachet resultaten per stap (tenzij je dat uitschakelt).
Belangrijke gevolgen voor debugging:
- Een foutmelding verwijst vrijwel altijd naar een specifieke Dockerfile-regel/stap.
COPYenADDkunnen alleen bestanden zien die in de build context zitten (en niet genegeerd zijn door.dockerignore).RUNdraait in een shell (standaard/bin/sh -c), met alle typische shell-valkuilen.- Caching kan fouten “verbergen” of juist oude fouten blijven herhalen als je niet begrijpt wat er gecachet wordt.
Eerste hulp: de build opnieuw draaien met maximale info
Begin altijd met een reproduceerbare build. Gebruik expliciete flags:
docker build --no-cache --progress=plain -t mijnapp:debug .
--no-cache: dwingt alle stappen opnieuw.--progress=plain: geeft volledige logregels (handig voor CI en voor het zien van exacte commando-output).
Als je BuildKit expliciet wilt aanzetten/uitzetten:
# BuildKit aan (meestal default)
DOCKER_BUILDKIT=1 docker build --progress=plain -t mijnapp:debug .
# Legacy builder (soms nuttig om gedrag te vergelijken)
DOCKER_BUILDKIT=0 docker build -t mijnapp:debug .
Bekijk ook welke Dockerfile gebruikt wordt:
docker build -f Dockerfile.dev --progress=plain -t mijnapp:debug .
En bouw tot een specifieke stage (bij multi-stage):
docker build --target builder --progress=plain -t mijnapp:builder .
Foutmeldingen leren lezen: laag, instructie, exit code
Docker toont meestal iets als:
- Stapnummer (bijv.
#7) - Dockerfile-regel (bijv.
Dockerfile:12) - Commando dat faalde
- Exit code (bijv.
exit code: 127)
Voorbeeld:
#7 0.231 /bin/sh: 1: yarn: not found
------
Dockerfile:12
--------------------
10 | COPY package.json yarn.lock ./
11 | RUN corepack enable
12 | >>> RUN yarn install --frozen-lockfile
13 |
--------------------
ERROR: failed to solve: process "/bin/sh -c yarn install --frozen-lockfile" did not complete successfully: exit code: 127
Interpretatie:
- Het faalt in stap 12:
RUN yarn install ... - De shell zegt:
yarn: not found - Exit code
127betekent meestal: command not found
Je debugt dus niet “Docker”, maar het commando in die laag.
Snelle mapping van exit codes
1: generieke fout (script/commando faalt)2: vaak misuse van shell builtins/argumenten (afhankelijk van tool)100: vaakapt-get(package manager) probleem127: command not found137: OOM kill (out of memory), proces werd afgeschoten139: segmentation fault
Veelvoorkomende Dockerfile-fouten met fixes
Syntax- en parsefouten
Symptoom: build faalt meteen met “parse error” of “unknown instruction”.
Voorbeeld:
unknown instruction: RNU
Oorzaak: typefout (RNU i.p.v. RUN).
Andere klassiekers:
- Verkeerde hoofdletters: instructies zijn meestal uppercase, maar Docker is tolerant; toch kan rommel verwarren.
- Verkeerde quotes of backslashes bij multi-line
RUN. - JSON-array syntax voor
CMD/ENTRYPOINTverkeerd.
Correcte voorbeelden:
RUN apt-get update && apt-get install -y curl
Multi-line met backslashes:
RUN apt-get update && apt-get install -y \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
JSON-vorm (exec form) voor CMD:
CMD ["node", "server.js"]
Debugtip: laat Docker precies tonen waar de parse faalt:
docker build --progress=plain --no-cache .
COPY/ADD en build context problemen
Symptoom: COPY failed: file not found in build context or excluded by .dockerignore
Voorbeeld:
failed to compute cache key: failed to calculate checksum of ref ... "/app/package.json": not found
Oorzaken:
- Je build context is verkeerd (je bouwt vanuit de verkeerde map).
.dockerignoresluit het bestand uit.- Je pad in
COPYklopt niet.
Controleer je build context:
Als je docker build . doet, is de context de huidige map. Controleer:
pwd
ls -la
Bekijk .dockerignore:
cat .dockerignore
Als je per ongeluk package*.json negeert, kan COPY package.json ... falen.
Correct gebruik van COPY:
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Let op: COPY . . kopieert alles uit de context (behalve genegeerde files). Grote contexten maken builds traag en onvoorspelbaar.
Pro-tip: inspecteer wat er echt in de context zit door tijdelijk dit te doen:
RUN ls -la
Of specifieker:
RUN find . -maxdepth 2 -type f | sed -n '1,200p'
(Alleen zinvol nadat je de relevante COPY hebt gedaan.)
RUN faalt: exit code 1/127/100
Exit code 127: commando bestaat niet
Voorbeeld:
/bin/sh: 1: python: not found
Oplossingen:
- Installeer het pakket (
python3,py3-pip, etc.) - Gebruik de juiste binary naam (
python3i.p.v.python) - Kies een base image die het al heeft
Voor Debian/Ubuntu:
RUN apt-get update && apt-get install -y python3 python3-pip
Voor Alpine:
RUN apk add --no-cache python3 py3-pip
Exit code 100: apt-get problemen
Voorbeeld:
E: Unable to locate package ...
Oorzaken:
apt-get updateniet gedaan (of cache is verwijderd)- Verkeerde repo’s
- Package naam klopt niet
- Netwerk/DNS issues
Correct patroon:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Belangrijk: combineer apt-get update en apt-get install in één RUN zodat de package index niet uit cache raakt.
Exit code 1: generiek
Dan moet je de output van het commando zelf lezen. Maak het commando explicieter:
RUN set -eux; \
node --version; \
npm --version; \
npm ci
-e: stop bij fout-u: undefined vars zijn fouten-x: echo commando’s (handig!)
Permissies en gebruikers (root vs non-root)
Symptoom: permission denied bij RUN, COPY, of tijdens runtime.
Veel images draaien als root tijdens build, maar je kunt USER veranderen. Als je USER node zet en daarna apt-get install probeert, faalt dat.
Voorbeeld fout:
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
Fix: doe OS-installaties als root, switch daarna:
FROM node:20-bookworm
WORKDIR /app
# root (default) voor apt
RUN apt-get update && apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
# pas daarna non-root
USER node
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci
COPY --chown=node:node . .
CMD ["node", "server.js"]
Debugtip: print de huidige user en permissies:
RUN id && whoami && ls -la
Netwerk/DNS/Proxy issues
Symptoom: timeouts bij apt, npm, pip, curl, git clone.
Voorbeelden:
Temporary failure resolving 'deb.debian.org'Could not resolve hostConnection timed out
Check of het een algemene netwerkfout is:
Voeg tijdelijk toe:
RUN set -eux; \
cat /etc/resolv.conf; \
ping -c 1 8.8.8.8 || true; \
ping -c 1 deb.debian.org || true; \
curl -I https://deb.debian.org || true
pingis niet altijd aanwezig; je kuntiputils-pinginstalleren of alleencurlgebruiken.
Proxy instellen tijdens build:
docker build \
--build-arg http_proxy=http://proxy:3128 \
--build-arg https_proxy=http://proxy:3128 \
--build-arg no_proxy=localhost,127.0.0.1 \
--progress=plain \
-t mijnapp:debug .
En in Dockerfile:
ARG http_proxy
ARG https_proxy
ARG no_proxy
Let op: zet secrets (tokens) niet als build-arg als je image publiek kan worden; gebruik BuildKit secrets (zie verderop).
Multi-stage build fouten
Multi-stage builds zijn geweldig, maar fouten worden soms verwarrend door COPY --from=....
Voorbeeld:
FROM node:20 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
Symptoom: COPY failed: stat /app/dist: file does not exist
Oorzaken:
- Build output heet anders (
buildi.p.v.dist) npm run buildfaalde maar je zag het niet (cache/target)- Je
WORKDIRof pad klopt niet
Debug aanpak:
- Bouw alleen de builder stage:
docker build --target builder --progress=plain -t mijnapp:builder .
- Start een container om te inspecteren:
docker run --rm -it mijnapp:builder sh
# of bash, afhankelijk van image
ls -la /app
ls -la /app/dist
- Voeg tijdelijke checks toe:
RUN set -eux; npm run build; ls -la /app; ls -la /app/dist
Platform/architectuur issues (amd64 vs arm64)
Symptoom: “exec format error” of binaries die niet draaien.
Voorbeeld:
standard_init_linux.go:228: exec user process caused: exec format error
Oorzaken:
- Je bouwt op Apple Silicon (arm64) maar verwacht amd64.
- Je downloadt een prebuilt binary voor de verkeerde architectuur.
Check je platform:
docker version
docker info | grep -i architecture
Forceer platform bij build:
docker build --platform=linux/amd64 -t mijnapp:amd64 .
En bij run:
docker run --rm --platform=linux/amd64 mijnapp:amd64
Debugtip: in de container:
uname -m
Line endings en shell-gedrag
Symptoom: scripts werken lokaal, falen in container met ^M of “not found”.
Voorbeeld:
/bin/sh^M: bad interpreter: No such file or directory
Oorzaak: Windows CRLF line endings.
Fix:
- Zet Git op LF voor scripts
- Converteer in build (tijdelijk of structureel)
Converteer met dos2unix:
RUN apt-get update && apt-get install -y dos2unix
COPY entrypoint.sh /entrypoint.sh
RUN dos2unix /entrypoint.sh && chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Shell nuance: RUN gebruikt standaard /bin/sh -c. Bash-specifieke syntax faalt dan.
Als je bash nodig hebt:
RUN apt-get update && apt-get install -y bash
SHELL ["/bin/bash", "-lc"]
RUN [[ -f "somefile" ]] && echo "bestaat"
BuildKit superpowers: progress, secrets, mounts, targets
Volledige logs
Gebruik:
docker build --progress=plain -t mijnapp:debug .
Targeted builds
docker build --target test --progress=plain -t mijnapp:test .
Secrets (tokens) veilig gebruiken
Stel je moet een private npm registry token gebruiken. Niet doen:
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
Dit kan in image history belanden.
Met BuildKit secrets:
docker build \
--secret id=npm_token,src=$HOME/.npm-token \
--progress=plain \
-t mijnapp:debug .
Dockerfile:
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npm_token \
set -eu; \
TOKEN="$(cat /run/secrets/npm_token)"; \
npm config set //registry.npmjs.org/:_authToken="$TOKEN"; \
npm ci
Cache mounts voor package managers
Dit versnelt builds en maakt debug herhaalbaar:
# syntax=docker/dockerfile:1.7
RUN --mount=type=cache,target=/root/.npm \
npm ci
Debuggen door een “shell in de build” te krijgen
Je kunt niet letterlijk “in” een mislukte laag springen, maar je kunt dichtbij komen met deze technieken.
1) Bouw tot een stage en run een shell
Als je multi-stage gebruikt, bouw de stage die faalt:
docker build --target builder --progress=plain -t mijnapp:builder .
docker run --rm -it mijnapp:builder sh
Van daaruit kun je commando’s handmatig uitvoeren:
cd /app
npm run build
ls -la
2) Maak een tijdelijke debug stage
Voeg onderaan je Dockerfile toe:
FROM builder AS debug
CMD ["sh"]
Bouw en start:
docker build --target debug -t mijnapp:debugshell .
docker run --rm -it mijnapp:debugshell
3) Splits een complexe RUN op (tijdelijk)
In plaats van:
RUN do_a && do_b && do_c
Tijdelijk:
RUN do_a
RUN do_b
RUN do_c
Zo zie je exact welke stap faalt. Daarna kun je weer combineren voor efficiënte layers.
4) Zet set -eux aan
Heel effectief bij shell scripts:
RUN set -eux; \
echo "Stap 1"; \
do_a; \
echo "Stap 2"; \
do_b
Caching en “het werkt op mijn machine”
Caching kan een build sneller maken, maar ook verwarrend:
- Je past een bestand aan dat niet in de cache key zit → stap blijft gecachet.
- Je verwacht dat
apt-get updateopnieuw draait, maar de heleRUNis gecachet. - Een
.dockerignorewijziging kan cache beïnvloeden.
Cache bewust uitschakelen
docker build --no-cache --progress=plain -t mijnapp:debug .
Cache bewust “busten” (zonder alles uit te zetten)
Gebruik een build-arg:
ARG CACHEBUST=1
RUN echo "cachebust=$CACHEBUST" && apt-get update
Build:
docker build --build-arg CACHEBUST=$(date +%s) -t mijnapp:debug .
Let op volgorde van COPY voor betere cache
Goed patroon (Node.js voorbeeld):
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Als je eerst COPY . . doet, dan breekt elke codewijziging de cache van npm ci.
Checklist: van fout naar fix
- Herhaalbaar maken
docker build --no-cache --progress=plain ...
- Vind de exacte Dockerfile-regel
- Lees de
>>>regel in de output.
- Lees de
- Classificeer het type fout
- Parse? COPY? RUN? Netwerk? Permissie?
- Reproduceer in isolatie
--targetstage bouwen- Tijdelijk
RUN ls -la,id,cat /etc/os-release
- Maak commando’s expliciet
set -eux- Print versies:
node -v,python3 --version,gcc --version
- Controleer context en .dockerignore
- Controleer platform
--platform=...,uname -m
- Verwijder tijdelijke debug code
- Combineer RUN’s weer waar zinvol
- Verwijder tokens/logging
Praktische case studies
Case 1: COPY failed: file not found door verkeerde context
Situatie: je projectstructuur:
repo/
docker/
Dockerfile
app/
package.json
src/
Je draait:
docker build -f docker/Dockerfile -t mijnapp .
Maar je Dockerfile doet:
COPY package.json ./
Dat faalt, want package.json zit in app/.
Fix 1 (paden aanpassen):
COPY app/package.json ./
Fix 2 (context veranderen):
docker build -f ../docker/Dockerfile -t mijnapp ../app
Dan is de context app/ en werkt COPY package.json ./.
Case 2: apt-get install faalt sporadisch in CI
Symptoom: timeouts of “Hash Sum mismatch”.
Aanpak:
- Log DNS en repo:
RUN set -eux; cat /etc/resolv.conf; cat /etc/apt/sources.list
- Gebruik retries en noninteractive:
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates curl; \
rm -rf /var/lib/apt/lists/*
- Als CI een proxy nodig heeft: build-args voor proxy meegeven (zie eerder).
Case 3: yarn: not found in Node 20 images
In recente Node images is Yarn niet altijd standaard beschikbaar zoals je verwacht. Oplossing: Corepack gebruiken of Yarn installeren.
Fix met Corepack:
FROM node:20-bookworm
WORKDIR /app
COPY package.json yarn.lock ./
RUN corepack enable
RUN yarn --version
RUN yarn install --frozen-lockfile
Als corepack enable faalt, controleer of het in die Node variant aanwezig is en print:
RUN node --version && corepack --version || true
Case 4: “works locally” maar faalt in container door bash-syntax
Je hebt:
RUN if [[ -f .env ]]; then echo ok; fi
[[ ... ]] is bash, maar /bin/sh (dash) snapt het niet.
Fix:
RUN if [ -f .env ]; then echo ok; fi
Of zet bash als shell:
RUN apt-get update && apt-get install -y bash
SHELL ["/bin/bash", "-lc"]
RUN if [[ -f .env ]]; then echo ok; fi
Extra debugcommando’s die bijna altijd helpen
Voeg tijdelijk toe rond de falende stap:
RUN set -eux; \
cat /etc/os-release; \
uname -a; \
env | sort | sed -n '1,120p'
Bestanden inspecteren:
RUN set -eux; \
pwd; \
ls -la; \
find . -maxdepth 2 -type f | sed -n '1,200p'
Disk space check (bij grote builds):
RUN df -h
Afronding
Falende Docker-builds debuggen is vooral: lees de foutmelding als een aanwijzing naar één specifieke laag, maak de build reproduceerbaar met --no-cache en --progress=plain, en reduceer complexiteit door te isoleren (targets/stages) en commando’s expliciet te maken (set -eux). Zodra je dit proces beheerst, worden zelfs “cryptische” errors meestal binnen minuten verklaarbaar—en oplosbaar.
Als je een concrete foutmelding + Dockerfile snippet plakt (inclusief de stap met >>>), kan ik de diagnose stap voor stap met je doorlopen en een minimale fix voorstellen.