Local Kubernetes on macOS with Apple's container — No Docker Required

Quick answer
Apple's container tool can stand up a local Kubernetes cluster on macOS with no Docker anywhere in the stack. It is not a new distribution — it is kind's node image and kubeadm running on Apple's own runtime. Here's how the k8s plugin works, every command it gives you, and where it sits against kind, minikube and Docker Desktop.
- What it actually is
- The architecture
- What create actually does
- The rest of the commands
- A first cluster, end to end
13 min read · Kubernetes
Apple's container tool can stand up a local Kubernetes cluster on macOS with no Docker involved anywhere in the stack. No Docker Desktop, no daemon, no third-party VM manager — the k8s plugin boots a cluster node directly on Apple's own container runtime and merges the credentials into your kubeconfig.
The important thing to understand up front is what it is not. Apple did not write a Kubernetes distribution. They wrote a driver. The node image, the bootstrap path, and the Kubernetes bits are all upstream and well-travelled — what Apple contributed is running them on their runtime instead of Docker's.
That is a better outcome than a bespoke implementation would have been, and it makes the whole thing much easier to reason about.
What it actually is
container k8s manages local single-node Kubernetes clusters backed by container VMs. Each cluster runs a Kubernetes control-plane node inside a container using the kindest/node image — from the KIND project — bootstrapped with kubeadm.
kindest/node is a well-tested artefact that ships kubeadm, kubelet, kubectl and containerd pre-installed. KIND has spent years making a Kubernetes node work correctly inside a container. Reusing it means the cluster you get is upstream Kubernetes, assembled the ordinary way, rather than a fork with its own surprises.
Two constraints to know before you start.
It is experimental. Apple's own documentation is explicit: the k8s command is an experimental feature and its subcommands and options are subject to change. Treat the CLI surface below as current rather than settled.
The platform requirement is strict. container needs a Mac with Apple silicon and is supported on macOS 26 only. Apple explicitly does not support older macOS versions and states that maintainers typically will not address issues that cannot be reproduced on macOS 26. Intel Macs are out entirely.
One practical note: the k8s plugin is new enough that you want a current release. An early version shipped the feature with a packaging bug that stopped container k8s working when installed from the release package, fixed in the following patch. If the command misbehaves on a fresh install, upgrade before you debug anything else.
The architecture
Apple's container runs each container as its own lightweight VM — that is the core design of the tool, covered in Apple's container on macOS. The Kubernetes plugin inherits that model:
The node itself is a VM. Inside it, containerd runs your pods as ordinary Linux containers — namespaces and cgroups, not nested VMs. You pay for one virtual machine per cluster, not one per pod.
kubectl reaches the API server through a published host port. The container side is always 6443; the host side is allocated dynamically, starting at 6445 and taking the next free port above it for each additional cluster — so your first cluster gets 6445 → 6443 and a second one does not. Read the actual mapping from container k8s list rather than assuming.
Images move out of band: the CLI reads from container's image store and streams an OCI tar directly into the node's containerd under the k8s.io namespace. No registry sits in that path.
What create actually does
container k8s create performs a real sequence of work, and knowing it makes debugging far easier:
- Pulls the node image if it is not already present
- Boots the node container
- Runs a prep script configuring the native containerd snapshotter and required sysctl values
- Runs
kubeadm init - Applies the kindnet CNI
- Removes the control-plane taint so pods can schedule on the single node
- Merges a kubeconfig entry into
~/.kube/configautomatically
Step 6 is what makes a single-node cluster usable at all — without it every pod would sit Pending forever against a NoSchedule taint. It is also precisely why this is a development cluster: you are running workloads on a control-plane node, which is what you would never do in production. See taints, tolerations and affinity for what that taint normally protects.
1# create a cluster with the default name (k8s-dev)
2container k8s create
3
4# create a cluster with a custom name and resource allocation
5container k8s create --name my-cluster --cpus 4 --memory 8g
6
7# create a cluster that removes itself when stopped
8container k8s create --name temp-cluster --rmThe defaults are sensible and worth knowing:
| Option | Default |
|---|---|
--name | k8s-dev |
--node-image | a pinned docker.io/kindest/node tag |
--cpus | ¼ of host CPUs, minimum 2 |
--memory | ¼ of host memory, minimum 2g |
--scheme | auto (also http, https) |
--max-concurrent-downloads | 3 |
--node-image is the most useful of these. Because the node is just kindest/node, the image tag is the Kubernetes version. Testing against a different release is a matter of creating a second cluster with a different node image — genuinely handy for rehearsing an upgrade, and much cheaper than doing it on a real cluster. See zero-downtime cluster upgrades for what you would be rehearsing.
The rest of the commands
start restarts a stopped cluster and re-merges the kubeconfig, because the container IP can change between starts. A thoughtful detail — the failure where your cluster is running but kubectl points at a stale address is tedious to diagnose.
container k8s start --name my-clusterdelete (aliased rm) stops and removes the cluster and cleans its entry out of ~/.kube/config. Tools that leave orphaned kubeconfig contexts behind are a persistent annoyance; this one tidies up after itself.
load-image gets a locally built image into the cluster without a registry:
# load an image into the default cluster
container k8s load-image my-app:latest
# load the amd64 variant of a multi-arch image
container k8s load-image --platform linux/amd64 my-app:latestIt exports from container's image store to a temporary OCI tar and imports it into the node's containerd via ctr images import. Short references are automatically qualified — alpine becomes docker.io/library/alpine:latest — and then tagged back under the short name, so kubelet resolves whichever form your manifest uses. --platform selects a variant when the local store holds a multi-arch manifest list, defaulting to the host architecture.
write-config re-merges credentials into a kubeconfig, optionally a non-default one:
container k8s write-config --name my-cluster --kubeconfig ~/.kube/my-cluster.kubeconfigOne behaviour to internalise, because the three commands differ: create sets current-context to the new cluster. write-config and start do not.
That split is defensible — creating a cluster implies you want to use it, whereas refreshing credentials on an existing one shouldn't yank you away from whatever you were doing. But it does mean container k8s create silently moves your active context, so if you had a production cluster selected, you are no longer on it. Worth knowing before you run create on a machine that has real clusters in its kubeconfig. Contexts are named after the cluster, with no prefix — a cluster called dev gives you a context called dev.
list shows clusters and nodes, discovered via a plugin=k8s container label:
CLUSTER NODE ROLE STATE CPUS MEMORY ADDR PORTS
my-cluster my-cluster control-plane running 4 4096 MB 192.168.64.5 6445->6443
Kubernetes Production Readiness Checklist
The pre-launch checks we run before calling a cluster production-ready — probes, resources, RBAC, upgrades, and backups. Plain Markdown you can commit to your repo.
Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.
A first cluster, end to end
The whole workflow, from nothing to a pod running your own image:
1# 1. stand up the cluster
2container k8s create --name dev --cpus 4 --memory 8g
3
4# 2. create already merged the kubeconfig AND made this the current context
5kubectl get nodes
6
7# 3. build an image with container, then load it in
8container build -t my-app:latest .
9container k8s load-image --name dev my-app:latest
10
11# 4. run it — imagePullPolicy matters here
12kubectl run my-app --image=my-app:latest --image-pull-policy=IfNotPresent
13kubectl get pods
14
15# 5. tear it down
16container k8s delete --name devStep 4 has the trap. A locally loaded image exists only in the node's containerd — there is no registry holding it. If your manifest leaves imagePullPolicy at the default and your tag is :latest, Kubernetes defaults to Always and kubelet will try to pull from Docker Hub, fail, and leave you with ImagePullBackOff on an image that is demonstrably present. Set IfNotPresent, or avoid the :latest tag. This bites people on kind too — see fixing ImagePullBackOff for the general version.
How it compares
The honest comparison, given this is kind's node image either way:
container k8s | kind | minikube | Docker Desktop k8s | |
|---|---|---|---|---|
| Needs Docker | No | Yes (or Podman) | Depends on driver | Yes |
| Node basis | kindest/node + kubeadm | kindest/node + kubeadm | Several drivers | Bundled |
| Isolation per node | Lightweight VM | Docker container | VM or container | Container in one VM |
| Multi-node | Not yet | Yes | Yes | No |
| Platform | macOS 26, Apple silicon only | Cross-platform | Cross-platform | Cross-platform |
| Maturity | Experimental | Mature | Mature | Mature |
| Licence cost | Free | Free | Free | Paid for larger orgs |
The one-line summary: this is kind, with Apple's runtime instead of Docker's. That is not a criticism. For a lot of people "I want a local cluster and I do not want Docker Desktop on this machine" is the entire requirement, and this satisfies it with no third-party runtime at all.
Where it loses today is multi-node. kind's ability to run a three-node cluster is what you need to test anything involving scheduling across nodes, topology spread constraints, PodDisruptionBudget behaviour during drains, or node failure. A single-node cluster with the control-plane taint stripped cannot exercise any of that. Multi-worker and HA control-plane support are both on Apple's roadmap — along with service load balancing and pointing a cluster at a local registry so load-image becomes unnecessary — but none of it has shipped.
Should you use it
Yes, if you are on an Apple silicon Mac running macOS 26, you already use container or want to drop Docker Desktop, and your local Kubernetes needs are "run my app, check the manifests work, hit it with kubectl." That covers most local Kubernetes usage.
Not yet, if you need multi-node behaviour, you are on Intel or an older macOS, your team is cross-platform and you want one tool everyone can run, or "subcommands and options are subject to change" is disqualifying for you.
And regardless: this is a development tool. A single node with the control-plane taint removed, kindnet, and no HA is not a rehearsal for production. For that you want a real cluster — see managed Kubernetes compared.
Frequently Asked Questions
Does this replace Docker Desktop?
For local Kubernetes on Apple silicon, it can. container k8s needs no Docker daemon, no Docker Desktop licence, and no third-party VM manager. What it does not yet replace is multi-node testing, Intel support, or Docker Desktop's broader feature set. If Kubernetes is the only reason Docker Desktop is installed on your machine, this is a credible replacement.
Is it a real Kubernetes cluster or a lookalike?
Real. It runs upstream Kubernetes bootstrapped with kubeadm from the kindest/node image, with the kindnet CNI. It is not a reimplementation or a lightweight substitute like k3s — it is the same node image the KIND project uses, run on a different container runtime.
Can I run a multi-node cluster?
Not currently. The plugin creates single control-plane clusters only. Multi-worker and HA control-plane support are on Apple's roadmap but have not shipped. If you need to test scheduling across nodes, drains, or topology spread constraints, use kind for that.
How do I get my locally built image into the cluster?
container k8s load-image my-app:latest. It exports the image from container's store as an OCI tar and imports it into the node's containerd in the k8s.io namespace, so no registry is involved. For multi-arch images in your local store, pass --platform to pick the variant. Remember to set imagePullPolicy: IfNotPresent so kubelet does not try to pull it from a registry anyway.
Why is kubectl still pointing at my old cluster?
Because write-config and start deliberately do not change current-context — they merge credentials and leave your active context alone. Switch with kubectl config use-context. Note the asymmetry: create does set the current context, so a fresh cluster becomes active immediately while a refreshed one does not.
What Kubernetes version do I get?
Whatever the node image pins. The default is a specific docker.io/kindest/node tag, and you override it with --node-image to run a different version — which makes this a convenient way to test manifests against more than one release. On what changes between versions, see the Kubernetes 1.36 upgrade guide.
Does it work on an Intel Mac?
No. container requires a Mac with Apple silicon and is supported on macOS 26 only. Apple states that maintainers typically will not address issues that cannot be reproduced on macOS 26.
Is it production-ready?
No, and it is not meant to be. Apple labels the k8s command experimental, and the cluster shape — single node, control-plane taint removed, no HA — is a development configuration by design. Use it to develop against Kubernetes, not to run anything you care about.
Where this is heading
The published roadmap is the interesting part: multi-worker clusters, an HA control plane with a load-balanced API endpoint, service load balancing that maps onto container's networking model, local registry support so load-image becomes unnecessary, and an abstraction letting nodes come from distributions other than kind.
If the multi-node and registry pieces land, this stops being "kind without Docker" and becomes a genuinely competitive local Kubernetes story on macOS. Today it is a clean, experimental single-node cluster — worth installing if you are on Apple silicon, not yet worth standardising a team on.
See also
- Apple's container on macOS — the runtime underneath this, and how VM-per-container works
- Kubernetes vs Docker Compose — whether you need a local cluster at all
- Taints, Tolerations and Affinity — the control-plane taint this plugin removes
- Fixing ImagePullBackOff — the first error you will hit with a loaded image
- Managed Kubernetes Compared — where to go when a dev cluster is not enough
Official References
- Dockerfile best practices — layer caching, image size and build ordering
- Dockerfile reference — every instruction and its semantics
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


