ImagePullBackOff: pull-fouten voor container-images in Kubernetes oplossen

ImagePullBackOff betekent dat de kubelet een container-image niet kon pullen en het opnieuw probeert met exponentieel oplopende wachttijden. De oorzaak staat altijd in de Events-sectie van kubectl describe pod: een typfout in de image-referentie, ontbrekende registry-credentials, Docker Hub-ratelimits, of een netwerkprobleem tussen de node en de registry. Dit artikel loopt door elke oorzaak, hoe je die vaststelt, en hoe je het oplost.

Wat ImagePullBackOff precies betekent

ErrImagePull en ImagePullBackOff zijn twee fasen van hetzelfde probleem. ErrImagePull verschijnt de eerste keer dat de kubelet er niet in slaagt een container-image van een registry te pullen. Als de pull blijft falen, gaat Kubernetes over op een exponentiële backoff-loop en verandert de status naar ImagePullBackOff.

De backoff-timing:

Poging Wachttijd voor de volgende poging
1 10 seconden
2 20 seconden
3 40 seconden
4 80 seconden
5+ 300 seconden (maximum 5 minuten)

De pod wordt niet afgesloten tijdens backoff. Hij zit gewoon te wachten op de volgende poging. Is de oorzaak tijdelijk (een korte registry-storing, een ratelimit-venster dat reset), dan herstelt de pod zich vanzelf. Is de oorzaak permanent (een typfout, een ontbrekend secret), dan blijft de pod in ImagePullBackOff hangen totdat je het probleem oplost.

Wanneer de kubelet images pullt

Het imagePullPolicy op elke container-spec bepaalt het pullgedrag:

  • Always: pullt bij elke containerstart. Standaard als je geen tag opgeeft of als de tag :latest is. De kubelet vergelijkt digests en slaat redundante layer-downloads over als de gecachte image overeenkomt.
  • IfNotPresent: pullt alleen als de image niet al op de node gecacht is. Standaard voor tags anders dan :latest.
  • Never: pullt nooit. Faalt met ImagePullBackOff als de image niet al op de node aanwezig is.

Kubernetes v1.33 alpha-noot. De KubeletEnsureSecretPulledImages feature gate (standaard uitgeschakeld) voegt credential-verificatie toe voor gecachte images. Voor v1.33 kon een pod met imagePullPolicy: IfNotPresent een gecachte private image gebruiken zonder geldige credentials. Met deze gate ingeschakeld krijgt diezelfde pod ImagePullBackOff tenzij de juiste imagePullSecrets geconfigureerd zijn. Zie je na een upgrade naar v1.33 met deze gate nieuwe pull-fouten? Controleer dan of de getroffen pods pull secrets missen die ze eerder niet nodig hadden.

De oorzaak achterhalen

De Events-sectie van kubectl describe pod is het belangrijkste diagnostische oppervlak voor image-pull-fouten. De exacte foutmelding vertelt je direct de oorzaakcategorie.

kubectl get pods -n <namespace>           # zoek de pod in ErrImagePull of ImagePullBackOff
kubectl describe pod <pod-name> -n <namespace>  # scroll naar Events

Typische Events-output:

Events:
  Type     Reason   Age              From     Message
  ----     ------   ----             ----     -------
  Normal   Pulling  3m               kubelet  Pulling image "registry.internal/myapp:v2.1"
  Warning  Failed   3m               kubelet  Failed to pull image "registry.internal/myapp:v2.1": not found
  Warning  Failed   3m               kubelet  Error: ErrImagePull
  Warning  BackOff  2m (x4 over 3m)  kubelet  Back-off pulling image "registry.internal/myapp:v2.1"

Foutmeldingenreferentie

Foutmelding in Events Wat het betekent
not found / manifest unknown De image-tag bestaat niet in de registry
repository does not exist / no such host Verkeerde registry-hostname of image-pad, of een DNS-fout
unauthorized / 401 / pull access denied Ontbrekende of onjuiste credentials voor een private registry
403 Forbidden Credentials zijn geldig maar missen permissie voor deze image
toomanyrequests / 429 Too Many Requests Registry-ratelimit bereikt (meestal Docker Hub)
i/o timeout / connection refused Netwerkprobleem tussen de node en de registry
x509: certificate signed by unknown authority TLS-certificaatprobleem (zelfondertekend of verlopen certificaat)

