eval $(minikube docker-env) repoints the shell’s Docker client at the daemon running inside the minikube VM. A docker build afterwards lands the image directly in the cluster’s container store, so pods can pull it without a registry. The pattern is correct but unforgiving: every failure mode looks like a different problem (image pull error, runtime crash, stale pod) and only a handful of them actually point back to the env-var setup.
What eval $(minikube docker-env) actually does#
The command prints shell exports that retarget the Docker CLI. Running it through eval applies them to the current shell:
$ minikube docker-env
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.49.2:2376"
export DOCKER_CERT_PATH="/Users/you/.minikube/certs"
export MINIKUBE_ACTIVE_DOCKERD="minikube"
# To point your shell to minikube's docker-daemon, run:
# eval $(minikube -p minikube docker-env)After eval, every docker command goes to the minikube daemon over TLS instead of the host’s Docker. docker images lists the cluster’s image store. docker build writes into it. The shell’s MINIKUBE_ACTIVE_DOCKERD is the only visible signal — there is no prompt change.
Reverse the binding when finished:
eval $(minikube docker-env -u)Forgetting to unset is the source of half the confusion downstream: a later docker build in the same shell unexpectedly populates the cluster instead of the host, and a docker rmi deletes from the wrong store.
A short verification sequence catches a misconfigured shell before any time is spent rebuilding:
echo "MINIKUBE_ACTIVE_DOCKERD=$MINIKUBE_ACTIVE_DOCKERD"
echo "DOCKER_HOST=$DOCKER_HOST"
docker info --format '{{.Name}}'
# Expected: MINIKUBE_ACTIVE_DOCKERD=minikube, DOCKER_HOST set, Name=minikubeIf Name is docker-desktop or anything other than minikube, the env was never applied to the current shell — the build will go to the wrong daemon.
The imagePullPolicy requirement#
An image built into the minikube daemon has no registry. Kubernetes’ default imagePullPolicy for a tag of :latest is Always, and for any non-:latest tag without an explicit policy is IfNotPresent. Both can defeat the workflow:
Alwaystriggers a registry pull on every pod start, which fails because the image only exists in the local store.IfNotPresentworks on first schedule but a node restart or image GC silently re-triggers the pull.
Set the policy explicitly on every workload that depends on a locally-built image:
containers:
- name: app
image: my-app:latest
imagePullPolicy: NeverNever is the only policy that guarantees Kubernetes uses the local image and surfaces a clear ErrImageNeverPull if the image is missing — which is exactly the diagnostic signal needed.
Five failure modes you’ll hit#
Each of these has the same root cause family — the image, the runtime, and the pod spec disagree about something — but the surface symptom is different.
1. ErrImagePull / ImagePullBackOff on a freshly-built image
Failed to pull image "my-app:latest": rpc error: code = Unknown desc =
Error response from daemon: pull access denied for my-app, repository does
not exist or may require 'docker login'The build went to the host’s Docker, not minikube’s. Either eval $(minikube docker-env) was never run, or it was run in a different shell. Confirm with echo $MINIKUBE_ACTIVE_DOCKERD in the build shell — if blank, the build missed the cluster. Re-eval, rebuild, redeploy.
2. lfstack.push crash on Go binaries
runtime: lfstack.push invalid packing: node 0xffff8b410100 cnt 0x1
packed 0x8b41010000000001 -> node 0xffff00008b410100
fatal error: lfstack.pushThe image was built for amd64 and is running through QEMU emulation on an ARM64 host. eval $(minikube docker-env) does not change architecture — docker build still produces images for the host arch unless --platform is set, and minikube’s containerd will run them through QEMU when they don’t match the node arch. See Architecture-mismatch debugging below.
3. exec format error
standard_init_linux.go:228: exec user process caused: exec format errorThe image was successfully pulled and started, but the binary inside is for the wrong architecture entirely (no QEMU registered, or no shim for the language runtime). Check docker manifest inspect my-app:latest — if the architecture doesn’t match the minikube node (kubectl get node -o jsonpath='{.items[0].status.nodeInfo.architecture}'), rebuild with docker buildx build --platform=linux/$(uname -m).
4. Image cached on an old pod, new pod gets the wrong build
$ kubectl rollout restart deployment/my-app
deployment.apps/my-app restarted
# but the new pod still runs yesterday's codeTwo pods can reference my-app:latest and resolve to two different image IDs if the build wrote a new image with the same tag while the old pod was still running. The old pod holds the old image ID; the new pod pulls (or doesn’t, with Never) and gets whichever copy the runtime decides. Always tag builds with a unique identifier (commit SHA, timestamp) and update the deployment manifest to that tag — never rely on :latest for in-cluster builds.
5. containerd runtime rejects the image
Failed to create pod sandbox: rpc error: code = Unknown desc =
failed to get sandbox image "my-app:latest": failed to pull image
"my-app:latest": ... no match for platform in manifestNewer minikube versions default to the containerd runtime, which is stricter about manifest validity than the older docker runtime. An image built by an older docker build can lack the OCI manifest fields containerd requires. Either rebuild with a recent Docker (24+) which produces OCI-compliant manifests by default, or switch the cluster runtime back with minikube start --container-runtime=docker.
Architecture-mismatch debugging#
On an Apple Silicon (M1/M2/M3/M4) host, the host Docker, the minikube daemon, and the minikube node are all ARM64. A naive docker build from the host shell against eval $(minikube docker-env) will produce an ARM64 image, which is exactly what the node wants. The trap is base images: FROM node:18 resolves the digest at build time against the daemon’s default platform, but a Dockerfile that hard-codes FROM --platform=linux/amd64 node:18 will produce an amd64 image even from an ARM64 daemon.
Confirm what was actually built:
eval $(minikube docker-env)
docker inspect my-app:latest --format '{{.Architecture}}'
# Expected on Apple Silicon: arm64If the architecture is wrong, the fix is at the Dockerfile level — strip the explicit --platform, or set it to $BUILDPLATFORM for buildkit to pick the correct one. The companion article on building ARM64 container images when upstream doesn’t ship them covers the binary-tarball pattern for projects with no published ARM64 image.
Three independent values must agree for an image to actually run:
- The architecture of the image (
docker inspect ... --format '{{.Architecture}}') - The architecture of the minikube node (
kubectl get node -o jsonpath='{.items[0].status.nodeInfo.architecture}') - The architecture the runtime is willing to execute (containerd will run a mismatched image only if a binfmt handler is registered for that arch)
A green check on the first two is not enough on a host where binfmt handlers have been installed for cross-arch experimentation — the runtime will accept the wrong-arch image and only fail at exec time. Always verify all three.
Alternatives and when to use which#
docker-env is one of four ways to get an image into a minikube cluster. Each has a sweet spot.
| Approach | When to use | Cost / friction |
|---|---|---|
eval $(minikube docker-env) + docker build | Iterative local development; need fast rebuild + immediate availability; image already builds cleanly with the host’s Docker | Shell-state risk (forget to -u); requires Docker on the host; build context streams to minikube daemon |
minikube image load my-app:latest | Image was built elsewhere (CI artifact, host Docker without env retarget); want to keep build and load steps explicit | Slower for large images (full tar export/import); requires the image to exist in the host Docker first |
minikube image build -t my-app:latest . | No host Docker installed (e.g. minikube-only environment); want a single command from Dockerfile to in-cluster image | Uses minikube’s built-in builder, fewer features than buildkit; slower than docker build against a warm cache |
minikube addons enable registry + push to localhost:5000 | Multi-node cluster; CI pipelines that already speak registry protocol; want imagePullPolicy: IfNotPresent to behave normally | Extra moving part to keep healthy; registry runs in-cluster and consumes resources |
kind load docker-image my-app:latest (kind equivalent) | Working in kind instead of minikube; same intent, different command | N/A — kind has no docker-env equivalent; loading is the only path |
The default for tight inner-loop work on a single-node cluster is docker-env. Switch to minikube image load the moment the build is happening in a different shell or process (CI, a Makefile target run from a fresh subshell). Switch to the registry addon when more than one node needs the image or when an existing tool already pushes to a registry.
The trap unique to docker-env is that the failure modes don’t look related. An ErrImagePull, a Go runtime crash, a stale-pod rollout, and a containerd manifest rejection all trace back to “the image, the runtime, and the pod disagree about something.” Knowing the four moving parts — the host Docker, the minikube Docker daemon, the cluster runtime, and the pod’s imagePullPolicy — is what makes the diagnostic ladder short instead of long.