Creating a kind Cluster With a Local Registry
Pushing to localhost:5001 from the host works, but pods referencing localhost:5001/api:dev sit in ImagePullBackOff with failed to do request: Head "https://localhost:5001/v2/api/manifests/dev": dial tcp [::1]:5001: connect: connection refused, or http: server gave HTTP response to HTTPS client. Both errors come from the same misunderstanding: inside a kind node, localhost is the node container, not your laptop. This page wires a local registry into a kind cluster so the same image reference works from the host and from pods, as part of local Kubernetes development.
A registry is the better way to get images into kind once images are large or the cluster has several nodes: pushes transfer only changed layers, and pods reference images exactly as in production, by registry and tag.
Diagnostic
Check where the node looks for the registry and what the kubelet reports:
#!/usr/bin/env bash
set -euo pipefail
docker ps --filter name=kind-registry --format '{{.Names}} {{.Ports}}' || true
kubectl --context kind-shop describe pod -l app=api | grep -A3 -E 'Failed|Back-off' | head -6
docker exec shop-control-plane sh -c 'ls /etc/containerd/certs.d 2>/dev/null || echo "no registry config dir"'
docker exec shop-control-plane sh -c 'curl -sS -m 3 http://localhost:5001/v2/ || echo "registry unreachable from node"'
Expected bad output:
kind-registry 127.0.0.1:5001->5000/tcp
Warning Failed 12s kubelet Failed to pull image "localhost:5001/api:dev": failed to do request: Head "https://localhost:5001/v2/api/manifests/dev": dial tcp [::1]:5001: connect: connection refused
no registry config dir
curl: (7) Failed to connect to localhost port 5001: Connection refused
registry unreachable from node
The registry runs and is published on the host, but the node has no mirror configuration and cannot reach localhost:5001 because nothing listens on the node's own loopback.
Root cause
kind nodes are Docker containers running containerd. When a pod references localhost:5001/api:dev, containerd inside the node resolves localhost to the node's own loopback interface, where no registry listens. Even when the address resolves, containerd defaults to HTTPS for any registry other than a few loopback special cases, so a plain HTTP registry fails with server gave HTTP response to HTTPS client. containerd supports per-registry configuration through hosts.toml files under /etc/containerd/certs.d/<registry>/, which can redirect localhost:5001 to another host and mark it as plain HTTP. kind enables that directory through the containerdConfigPatches entry in the cluster config, and the registry container must join the kind Docker network so the node can reach it by name.
Using localhost:5001 as the image reference, rather than kind-registry:5000, is deliberate: it is the name that works from the host for docker push and, after the mapping, from the node for pulls. One reference everywhere means manifests and Tilt configs need no translation.
Resolution
- Create the cluster with the registry config directory enabled (the
containerdConfigPatchesblock from the parent topic'sk8s/kind.yaml):
#!/usr/bin/env bash
set -euo pipefail
kind get clusters | grep -qx shop || kind create cluster --config k8s/kind.yaml
- Start the registry and attach it to the kind network:
#!/usr/bin/env bash
set -euo pipefail
reg_name=kind-registry
reg_port=5001
if [ "$(docker inspect -f '{{.State.Running}}' "$reg_name" 2>/dev/null || true)" != true ]; then
docker run -d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "$reg_name" registry:2.8.3
fi
docker network connect kind "$reg_name" 2>/dev/null || true
- Write the containerd mirror mapping into every node:
#!/usr/bin/env bash
set -euo pipefail
reg_dir="/etc/containerd/certs.d/localhost:5001"
for node in $(kind get nodes --name shop); do
docker exec "$node" mkdir -p "$reg_dir"
printf '[host."http://kind-registry:5000"]\n' | docker exec -i "$node" cp /dev/stdin "$reg_dir/hosts.toml"
done
containerd reads hosts.toml on each pull, so no restart is needed.
- Document the registry for tools by applying the standard
local-registry-hostingConfigMap, which Tilt and other tools read to discover where to push:
apiVersion: v1
kind: ConfigMap
metadata:
name: local-registry-hosting
namespace: kube-public
data:
localRegistryHosting.v1: |
host: "localhost:5001"
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
- Push and deploy with the same reference:
#!/usr/bin/env bash
set -euo pipefail
tag="dev-$(git rev-parse --short HEAD)-$(date +%s)"
docker build -t "localhost:5001/api:$tag" ./api
docker push "localhost:5001/api:$tag"
kubectl --context kind-shop set image deployment/api api="localhost:5001/api:$tag"
kubectl --context kind-shop rollout status deployment/api --timeout=90s
Expected output
$ kubectl --context kind-shop get pods -l app=api
NAME READY STATUS RESTARTS AGE
api-7c9d5f8b6d-x2kqp 1/1 Running 0 14s
$ docker exec shop-control-plane crictl images | grep api
localhost:5001/api dev-3f2a91c-1726650011 4b1e9c7a2d11 61.2MB
The pod pulled the image through the mirror, and crictl on the node shows it cached under the localhost:5001 name.
The second push after a small code change is where the registry pays off. Only the layers that changed — typically the application layer of a few megabytes — travel to the registry, and the node pulls only those same layers. Compared with kind load docker-image, which copies the full image tarball into every node on every load, a 600 MB image with a 3 MB change goes from roughly twenty seconds per load to about two seconds per push-and-pull on a typical laptop. The difference grows with each additional node in the cluster, since loads are repeated per node while registry pulls share the layers already present.
If a pull still fails after the setup, run docker exec shop-control-plane crictl pull localhost:5001/api:<tag> directly on the node. It bypasses the kubelet and prints containerd's own error, which names the exact host and scheme it tried — the fastest way to see whether hosts.toml is being read.
Prevention
Script the whole setup in
k8s/up.shso the cluster, registry, network attachment andhosts.tomlare always created together. Partial setups — a new cluster with an old registry that is no longer on thekindnetwork after a Docker restart — cause most recurrences.Use unique tags per build. Reusing
:devmeansset imagechanges nothing and pods keep the old image; a commit-plus-timestamp tag always triggers a rollout.Check the mapping in
make doctorwithdocker exec <node> cat /etc/containerd/certs.d/localhost:5001/hosts.toml, so a cluster recreated by hand without the script is caught immediately.
Platform caveats
Apple Silicon (ARM64): images built on the Mac are arm64 and run natively on kind's arm64 nodes. Do not reuse these tags for amd64 environments; build release images for amd64 in CI with buildx multi-arch builds.
macOS (Docker Desktop): port 5000 is taken by the AirPlay Receiver on recent macOS versions, which is why the registry is published on 5001. Keep that convention to avoid
address already in use.
WSL2: the registry container and kind nodes share Docker Desktop's network, so the setup is identical. Push from WSL using
localhost:5001; pushes from Windows PowerShell work too, through Docker Desktop's port forwarding.
Rollback
#!/usr/bin/env bash
set -euo pipefail
docker rm -f kind-registry
kind delete cluster --name shop
Without the registry, fall back to kind load docker-image, which needs no configuration at all.
Frequently Asked Questions
Why not reference kind-registry:5000 in pod specs?
Pulls from the node would work, but docker push kind-registry:5000/... from the host would not resolve. Using localhost:5001 everywhere and mapping it inside the node keeps a single reference for push and pull.
Does the registry survive a Docker restart?
The container restarts because of --restart=always, but it may not rejoin the kind network. Re-run docker network connect kind kind-registry in the setup script on every up; the command is harmless when already connected.
How do I clear old images from the registry?
The simplest approach is to delete and recreate the registry container, since local images are rebuilt on demand. For long-running registries, enable deletion with REGISTRY_STORAGE_DELETE_ENABLED=true and run the registry's garbage collector.
Can the same registry serve several kind clusters?
Yes. Connect it to the kind network once and write hosts.toml into each cluster's nodes. Every cluster then pulls from the same cache, which saves pushes when switching between projects.