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. anarm64-only image on anamd64node).
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 bekubernetes.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
-
Reproduce on the node with
crictl pull. Ifcrictl pullsucceeds but the pod still fails, the issue is the pull secret wiring, not the registry. -
For
manifest unknown, list the available tags withcrane ls registry.example.com/weband push or re-tag the missing one. -
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 anodeSelector: { kubernetes.io/arch: arm64 }. -
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 -
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 -
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 = truesystemctl 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 thekubernetes.io/dockerconfigjsontype; a generic secret will silently fail to authenticate. - Manage
/etc/containerd/config.tomlregistry blocks with configuration management so air-gapped nodes share an identical, tested mirror setup. - Validate
crictl pullof your base images during node bootstrap to catch mirror and TLS issues before pods schedule. See more in Kubernetes & Helm guides.
Related Errors
- Kubernetes Error: ErrImagePull First-Attempt Pull Failure — the kubelet-level status you see when this CRI error first surfaces.
- Kubernetes Error: ImagePullBackOff / ErrImagePull Image Pull Failures — the backoff state once the kubelet retries this failure repeatedly.
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.
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.