{"page":{"agent_metadata":{"content_type":"runbook","outputs":["docker-desktop-memory-cap","minikube-resource-change-procedure","pvc-backup-cadence","arm64-image-build-pattern","post-corruption-recovery-checklist"],"prerequisites":["minikube-setup-and-drivers","arm64-k8s-images","single-node-kubernetes-disaster-recovery"]},"categories":["kubernetes"],"content_plain":"A minikube cluster on Apple Silicon looks like a pure Kubernetes problem until the first Docker Desktop crash. The failure modes that bite hardest on M-series Macs live one layer below the cluster: in Docker Desktop\u0026rsquo;s memory allocator, in QEMU\u0026rsquo;s address-space layout, and in the destructive default of minikube delete. None of these are mentioned in the standard minikube setup guide, and all three will eat real workload state when they fire. This is the operational layer on top of minikube setup and drivers and ARM64 K8s images — the host-side discipline that keeps the cluster alive.\nDocker Desktop memory ceiling# The most dangerous Docker Desktop default on Apple Silicon is not a security setting — it is the memory auto-allocator. Docker Desktop allocates roughly 60% of system RAM by default. On a 64 GiB Mac that lands near 40 GiB. Under a real workload (a database, a message bus, a forge, observability, and a couple dozen pods) that allocation puts the macOS host inside the jetsam pressure window. macOS responds by killing the largest non-foreground process. That is almost always com.docker.backend. Docker dies, the minikube node dies with it, and any in-flight writes to PVCs that were not yet fsync\u0026rsquo;d to the underlying volume go with them.\nThe fix is a hard cap, applied while Docker is fully quit:\n# Edit while Docker Desktop is not running. ~/Library/Group Containers/group.com.docker/settings-store.jsonSet MemoryMiB so the cap leaves at least 16 GiB free for macOS, the editor, the browser, and whatever else the host runs. On a 64 GiB host, capping at 24 GiB (\u0026quot;MemoryMiB\u0026quot;: 24576) is a verified-good number — the cluster gets enough for the workload, the host stays clear of jetsam thresholds, and the macOS Virtualization framework hard-reserves the cap rather than treating it as a soft target. The exact key path may shift between Docker Desktop releases; verify against the current release before scripting it.\nVerify the cap took after restart:\ndocker info | grep -i \u0026#39;total memory\u0026#39; # Reports ~23 GiB on a 24 GiB cap (overhead accounts for the rest)The minikube --memory flag must be set to at most the Docker Desktop cap. A typical small autonomous-services rig (database + chat + forge + Prometheus + 20-ish pods) needs:\nminikube start \\ --cpus=8 \\ --memory=24576 \\ --disk-size=100g \\ --driver=docker--memory here is what minikube reports to Kubernetes as the node capacity. Setting it above Docker Desktop\u0026rsquo;s MemoryMiB is a direct path back to the jetsam window.\nCap Docker Desktop manually. The auto-allocator is not safe at workload scale on Apple Silicon.\nNative ARM64 image requirement# The Docker driver runs containers natively as ARM64 on M-series Macs — zero emulation, no Rosetta in the data path. That is the right driver. The QEMU driver triggers a fatal Go-runtime crash on every Go binary; the signature is in the dedicated article on the QEMU/Go incompatibility. The operational consequence: any image without an ARM64 manifest must be built natively before it can run.\nMany projects publish ARM64 binary tarballs years before they publish ARM64 Docker images. The pattern that works: download the binary, write a minimal Dockerfile, build directly into the minikube Docker daemon so the image lives where the cluster pulls from without a registry roundtrip.\neval $(minikube docker-env) \u0026amp;\u0026amp; \\ docker build -t my-app:\u0026lt;version\u0026gt;-arm64 docker/my-app-arm64Reference the locally-built image in Helm values with a pull policy that does not try to fetch from a registry:\nimage: repository: my-app tag: \u0026#34;\u0026lt;version\u0026gt;-arm64\u0026#34; pullPolicy: IfNotPresent # NOT Always — the image only exists locallyThis pattern only works with the Docker container runtime. On containerd or CRI-O, substitute minikube image build.\nFor non-Go amd64 images (Python, Node.js, C/C++), QEMU is fine. Install the binfmt handler once per host:\ndocker run --privileged --rm tonistiigi/binfmt --install amd64The handler persists across Docker restarts but not across host reboots. Reinstall after every reboot, or fold it into a launchd job.\nThe decision flow is short:\nDoes the image have an arm64 entry in docker manifest inspect? Use it directly. Is the application written in Go and only published for amd64? Build a native ARM64 image from the upstream binary tarball or source. QEMU will not work. Otherwise (non-Go amd64-only image): install the binfmt handler and run it as-is, accepting the emulation cost. On ARM64, the absence of an official ARM64 Docker image is rarely a blocker. The absence of an ARM64 binary release usually is.\nThe minikube-delete data-loss trap# minikube delete is not a reset button. It is a destroy button. It removes the node, its container filesystem, every PVC bound to the local-path provisioner, and every image cached in the node\u0026rsquo;s container store. Any database, message store, repository, or PV-backed state goes with it. Treat it like rm -rf.\nThe trap fires hardest during resource changes. Minikube prints a warning when CPU or memory flags differ from the existing cluster:\nThese changes will require a cluster reset. Do you want to continue?The warning is overcautious. The safe order is to try a stop-and-restart against the existing cluster first:\nminikube stop # Adjust Docker Desktop MemoryMiB if needed, restart Docker minikube start --memory=\u0026lt;new\u0026gt; --cpus=\u0026lt;new\u0026gt;This works in the majority of cases. Only fall back to minikube delete if start truly refuses to come up against the existing data directory. Before any destructive operation runs, snapshot any stateful workload that lives on a PVC. For PostgreSQL:\nkubectl exec -n \u0026lt;namespace\u0026gt; \u0026lt;postgres-pod\u0026gt; -c postgresql -- \\ pg_dump -U \u0026lt;user\u0026gt; \u0026lt;db\u0026gt; \u0026gt; /tmp/cluster-backup.sqlApply the same discipline to anything else with PV-backed state — the message store, the forge, the registry. The companion runbook on single-node Kubernetes disaster recovery covers what the snapshot cadence and restore path look like in full; this article\u0026rsquo;s contribution is the host-side trigger that makes the snapshot necessary.\nA separate but related lesson: an unreviewed cluster wipe can quietly take repos and registries with it. After one such incident, more than a dozen agent-built repositories were initially considered lost before they turned up in an off-host backup. The lesson is not the casualty count — it is that a single-node cluster is a single point of failure for everything that depends on it, and the inventory of \u0026ldquo;everything\u0026rdquo; is always larger than the operator remembers.\nBack up your PVCs before you change minikube\u0026rsquo;s resources, not after you regret it.\nCommon port-forward and image patterns# The minikube Docker daemon doubles as a local image registry. Two patterns recur:\n# Build into the cluster\u0026#39;s daemon (no push required) eval $(minikube docker-env) docker build -t \u0026lt;image\u0026gt;:\u0026lt;tag\u0026gt; . # Forward a service to localhost for local-tooling access kubectl port-forward -n \u0026lt;namespace\u0026gt; svc/\u0026lt;your-service\u0026gt; \u0026lt;local\u0026gt;:\u0026lt;service\u0026gt;Service names in the second command are deployment-specific — substitute the service for your database, message bus, or forge. Port-forward sessions die when the pod restarts; a small supervisor (e.g. a tmux window per service, or a Makefile target with wait semantics) keeps them stable across pod churn.\nWhen using locally-built images, every Helm chart that pulls them needs pullPolicy: IfNotPresent (or Never). The default Always policy issues a registry fetch on every pod schedule, fails to find the image, and leaves the pod in ErrImagePull despite the image being present in the daemon.\nThe minikube config caches CPU and memory choices in ~/.minikube/config/config.json. Inspect it with minikube config view. When start flags appear to be ignored, the cached values are usually the cause — either pass the flags explicitly on every minikube start, or update the cache with minikube config set memory 24576 and friends so the persisted values match the desired posture.\nDebugging# Three signatures cover most of the host-layer failures on this stack.\nDocker Desktop killed by jetsam:\nbackend cancelled with error: \u0026lt;nil\u0026gt; desktop state:ExitHealthyState agent-api: context cancelledSource frame: backend.go:560. The VM boots fine, dockerd runs, then the macOS host process com.docker.backend is externally cancelled. External is the tell — an application-level dockerd error surfaces with a non-nil error value and a different state machine. When this signature appears, the cause is almost always memory pressure; check MemoryMiB first, not Docker logs.\nGo binary running under QEMU on ARM64:\nruntime: lfstack.push invalid packing: node 0xffff8b410100 cnt 0x1 packed 0x8b41010000000001 -\u0026gt; node 0xffff00008b410100 fatal error: lfstack.push goroutine 1 [running]: runtime.throw({0x2bfaa07, 0x14})Always means: an amd64 Go binary is running under QEMU on an ARM64 host. There is no application-side fix. Stop debugging the application; debug the image manifest:\ndocker manifest inspect \u0026lt;image\u0026gt;:\u0026lt;tag\u0026gt; | jq \u0026#39;.manifests[].platform\u0026#39;If the manifest only lists amd64, build a native ARM64 image. If the manifest lists arm64 and the binary still crashes, the image\u0026rsquo;s ARM64 entry is mislabeled and was actually built for a different platform — rare, but not unheard of for community images.\nPod CrashLoopBackOff with no application log:\nkubectl logs \u0026lt;pod\u0026gt; --previous # Empty output, or only a runtime panic with no application frames docker manifest inspect \u0026lt;image\u0026gt;:\u0026lt;tag\u0026gt; | jq \u0026#39;.manifests[].platform\u0026#39;Empty --previous logs combined with crash-looping is the canonical signature for an architecture mismatch — the binary dies before it can log anything. The manifest inspection takes one command and rules out (or confirms) the architecture cause before any deeper debugging.\nRecovery after a host-side crash# When Docker Desktop has gone down hard and the cluster is unrecoverable, the order matters. Cap Docker Desktop\u0026rsquo;s memory before restarting it — coming up under the same auto-allocation will produce the same crash. Rename, do not delete, the old Docker data folder so a forensic recovery is still possible:\nmv ~/Library/Containers/com.docker.docker/Data \\ ~/Library/Containers/com.docker.docker/Data.broken-$(date +%s)Restore PVCs from the most recent snapshot before bringing dependent workloads back up. Rebuild any locally-built ARM64 images; their cache lived inside the minikube node\u0026rsquo;s container store and went with the cluster. If using an off-host backup drive (/Volumes/\u0026lt;your-backup-drive\u0026gt; or similar), confirm the backup was current before relying on it — backup cadence drift is the second-most-common cause of unexpected data loss on a single-node setup, after the minikube delete trap itself.\nThe recovery sequence in order:\nQuit Docker Desktop fully. Verify with pgrep -l docker returning nothing relevant. Edit MemoryMiB in the settings store to a safe cap. Do not skip this step on the assumption \u0026ldquo;the crash was a one-off.\u0026rdquo; Rename, do not delete, the existing Docker Data folder. Forensic value disappears the moment it is removed. Restart Docker Desktop and confirm docker info reports the capped memory. Reinstall the binfmt amd64 handler if non-Go amd64 workloads are part of the stack. Run minikube start with explicit memory and CPU flags matching the cap. Restore PVCs from snapshot before any application Helm release runs. Rebuild locally-sourced ARM64 images into the new cluster\u0026rsquo;s daemon. Bring up workloads in dependency order — datastore, then service tier, then anything that talks to them. A factory-reset Docker Desktop is the last step, not the first. Reset throws away the chance to inspect what the previous state contained, and on Apple Silicon the previous state usually still holds the answer to why the crash happened.\n","date":"2026-05-07","description":"Operational guide for running minikube on M-series Macs: Docker Desktop memory caps, the minikube-delete data-loss trap, native ARM64 image requirements, and post-corruption recovery.","lastmod":"2026-05-07","levels":["intermediate"],"reading_time_minutes":9,"section":"knowledge","skills":["minikube-on-mac-setup","docker-desktop-memory-tuning","k8s-cluster-recovery"],"tags":["minikube","apple-silicon","arm64","docker-desktop","macos","jetsam","disaster-recovery"],"title":"Running Kubernetes on Apple Silicon: Setup, Gotchas, Recovery","tools":["minikube","docker-desktop","kubectl","helm"],"url":"https://agent-zone.ai/knowledge/kubernetes/kubernetes-on-apple-silicon-setup-gotchas/","word_count":1799}}