Waarom je Docker-container steeds opnieuw start (en hoe je het oplost)
Een Docker-container die “steeds opnieuw start” is bijna altijd een symptoom, geen oorzaak. Docker doet precies wat je gevraagd hebt: een proces starten, en als dat proces stopt (crasht, klaar is, of direct exit), dan eindigt de container. Als je daarbovenop een restart policy hebt gezet (bijv. restart: always), dan gaat Docker hem automatisch opnieuw starten—en lijkt het alsof de container “in een loop” zit.
In deze tutorial leer je:
- Hoe Docker bepaalt of een container “levend” is
- De meest voorkomende oorzaken van restart loops
- Hoe je met concrete commando’s de echte fout vindt
- Hoe je het structureel oplost (entrypoints, signals, healthchecks, afhankelijkheden, permissies, resources)
- Hoe je in Docker Compose en in Kubernetes soortgelijke problemen herkent
Alle voorbeelden zijn echte commando’s die je op Linux/macOS kunt draaien. Op Windows (PowerShell) zijn de commando’s vergelijkbaar, maar de shell-syntax kan afwijken.
1. Wat betekent “container restart” technisch?
1.1 Een container leeft zolang PID 1 leeft
In Docker is een container in essentie een geïsoleerde omgeving waarin één “hoofdproces” draait. Dat hoofdproces is PID 1 binnen de container. Zodra PID 1 stopt (exit code 0 of niet-0), stopt de container.
- Exit code
0betekent: “normaal beëindigd” - Exit code
1of anders betekent: “fout” (of in elk geval “niet succesvol”)
Als je een restart policy hebt ingesteld, zal Docker bij het stoppen van de container proberen hem opnieuw te starten.
1.2 Restart policies: wanneer start Docker automatisch opnieuw?
Je kunt de restart policy zien via:
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' <container>
docker inspect -f '{{json .HostConfig.RestartPolicy}}' <container
Veelvoorkomende policies:
no(default): niet automatisch herstartenon-failure[:max-retries]: herstart alleen als exit code != 0always: altijd herstarten, ook bij exit code 0unless-stopped: altijd herstarten, tenzij je hem expliciet stopt
In Docker Compose zie je dit vaak als:
restart: always
Belangrijk:
restart: alwayskan een container herstarten die “gewoon klaar” is (exit 0). Dat voelt als een bug, maar is precies wat je gevraagd hebt.
2. Snel diagnosepad: in 2 minuten van symptoom naar oorzaak
Als je container in een restart loop zit, doe dit:
2.1 Bekijk status en exit code
docker ps -a --filter "name=<naam>" --no-trunc
Je ziet iets als:
Restarting (1) 10 seconds agoExited (137) ...Exited (0) ...
Exit codes die vaak terugkomen:
1: generieke fout126: command found but not executable127: command not found137: killed (vaak OOM kill)139: segfault143: SIGTERM (normale stop door Docker/Orchestrator)
2.2 Lees logs (huidig en vorige poging)
docker logs <container>
docker logs --tail 200 <container>
docker logs -f <container>
Als hij zó snel crasht dat je nauwelijks logs ziet:
docker logs --details <container>
En cruciaal: logs van de vorige crash (als hij al opnieuw gestart is):
docker logs --previous <container>
2.3 Inspecteer de exacte startcommando’s
docker inspect <container> | less
Of gericht:
docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' <container>
2.4 Probeer interactief te starten zonder restart policy
Stop en verwijder de container (let op volumes/data):
docker rm -f <container>
Start hem opnieuw zonder restart:
docker run --rm -it <image> sh
Of als sh ontbreekt (bijv. distroless), gebruik een debug-image of override entrypoint (zie later).
3. De meest voorkomende oorzaken (en oplossingen)
Oorzaak A: Je proces eindigt meteen (container “doet niets”)
Symptomen
- Exit code
0 - Logs zijn leeg of tonen “done” en dan stopt hij
- Met
restart: alwayslijkt hij te “flappen”
Waarom dit gebeurt
Veel images zijn bedoeld om een langlopend proces te draaien (webserver, worker, daemon). Als jouw CMD een script is dat direct klaar is, stopt PID 1 en dus de container.
Oplossingen
- Zorg dat je een langlopend proces draait (bijv.
nginx -g 'daemon off;'). - Gebruik geen “dummy” zoals
tail -f /dev/nullals structurele oplossing—alleen voor debugging.
Voorbeeld (Dockerfile):
CMD ["python", "app.py"]
Als app.py direct exit, stopt de container. Maak er een server van of een loop/worker die blijft draaien.
Oorzaak B: Verkeerde CMD/ENTRYPOINT (command not found / not executable)
Symptomen
- Exit code
127(command not found) - Exit code
126(not executable) - Logs:
exec: "…": executable file not found in $PATH
Diagnose
docker logs --previous <container>
docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' <container>
Oplossingen
- Controleer pad en bestandsrechten
- Zorg dat je script een shebang heeft en executable is
- Zet het juiste commando in JSON-array vorm (exec form), niet in shell form als je geen shell hebt
Fout voorbeeld (shell form, kan onverwacht gedrag geven):
CMD myapp --port 8080
Beter (exec form):
CMD ["myapp", "--port", "8080"]
Script executable maken:
chmod +x entrypoint.sh
Dockerfile:
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Oorzaak C: Crash door misconfiguratie (env vars, config files, secrets)
Symptomen
- Exit code
1 - Logs tonen config error (bijv. “missing DATABASE_URL”)
Diagnose
Bekijk environment en mounts:
docker inspect -f '{{json .Config.Env}}' <container> | jq
docker inspect -f '{{json .Mounts}}' <container> | jq
Check of config-bestanden bestaan in de container:
docker exec -it <container> sh
ls -la /etc/myapp
cat /etc/myapp/config.yml
Maar als hij te snel crasht, start een shell met dezelfde image:
docker run --rm -it --entrypoint sh <image>
Oplossingen
- Voeg ontbrekende env vars toe
- Mount de juiste config
- Gebruik defaults en duidelijke error messages in je app
- Valideer config bij startup en log expliciet wat ontbreekt
Voorbeeld (docker run):
docker run --rm -e DATABASE_URL="postgres://user:pass@db:5432/app" <image>
Voorbeeld (Compose):
services:
app:
image: myapp:latest
environment:
DATABASE_URL: postgres://user:pass@db:5432/app
Oorzaak D: Afhankelijkheid is nog niet klaar (database, broker, API)
Symptomen
- App start, probeert DB te bereiken, faalt, exit 1
- In logs:
connection refused,timeout,could not translate host name
Belangrijk misverstand: depends_on is geen “ready” check
In Docker Compose zorgt depends_on er alleen voor dat containers gestart worden in volgorde, niet dat een service klaar is.
Oplossingen
- Maak je app robuust: retry met backoff.
- Gebruik healthchecks en wacht op “healthy”.
- Gebruik een wait-script (met mate) zoals
wait-for-it,dockerize, of eigen script.
Compose met healthcheck (Postgres):
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 20
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
Let op:
condition: service_healthywerkt in de klassieke docker-compose implementatie; in de nieuwe Compose-spec is gedrag afhankelijk van tooling/versie. Test dit altijd.
In je app (conceptueel): probeer DB-verbinding meerdere keren voordat je exit.
Oorzaak E: PID 1 en signal handling (slechte entrypoint, zombie processes)
Symptomen
- Container stopt “raar”, of herstart in orchestrators
- SIGTERM wordt niet netjes afgehandeld
- Subprocessen blijven hangen; app reageert niet; healthcheck faalt
Waarom PID 1 speciaal is
PID 1 in Linux heeft afwijkend signaalgedrag en moet child processes reapen. Als je een shellscript als PID 1 gebruikt zonder exec, kan je app signalen missen.
Oplossing: gebruik exec in entrypoint scripts
Slecht (shell blijft PID 1):
#!/bin/sh
myapp --port 8080
Goed (myapp wordt PID 1):
#!/bin/sh
set -e
exec myapp --port 8080
Overweeg ook een init zoals tini:
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["myapp"]
Oorzaak F: OOM kill (te weinig geheugen) → exit code 137
Symptomen
- Exit code
137 - In
dmesgof Docker events zie je OOM - Container herstart steeds onder load
Diagnose
Bekijk exit code:
docker ps -a --no-trunc | grep <naam>
Bekijk events:
docker events --since 1h | grep -i oom
Bekijk kernel logs (Linux):
dmesg -T | grep -i -E "oom|killed process"
Bekijk resource usage:
docker stats <container>
Oplossingen
- Geef meer geheugen (Compose
mem_limit, of Docker run--memory) - Fix memory leak / verlaag concurrency
- Zet limieten op JVM/Node/Python workers
Voorbeeld (docker run):
docker run --memory=512m --cpus=1.0 myapp:latest
Voorbeeld (Compose, niet overal identiek ondersteund):
services:
app:
image: myapp:latest
mem_limit: 512m
Voor Swarm/Compose v3-achtige deployments gebruik je deploy.resources.limits, maar dat werkt alleen in bepaalde orchestrators.
Oorzaak G: Healthcheck faalt → orchestrator herstart (of Compose “unhealthy”)
Symptomen
- Container blijft draaien, maar wordt toch herstart in Kubernetes
- In Compose: status
unhealthy - Je ziet herstarts zonder duidelijke app-crash
Diagnose
Check health status:
docker inspect -f '{{json .State.Health}}' <container> | jq
Bekijk healthcheck logs:
docker inspect -f '{{range .State.Health.Log}}{{println .Output}}{{end}}' <container>
Oplossingen
- Maak healthcheck realistisch (timeout/retries)
- Zorg dat de app pas “ready” is als dependencies beschikbaar zijn
- Scheid “liveness” en “readiness” (vooral in Kubernetes)
Voorbeeld healthcheck:
HEALTHCHECK --interval=10s --timeout=3s --retries=5 \
CMD wget -qO- http://127.0.0.1:8080/health || exit 1
Oorzaak H: Bestandssystemen en permissies (read-only, verkeerde UID/GID)
Symptomen
- Logs: “permission denied”, “cannot write”, “read-only file system”
- App crasht direct bij het schrijven van pid/log/tmp
Diagnose
Bekijk user:
docker inspect -f '{{.Config.User}}' <container>
Ga in een shell en test:
docker run --rm -it --entrypoint sh <image>
id
touch /tmp/test
Bekijk volume mount opties:
docker inspect -f '{{json .Mounts}}' <container> | jq
Oplossingen
- Zorg dat directories bestaan en schrijfbaar zijn voor de runtime user
- Gebruik
RUN chownin Dockerfile - Pas volume-permissies aan op host
Dockerfile voorbeeld:
RUN adduser -D -u 10001 appuser
WORKDIR /app
COPY . /app
RUN chown -R 10001:10001 /app
USER 10001
CMD ["./myapp"]
4. Praktische troubleshooting workflow (stap voor stap)
Hier is een workflow die vrijwel altijd werkt.
Stap 1: Check restart policy en status
docker ps -a --no-trunc | sed -n '1p;/<naam>/p'
docker inspect -f 'RestartPolicy={{.HostConfig.RestartPolicy.Name}}' <container>
Als je always ziet, onthoud: ook exit 0 kan herstarten.
Stap 2: Bekijk logs van de vorige run
docker logs --previous --tail 200 <container>
Als --previous niets geeft, is hij mogelijk nog nooit succesvol gestart of logging driver is anders ingesteld.
Stap 3: Bekijk exit code en foutreden
docker inspect -f 'Status={{.State.Status}} ExitCode={{.State.ExitCode}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' <container>
Stap 4: Reproduceer zonder restart policy
Start dezelfde image handmatig:
docker run --rm -it --name debug-myapp <image>
Of override command:
docker run --rm -it <image> myapp --help
Of override entrypoint:
docker run --rm -it --entrypoint sh <image>
Stap 5: Controleer netwerk/DNS (als dependencies betrokken zijn)
Test DNS:
docker exec -it <container> sh -lc 'getent hosts db || nslookup db || ping -c1 db'
Test TCP connect:
docker exec -it <container> sh -lc 'nc -vz db 5432'
nczit niet in elke image. In Alpine:apk add --no-cache netcat-openbsd. In Debian/Ubuntu:apt-get update && apt-get install -y netcat-openbsd.
Stap 6: Check resources
docker stats --no-stream <container>
En events:
docker events --since 30m
5. Veelvoorkomende scenario’s met concrete fixes
Scenario 1: Node.js container restart door “missing script: start”
Logs:
npm ERR! missing script: start
Fix:
- Voeg
"start": "node server.js"toe aanpackage.json - Of zet CMD correct:
CMD ["node", "server.js"]
Scenario 2: Python container stopt direct omdat je script klaar is
Als je een batch job wil draaien: zet restart policy op on-failure of no.
Compose:
restart: "no"
Of:
restart: on-failure:3
Let op: Compose syntax kan verschillen per versie; vaak is het:
restart: on-failure
En max-retries is niet overal beschikbaar.
Scenario 3: Nginx container restart door daemon mode
Nginx wil standaard daemonizen. In Docker moet het in foreground blijven.
Correct:
nginx -g 'daemon off;'
Veel officiële images doen dit al; als je eigen config/entrypoint overschrijft, kan het misgaan.
Scenario 4: Java OOM na enkele minuten
Zet JVM heap expliciet:
java -Xms128m -Xmx256m -jar app.jar
Of voor containers (moderne JVM’s herkennen cgroups meestal, maar niet altijd zoals je verwacht). Monitor met:
docker stats
6. Debuggen als je image “distroless” is (geen shell)
Distroless images hebben vaak geen sh, ls, cat. Dan kun je niet zomaar docker exec -it ... sh doen.
Optie A: Gebruik --entrypoint werkt niet zonder shell
Als er geen shell is, faalt dit:
docker run --rm -it --entrypoint sh <image>
Optie B: Gebruik een debug sidecar/container met dezelfde filesystem via volume (lastiger)
Praktischer: bouw een debug-variant of gebruik docker build --target met een debug stage.
Optie C: Gebruik docker run met een debug image en network namespace delen
Als je app container draait (of kort draait), kun je een toolbox container in hetzelfde netwerk zetten:
docker run --rm -it --network container:<container> nicolaka/netshoot
Dan kun je DNS/HTTP/TCP testen vanuit dezelfde network namespace.
7. Structurele best practices om restart loops te voorkomen
7.1 Maak startup robuust (retry/backoff)
Een container die bij de eerste mislukte connect meteen exit doet, is fragiel. Beter:
- Retry 10–60 seconden met exponential backoff
- Log duidelijk “waiting for DB…”
7.2 Gebruik duidelijke exit codes en logging
Zorg dat je app bij config errors expliciet logt:
- Welke env var ontbreekt
- Welke file niet gevonden is
- Welke host/poort niet bereikbaar is
7.3 Gebruik healthchecks verstandig
- Healthcheck moet snel en betrouwbaar zijn
- Te strenge timeouts geven false negatives
- In Kubernetes: gebruik readiness voor verkeer, liveness voor herstarten
7.4 Zorg dat PID 1 correct is
- Gebruik
execin shell entrypoints - Overweeg
tiniof een init - Vermijd complexe shell pipelines als PID 1
7.5 Zet restart policy passend bij het type workload
- Webserver/worker:
alwaysofunless-stopped - Batch job:
noofon-failure - Experimenteel: liever geen agressieve restart policy totdat je logs stabiel zijn
8. Docker Compose: typische valkuilen en checks
8.1 Toon de effectieve config
docker compose config
Hiermee zie je wat Compose uiteindelijk gaat gebruiken (samengevoegde env vars, defaults, etc.).
8.2 Bekijk logs per service
docker compose logs -f --tail 200
docker compose logs --no-color app
8.3 Herstarts tellen
docker compose ps
Of met Docker:
docker inspect -f 'Restarts={{.RestartCount}}' <container>
9. Kubernetes parallel: CrashLoopBackOff is hetzelfde concept
In Kubernetes heet dit vaak CrashLoopBackOff. De kern blijft: het proces stopt, dus de pod wordt herstart.
Handige commando’s:
kubectl get pods
kubectl describe pod <pod>
kubectl logs <pod> --previous
Als je app “ready” wordt gemarkeerd maar daarna faalt, kijk naar:
- Liveness probe te streng
- Readiness probe faalt door dependency
- Resource limits te laag (
OOMKilled)
10. Checklist: van “restart loop” naar oplossing
- Wat is de exit code?
docker inspect -f '{{.State.ExitCode}}' <container> - Wat zeggen logs (ook
--previous)?docker logs --previous --tail 200 <container> - Wat draait er als PID 1 (entrypoint/cmd)?
docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' <container> - Is er een restart policy die het erger laat lijken?
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' <container> - Zijn dependencies bereikbaar?
docker exec -it <container> sh -lc 'nc -vz db 5432' - Is het OOM?
docker events --since 1h | grep -i oom - Permissions/volumes oké?
docker inspect -f '{{json .Mounts}}' <container> | jq
11. Conclusie
Een container die steeds opnieuw start is bijna altijd één van deze categorieën:
- Het hoofdproces stopt direct (exit 0 of 1)
- Het commando bestaat niet of is niet uitvoerbaar (126/127)
- Misconfiguratie (env/config/secrets)
- Dependency is niet “ready” (DB/broker)
- Resourceproblemen (OOM → 137)
- Healthcheck/liveness-probe faalt
- PID 1/entrypoint-problemen (geen
exec, slechte signal handling) - Permissie- en filesystemproblemen
De snelste route naar de oplossing is consequent dezelfde aanpak: exit code + logs (ook previous) + inspect entrypoint/cmd + reproduceer interactief. Daarna pas ga je optimaliseren met healthchecks, retries en een passende restart policy.
Als je wilt, kun je je docker ps -a output, docker inspect van de container (zonder secrets), en de laatste ~200 regels docker logs --previous delen; dan kan ik de oorzaak meestal heel gericht aanwijzen.