Kubernetes
13 min readAugust 8, 2026Updated August 19, 2026

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

Part ofKubernetes
AJ
Ajeet Yadav
Platform & Cloud Engineer
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.

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:

Rendering diagram…

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:

  1. Pulls the node image if it is not already present
  2. Boots the node container
  3. Runs a prep script configuring the native containerd snapshotter and required sysctl values
  4. Runs kubeadm init
  5. Applies the kindnet CNI
  6. Removes the control-plane taint so pods can schedule on the single node
  7. Merges a kubeconfig entry into ~/.kube/config automatically

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.

bash
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 --rm

The defaults are sensible and worth knowing:

OptionDefault
--namek8s-dev
--node-imagea pinned docker.io/kindest/node tag
--cpus¼ of host CPUs, minimum 2
--memory¼ of host memory, minimum 2g
--schemeauto (also http, https)
--max-concurrent-downloads3

--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.

bash
container k8s start --name my-cluster

delete (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:

bash
# 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:latest

It 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:

bash
container k8s write-config --name my-cluster --kubeconfig ~/.kube/my-cluster.kubeconfig

One 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:

bash
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 dev

Step 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 k8skindminikubeDocker Desktop k8s
Needs DockerNoYes (or Podman)Depends on driverYes
Node basiskindest/node + kubeadmkindest/node + kubeadmSeveral driversBundled
Isolation per nodeLightweight VMDocker containerVM or containerContainer in one VM
Multi-nodeNot yetYesYesNo
PlatformmacOS 26, Apple silicon onlyCross-platformCross-platformCross-platform
MaturityExperimentalMatureMatureMature
Licence costFreeFreeFreePaid 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

Official References

Was this article helpful?

Be the first to rate this article

Related Topics

Apple
Kubernetes
macOS
kind
Containers
Local Development
kubeadm

Found this useful? Share it.

Practice this

Related tools

Read Next