Voor bredere event-queries over namespaces heen:

kubectl get events -n <namespace> --field-selector type=Warning
kubectl get events --all-namespaces --field-selector reason=BackOff

Verkeerde imagenaam of tag

De meest voorkomende oorzaak. Een typfout in de imagenaam, tag of registry-hostname levert een manifest unknown of not found fout op.

Hoe je het verifieert

Pull de image eerst vanaf een lokaal werkstation:

docker pull registry.internal/myapp:v2.1

Faalt dit ook lokaal? Dan klopt de image-referentie niet.

Veelgemaakte fouten:

  • Typfout in tag: nginx:lates in plaats van nginx:latest
  • Tag weggelaten: zonder tag is de default :latest, en veel private registries publiceren geen latest tag
  • Verkeerde versie: myapp:v3 terwijl alleen v2.1 gepusht is
  • Padsegmenten omgewisseld: myapp/myorg:v1 in plaats van myorg/myapp:v1
  • Verwijderde tag: een CI/CD-pipeline heeft oude tags opgeruimd na deployment

Om beschikbare tags in een registry op te vragen:

# Docker Hub
curl -s https://registry.hub.docker.com/v2/repositories/myorg/myapp/tags/ | jq '.results[].name'

# Elke OCI-registry (met crane, uit google/go-containerregistry)
crane ls registry.internal/myorg/myapp

Oplossing

Bewerk de bovenliggende resource (Deployment, StatefulSet, DaemonSet), niet de pod direct:

kubectl set image deployment/my-deployment app=registry.internal/myapp:v2.2 -n <namespace>

Voor onveranderlijkheid kun je images pinnen op digest in plaats van tag:

spec:
  containers:
  - name: app
    image: registry.internal/myapp@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2

Je weet dat het gelukt is als kubectl get pods -n <namespace> de pod in Running-status toont en kubectl describe pod een Pulled-event laat zien met de juiste image-referentie.

Private registry-authenticatie (imagePullSecrets)

Als een pod probeert te pullen van een private registry zonder credentials, retourneert de registry 401 Unauthorized of de misleidende melding repository does not exist or may require 'docker login'. De kubelet pullt standaard zonder credentials; je moet imagePullSecrets expliciet configureren.

Stap 1: maak het secret aan

kubectl create secret docker-registry regcred \
  --docker-server=registry.internal \
  --docker-username=deploy-bot \
  --docker-password=<token> \
  -n my-namespace

Voor Docker Hub gebruik je https://index.docker.io/v1/ als serverwaarde.

Secrets zijn namespace-scoped. Het secret moet in dezelfde namespace als de pod staan.

Stap 2: verifieer het secret

kubectl get secret regcred -n my-namespace \
  --output="jsonpath={.data.\.dockerconfigjson}" | base64 --decode

De gedecodeerde JSON zou je registry-hostname als key moeten bevatten in het auths-object.

Stap 3: verwijs naar het secret in de pod-spec

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  namespace: my-namespace
spec:
  template:
    spec:
      imagePullSecrets:
      - name: regcred
      containers:
      - name: app
        image: registry.internal/myapp:v2.1

Koppelen aan een ServiceAccount

In plaats van imagePullSecrets aan elke pod-spec toe te voegen, kun je het secret koppelen aan het default ServiceAccount. Elke nieuwe pod in de namespace die geen ander ServiceAccount specificeert, erft het pull secret dan automatisch:

kubectl patch serviceaccount default \
  -p '{"imagePullSecrets": [{"name": "regcred"}]}' \
  -n my-namespace

Dit geldt alleen voor nieuwe pods. Bestaande pods moet je herstarten.

Cloudregistries met verlopende credentials

AWS ECR-tokens verlopen elke 12 uur. GCR-serviceaccount JSON-keys zijn langlevend maar een beveiligingsrisico. Azure ACR-serviceprincipal-credentials verlopen ook. Statische imagePullSecrets gaan bij al deze registries stuk.

