Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Kubernetes & Helm By James Joyner IV · · 9 min read

Kubernetes Error Guide: 'Failed to pull image' CRI / containerd Pull Failures

Fix 'Failed to pull image' rpc errors in Kubernetes: resolve manifest unknown, 401/403/429 auth and rate limits, arch mismatches, and containerd registry config.

  • #kubernetes
  • #troubleshooting
  • #errors
  • #registry

Exact Error Message

Failed to pull image is the kubelet event wrapping a low-level error returned by the CRI (containerd or CRI-O). The wrapped rpc error exposes exactly where in the pull-and-unpack pipeline it broke. You will see it in pod events:

Events:
  Type     Reason   Age   From     Message
  ----     ------   ----  ----     -------
  Normal   Pulling  20s   kubelet  Pulling image "registry.example.com/web:v2.3.1"
  Warning  Failed   18s   kubelet  Failed to pull image "registry.example.com/web:v2.3.1": rpc error: code = Unknown desc = failed to pull and unpack image "registry.example.com/web:v2.3.1": failed to resolve reference "registry.example.com/web:v2.3.1": unexpected status from HEAD request to https://registry.example.com/v2/web/manifests/v2.3.1: 401 Unauthorized

Common variants — missing manifest, Docker Hub rate limiting, and an architecture mismatch:

Failed to pull image "registry.example.com/web:v2.3.1": rpc error: code = NotFound desc = failed to pull and unpack image: failed to resolve reference: registry.example.com/web:v2.3.1: not found: manifest unknown

Failed to pull image "nginx:1.27": rpc error: code = Unknown desc = failed to pull and unpack image "docker.io/library/nginx:1.27": failed to resolve reference: unexpected status: 429 Too Many Requests: toomanyrequests: You have reached your pull rate limit

Failed to pull image "myapp:1.0": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/library/myapp:1.0": no match for platform in manifest: not found

What the Error Means

The kubelet delegates the actual pull to the container runtime over the CRI socket. Failed to pull image is the kubelet’s wrapper; the rpc error: code = ... half is what containerd reported. The phrase failed to pull and unpack image confirms the failure happened inside the runtime, not in the Kubernetes scheduling layer.

Reading the tail of the message pinpoints the stage:

  • failed to resolve reference ... unexpected status: 401/403 — registry rejected authentication.
  • unexpected status: 429 ... toomanyrequests — Docker Hub rate limit.
  • not found: manifest unknown — the tag or digest does not exist in the repo.
  • no match for platform in manifest — the image has no manifest for the node’s CPU architecture (e.g. an arm64-only image on an amd64 node).

Because the pull happens per node, you can debug it directly on the node with crictl and ctr, bypassing Kubernetes entirely.

Common Causes

  • manifest unknown. The repo exists but the requested tag or digest was never pushed, or was garbage-collected.
  • Platform / architecture mismatch. A multi-arch image is missing the node’s platform, or a single-arch image targets the wrong CPU (no match for platform, linux/amd64).
  • Docker Hub rate limiting (429 toomanyrequests). Anonymous or free-tier pulls exceed Hub’s limit until the window resets.
  • Private registry auth not wired up. No imagePullSecret, or a secret of the wrong type — it must be kubernetes.io/dockerconfigjson.
  • Registry mirror or insecure-registry misconfiguration in /etc/containerd/config.toml, so containerd cannot resolve or trust the host.
  • Air-gapped cluster expecting a pull-through mirror that is missing the image.

How to Reproduce the Error

Force a platform mismatch on an amd64 node by referencing an arm64-only image:

apiVersion: v1
kind: Pod
metadata:
  name: arch-mismatch
  namespace: default
spec:
  containers:
    - name: app
      image: arm64v8/alpine:3.20   # no linux/amd64 manifest
kubectl apply -f arch-mismatch.yaml
kubectl describe pod arch-mismatch | grep -A4 Events

The event reports no match for platform in manifest: not found.

Diagnostic Commands

