Skip to main content
RunBook Academy

KubernetesCXV · Image Registry OperationsImage registry operations

Registry cache — pull-through proxy and local kubelet cache

Advanced⏱ ~17 minkubectldocker

What you'll learn

  • Configure a pull-through proxy cache (distribution)
  • Configure Harbor as a proxy cache
  • Reason about the kubelet local image cache
  • Apply the operational discipline of registry caching

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

Registry caching is essential for production performance and resilience. This lesson walks pull-through proxy caches, kubelet local cache, and the discipline.

The registry cache architecture

flowchart LR
    A[kubelet] -->|pull| B[Pull-through proxy cache]
    B -->|cache hit| C[Return cached image]
    B -->|cache miss| D[Upstream registry]
    D -->|return image| B
    B -->|cache and return| A
    E[Upstream down] -->|cache hit serves| A

The cache architecture:

  • Pull-through proxy. A registry that proxies pulls from an upstream; caches the result.
  • kubelet local cache. Per-node image storage; caches pulled images for reuse.

Both layers are essential. The proxy cache reduces upstream load; the kubelet cache reduces pull latency.

distribution pull-through proxy

# distribution config.yaml
version: 0.1
log:
  level: info
storage:
  filesystem:
    rootdirectory: /var/lib/registry
proxy:
  remoteurl: https://registry-1.docker.io
  username: [username]
  password: [password]

The distribution registry configuration:

  • storage. Where images are stored (local filesystem, S3, etc.).
  • proxy.remoteurl. The upstream registry URL.
  • proxy.username/password. Credentials for the upstream (Docker Hub, private registry).

When a Pod pulls an image, distribution checks its local cache; if missing, it pulls from the upstream, caches, and returns to the Pod.

Harbor as proxy cache

# Harbor proxy cache project
harbor-cli project create \
  --name dockerhub-cache \
  --public false \
  --proxy-cache \
  --proxy-url https://registry-1.docker.io

Harbor’s proxy cache:

  • A project is configured as a proxy cache.
  • The project’s URL is harbor.example.com/dockerhub-cache/library/<image>.
  • Pods pull from this URL; Harbor proxies to Docker Hub and caches.
# Pod spec
containers:
  - name: app
    image: harbor.example.com/dockerhub-cache/library/nginx:1.25

The Pod pulls nginx:1.25 from Harbor’s proxy cache. Harbor pulls from Docker Hub on first request, caches, and serves subsequent requests.

The kubelet local cache

flowchart LR
    A[kubelet] -->|pull image| B["Local cache: /var/lib/containerd"]
    B --> C{Image present?}
    C -->|Yes| D[Use cached image]
    C -->|No| E[Pull from registry]
    E --> B
    F[Disk pressure] -->|GC at 80%| B

The kubelet local cache:

  • Storage. /var/lib/containerd (containerd), /var/lib/docker (docker).
  • Hit. The kubelet uses the cached image; no pull.
  • Miss. The kubelet pulls from the registry.
  • GC. At 80% disk pressure, the kubelet garbage collects least-recently-used images.

The configuration

# /etc/containerd/config.toml (containerd)
[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "k8s.gcr.io/pause:3.9"

[plugins."io.containerd.grpc.v1.cri".containerd]
  [plugins."io.containerd.grpc.v1.cri".containerd.mirrors]
    [plugins."io.containerd.grpc.v1.cri".containerd.mirrors."docker.io"]
      endpoint = ["https://harbor.example.com/dockerhub-cache"]
    [plugins."io.containerd.grpc.v1.cri".containerd.mirrors."gcr.io"]
      endpoint = ["https://gcr.example.com"]

The containerd configuration:

  • mirrors. Map upstream registries to mirror endpoints.
  • The kubelet (via containerd) tries the mirror first; falls back to the upstream if the mirror fails.

The garbage collection

# kubelet configuration
--image-gc-high-threshold=85
--image-gc-low-threshold=80
--minimum-image-ttl-duration=2m

The kubelet GC:

  • high threshold (85%). When disk usage exceeds this, GC starts.
  • low threshold (80%). GC stops when disk usage drops below this.
  • minimum-image-ttl-duration (2m). Images must be at least 2 minutes old before they are eligible for GC.

Quiz

Knowledge check · 4 questions

  1. Q1. What determines when the kubelet removes unused images from a node?

  2. Q2. A node's image disk usage sitting just below the GC high threshold is expected behaviour.

  3. Q3. Deal with a node under disk pressure whose kubelet reports that image garbage collection cannot free enough space.

    Node `worker-11` has `DiskPressure: True` and is evicting Pods. The kubelet log repeats `failed to garbage collect required amount of images. Wanted to free 8589934592 bytes, but freed 1073741824 bytes`. `crictl images` lists 180 images totalling around 92 GB on a 100 GB image filesystem, and a CI namespace on this node runs roughly 40 short-lived Jobs an hour, each with its own image tag.

  4. Q4. In a containerd registry mirror configuration, what happens to a `docker.io` pull when the in-cluster cache is listed as its endpoint, and what happens when the cache does not hold the image?

Passing score: 75%. Answers are checked in this browser.

The operational discipline

Registry caching in production rests on five non-negotiable elements:

  • Pull-through proxy cache. Always; for both performance and resilience.
  • kubelet local cache. Default; tune GC thresholds.
  • Multi-registry mirrors. Configure multiple endpoints.
  • Monitor cache hit rate. High hit rate means less upstream load.
  • Document the cache topology. In the runbook.

Registry caching is production performance and resilience. The discipline is pull-through cache with monitoring.