De productieoplossing: kubelet credential providers (GA sinds Kubernetes 1.26). De kubelet roept bij elke pull een extern plugin-binary aan voor verse credentials. Geen CronJobs die secrets vernieuwen, geen verlopen tokens. Cloudproviders onderhouden de plugins:

  • AWS: ecr-credential-provider (cloud-provider-aws)
  • GCP: Workload Identity / gcp-auth-webhook
  • Azure: acr-credential-provider

Je weet dat het gelukt is als kubectl describe pod een Pulled-event toont in plaats van Failed, en kubectl get pods de status Running laat zien.

Docker Hub-ratelimits

Docker Hub past pull-ratelimits toe op basis van accounttype. Sinds april 2025:

Accounttype Pull-limiet
Niet-geauthenticeerd (anoniem) 100 pulls per 6 uur, per bron-IP
Geauthenticeerd (gratis Personal) 200 pulls per 6 uur, per account
Pro / Team / Business Onbeperkt

Het kritieke detail: niet-geauthenticeerde limieten gelden per bron-IPv4-adres. In een managed Kubernetes-cluster waar alle nodes dezelfde NAT-gateway delen, concurreert het hele cluster om 100 pulls vanaf een enkel IP. Een cluster met autoscaling workloads kan dat binnen minuten opgebruiken.

Als de limiet bereikt is, tonen de Events in kubectl describe pod:

toomanyrequests: You have reached your pull rate limit.

Diagnose

Controleer het resterende quotum vanaf een node (of een machine die het uitgaande IP van het cluster deelt):

TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/nginx:pull" | jq -r .token)
curl -I -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/library/nginx/manifests/latest 2>/dev/null | grep ratelimit

De ratelimit-remaining header toont hoeveel pulls je nog over hebt.

Oplossingen

Authenticeer je pulls. Zelfs een gratis Docker Hub-account verdubbelt je quotum en ontkoppelt het van je IP-adres. Maak een personal access token aan bij Docker Hub en voeg het toe als imagePullSecret:

kubectl create secret docker-registry dockerhub-creds \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username=je-dockerhub-user \
  --docker-password=<personal-access-token> \
  -n <namespace>

kubectl patch serviceaccount default \
  -p '{"imagePullSecrets": [{"name": "dockerhub-creds"}]}' \
  -n <namespace>

Gebruik een pull-through cache. Zet een registry-mirror op (Harbor, Nexus, of een gewone registry:2 met proxy cache) die Docker Hub fronted. Configureer containerd op alle nodes om de mirror te gebruiken via /etc/containerd/certs.d/docker.io/hosts.toml:

server = "https://registry-1.docker.io"

[host."https://registry-mirror.internal"]
  capabilities = ["pull", "resolve"]

Containerd pikt hosts.toml-wijzigingen dynamisch op. Geen restart nodig.

Zet imagePullPolicy: IfNotPresent voor stable tagged images. Dit voorkomt onnodige re-pulls als een node de image al gecacht heeft:

imagePullPolicy: IfNotPresent

Je weet dat het gelukt is als de toomanyrequests-fout verdwijnt uit de Events van kubectl describe pod en de pod naar Running gaat.

Troubleshooting op nodeniveau met crictl

Als kubectl describe pod niet genoeg detail geeft, ga dan naar de node. crictl praat direct met de container-runtime (containerd of CRI-O) via de CRI-socket, en omzeilt de Kubernetes API.

Verbinden met de node

kubectl get pod <pod-name> -n <namespace> -o wide   # zoek de nodenaam
ssh <node>

Voor containerd (standaard sinds Kubernetes 1.24):

sudo crictl --runtime-endpoint unix:///var/run/containerd/containerd.sock images

Of stel het endpoint persistent in via /etc/crictl.yaml:

runtime-endpoint: unix:///var/run/containerd/containerd.sock

Belangrijke commando's

# Gecachte images bekijken
sudo crictl images | grep myapp

# Pull direct testen (snelste manier om auth/netwerkproblemen te isoleren)
sudo crictl pull registry.internal/myapp:v2.1

# Testen met credentials
sudo crictl pull --creds deploy-bot:<token> registry.internal/myapp:v2.1

