← Terug naar tutorials

Hoe je Docker restart-loops snel en blijvend oplost

dockerdevopscontainerstroubleshootingrestart-loopkuberneteslogginghealthchecks

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:

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.

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:

In Docker Compose zie je dit vaak als:

restart: always

Belangrijk: restart: always kan 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:

Exit codes die vaak terugkomen:

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

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

  1. Zorg dat je een langlopend proces draait (bijv. nginx -g 'daemon off;').
  2. Gebruik geen “dummy” zoals tail -f /dev/null als 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

Diagnose

docker logs --previous <container>
docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' <container>

Oplossingen

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

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

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

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

  1. Maak je app robuust: retry met backoff.
  2. Gebruik healthchecks en wacht op “healthy”.
  3. 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_healthy werkt 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

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

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

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

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

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

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

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'

nc zit 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:

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:

7.2 Gebruik duidelijke exit codes en logging

Zorg dat je app bij config errors expliciet logt:

7.3 Gebruik healthchecks verstandig

7.4 Zorg dat PID 1 correct is

7.5 Zet restart policy passend bij het type workload


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:


10. Checklist: van “restart loop” naar oplossing

  1. Wat is de exit code?
    docker inspect -f '{{.State.ExitCode}}' <container>
  2. Wat zeggen logs (ook --previous)?
    docker logs --previous --tail 200 <container>
  3. Wat draait er als PID 1 (entrypoint/cmd)?
    docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' <container>
  4. Is er een restart policy die het erger laat lijken?
    docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' <container>
  5. Zijn dependencies bereikbaar?
    docker exec -it <container> sh -lc 'nc -vz db 5432'
  6. Is het OOM?
    docker events --since 1h | grep -i oom
  7. 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:

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.