# Reproduce the pull on the node, outside Kubernetes, with verbose output
crictl pull registry.example.com/web:v2.3.1
crictl pull --debug registry.example.com/web:v2.3.1

# Pull with the lower-level containerd client (bypasses CRI auth plumbing)
ctr -n k8s.io images pull registry.example.com/web:v2.3.1

# Force a specific platform to confirm an arch mismatch
ctr images pull --platform linux/amd64 docker.io/library/myapp:1.0

# Inspect the multi-arch manifest list to see which platforms exist
crane manifest registry.example.com/web:v2.3.1 | jq '.manifests[].platform'

# Read containerd logs for the pull failure
journalctl -u containerd -n 80 --no-pager | grep -i 'pull\|resolve\|401\|429'

# Check the registry / mirror config containerd is using
grep -A6 'registry' /etc/containerd/config.toml

Step-by-Step Resolution

  1. Reproduce on the node with crictl pull. If crictl pull succeeds but the pod still fails, the issue is the pull secret wiring, not the registry.

  2. For manifest unknown, list the available tags with crane ls registry.example.com/web and push or re-tag the missing one.

  3. For a platform mismatch, build and push a multi-arch image (docker buildx build --platform linux/amd64,linux/arm64 --push) or schedule the pod onto a node whose architecture the image supports with a nodeSelector: { kubernetes.io/arch: arm64 }.

  4. For 429 toomanyrequests, authenticate the pulls so they count against your account, or mirror the image into a private registry:

    kubectl create secret docker-registry dockerhub \
      --docker-server=https://index.docker.io/v1/ \
      --docker-username=$DH_USER --docker-password=$DH_TOKEN -n default
  5. For private-registry 401/403, confirm the secret is the correct type and wired to the pod:

    kubectl get secret regcred -o jsonpath='{.type}'   # must be kubernetes.io/dockerconfigjson
    kubectl get secret regcred -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
  6. For mirror / insecure-registry issues, configure containerd and restart it:

    # /etc/containerd/config.toml
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.internal"]
      endpoint = ["https://mirror.internal:5000"]
    [plugins."io.containerd.grpc.v1.cri".registry.configs."mirror.internal:5000".tls]
      insecure_skip_verify = true
    systemctl restart containerd
    crictl pull registry.internal/web:v2.3.1   # verify the mirror works

Prevention and Best Practices

  • Build multi-arch images for every architecture in the cluster so the runtime always finds a matching manifest.
  • Mirror critical public images (Docker Hub base images) into a private registry to escape rate limits and outages.
  • Always create pull secrets with kubectl create secret docker-registry, which guarantees the kubernetes.io/dockerconfigjson type; a generic secret will silently fail to authenticate.
  • Manage /etc/containerd/config.toml registry blocks with configuration management so air-gapped nodes share an identical, tested mirror setup.
  • Validate crictl pull of your base images during node bootstrap to catch mirror and TLS issues before pods schedule. See more in Kubernetes & Helm guides.

Frequently Asked Questions

What does “failed to pull and unpack image” actually mean? It is containerd’s error for a pull that broke during the resolve, fetch, or unpack stage. The text after it (the rpc error and HTTP status) tells you which stage failed and why.

How do I test an image pull without Kubernetes? Run crictl pull <image> on the node — it uses the same containerd backend the kubelet uses. For an even lower-level test that bypasses CRI auth plumbing, use ctr -n k8s.io images pull <image>.

Why does Docker Hub return 429 toomanyrequests? Hub rate-limits anonymous and free pulls per IP or account. Many nodes behind one NAT share a limit quickly. Authenticate the pulls or mirror the images privately.

What is “no match for platform in manifest”? The image has no manifest for the node’s CPU architecture. Build a multi-arch image with docker buildx, or schedule the pod onto a matching-architecture node.

Does my pull secret need a specific type? Yes. It must be kubernetes.io/dockerconfigjson. Creating it with kubectl create secret docker-registry sets that type automatically; a hand-rolled generic secret will not authenticate.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.