# Verbose output voor debugging
sudo crictl --debug pull registry.internal/myapp:v2.1

# Schijfruimte controleren (image-pulls falen als de node vol is)
df -h /var/lib/containerd

# Ongebruikte images opruimen als de schijf vol is
sudo crictl rmi --prune

containerd vs Docker: een veelgemaakte fout

Sinds Kubernetes 1.24 is Docker (dockershim) verwijderd. Alle moderne clusters draaien containerd of CRI-O. De Docker CLI communiceert niet meer met de container-runtime die Kubernetes gebruikt.

De praktische consequentie: credentials opgeslagen in /root/.docker/config.json worden niet gebruikt door containerd voor CRI image-pulls. Ben je gemigreerd van een Docker-gebaseerd cluster en werken je pulls niet meer? Dan is dit waarschijnlijk de oorzaak. Gebruik imagePullSecrets (via de Kubernetes API) of configureer containerd's hosts.toml voor node-level credentials.

Netwerk- en TLS-problemen

DNS- en firewallproblemen leveren no such host, i/o timeout of connection refused op in de Events-sectie.

Vanaf de node:

nslookup registry.internal                    # DNS-resolutie
curl -v https://registry.internal/v2/         # TCP + TLS-connectiviteit

Voor zelfondertekende registry-certificaten (x509: certificate signed by unknown authority), distribueer het CA-certificaat naar de nodes en configureer containerd:

# /etc/containerd/certs.d/registry.internal/hosts.toml
[host."https://registry.internal"]
  capabilities = ["pull", "resolve"]
  ca = "/etc/containerd/certs.d/registry.internal/ca.crt"

Voor clusters achter een corporate proxy, stel de proxy-omgevingsvariabelen in op de containerd service unit of kubelet-omgeving:

HTTPS_PROXY=http://proxy.internal:3128
NO_PROXY=10.0.0.0/8,192.168.0.0/16,.cluster.local

Je weet dat het gelukt is als curl -v https://registry.internal/v2/ HTTP 200 retourneert (of 401 voor auth-vereiste registries) en de pull van de pod slaagt bij de volgende poging.

Wanneer escaleren

Heb je alle oorzaken hierboven doorlopen en staat de pod nog steeds vast? Verzamel dan het volgende voordat je hulp vraagt:

  • Volledige output van kubectl describe pod <pod-name> -n <namespace> (met name Events)
  • De exacte image-referentie uit de pod-spec (kubectl get pod <pod> -o yaml | grep image:)
  • Of de image direct vanaf de node gepulld kan worden (sudo crictl pull <image>)
  • Kubernetes-versie (kubectl version)
  • Container-runtime en versie (sudo crictl version)
  • Node-conditions (kubectl describe node <node> | grep -A5 Conditions)
  • Schijfruimte op de node (df -h /var/lib/containerd)
  • Of een corporate proxy, firewall of admission webhook (OPA Gatekeeper, Kyverno) mogelijk interfereert

Herhaling voorkomen

  • Pin images op digest in productie-workloads om verrassingen door tag-drift te voorkomen.
  • Koppel imagePullSecrets aan ServiceAccounts in plaats van individuele pod-specs, zodat nieuwe deployments automatisch credentials erven.
  • Gebruik kubelet credential providers in plaats van statische secrets voor cloudregistries met verlopende tokens.
  • Draai een pull-through cache voor Docker Hub om afhankelijkheid van ratelimits op een externe service te vermijden.
  • Monitor op ImagePullBackOff-events cluster-breed. Een query als kubectl get events --all-namespaces --field-selector reason=BackOff in een periodieke check vangt problemen vroeg op.
  • Herstart na het oplossen van de oorzaak de getroffen pod of Deployment (kubectl rollout restart deployment/<name>) om de backoff-timer direct te resetten, in plaats van tot 5 minuten te wachten op de volgende automatische retry.

Is je pod wel gestart maar herstart hij steeds? Dat is een ander probleem. Zie CrashLoopBackOff: waarom je Kubernetes-pod steeds herstart voor diagnose van container-crashloops. Start de container wel maar faalt hij op health checks? Zie dan hoe je Kubernetes health probes configureert.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.