cloud-sre
Apple container, Docker, and Kubernetes: what actually starts a container
A practical layer-by-layer comparison of Apple container, Docker, and Kubernetes: image handling, process startup, Pod sandboxing, and runtime boundaries.
Apple container, Docker, and Kubernetes all talk about containers, but they sit at different layers.
The common misunderstanding is this: Kubernetes has containers, but Kubernetes does not directly implement Linux containers. After a Pod is scheduled to a node, kubelet asks a CRI runtime to create the Pod sandbox and containers. The runtime stack, usually containerd or CRI-O plus a lower-level runtime, is what turns an image into running processes.
Apple container is different again. It works with OCI images, but on macOS it does not emulate Linux namespaces inside the macOS kernel. It runs each Linux container inside a lightweight Linux virtual machine.
Terms used here
| Term | Meaning |
|---|---|
| OCI image | The Open Container Initiative image format used across Docker, containerd, Kubernetes runtimes, and Apple container. |
| Runtime | The component that turns an image and spec into running processes. Depending on context, this may mean Docker daemon, containerd, CRI-O, runc, or Apple container’s runtime helper. |
| Namespace / cgroup | Core Linux container isolation primitives. Namespaces isolate views such as process, network, and mount state; cgroups constrain resources such as CPU and memory. |
| VM-backed container | A container process running inside a Linux VM. This is a common boundary when a non-Linux host needs to run Linux containers. |
| Pod sandbox | The base execution environment for a Kubernetes Pod. It establishes shared Pod boundaries such as networking before application containers join them. |
| CRI | Kubernetes Container Runtime Interface. kubelet uses it to talk to container runtimes. |
| CNI | Container Network Interface. Kubernetes uses CNI plugins to configure Pod networking. |
The short version
If the question is “who starts the process inside the container?”, the split looks like this:
| System | Layer | What finally starts the process | Isolation boundary |
|---|---|---|---|
Apple container | Local macOS OCI container tool and runtime | A host runtime helper starts a lightweight Linux VM; vminitd inside the VM starts the OCI process | One lightweight VM per Linux container |
| Docker Engine | Local image, build, network, volume, and container lifecycle tooling | Docker daemon delegates to the container runtime stack | Linux namespaces and cgroups on Linux; Docker Desktop on macOS also needs a Linux VM |
| Kubernetes | Cluster orchestration | kubelet calls a CRI runtime; the runtime creates the Pod sandbox and containers | Pod is the scheduling unit; containers are process units inside the Pod |
These are not clean substitutes for one another.
Docker and Apple container are closer to the local “run a container” layer. Kubernetes is the layer that keeps Pods running across a fleet of machines.
How Apple container works
Apple container is a Swift command-line tool plus local services. It consumes and produces OCI images, so the surface area feels familiar:
container run alpine uname -a
Under the hood, it does not ask the macOS kernel to provide Linux container isolation. macOS does not have Linux namespaces and cgroups. Apple chose a VM-backed design: each Linux container gets a lightweight Linux VM.
The startup path is roughly:
container CLI
-> container-apiserver
-> container-core-images / container-network-vmnet / container-runtime-linux
-> apple/containerization Swift package
-> macOS Virtualization.framework
-> Linux VM
-> /sbin/vminitd
-> OCI process
A few details matter.
First, the host side is split into an API server and helper services. Apple’s technical overview describes container-apiserver, image service, network service, and Linux runtime helper separately. The CLI does not directly fork a Linux process.
Second, the Linux boundary comes from Virtualization.framework. In apple/containerization, VZVirtualMachineInstance wraps VZVirtualMachine, configures a Linux kernel, and passes a kernel command line that uses init=/sbin/vminitd.
Third, vminitd is the guest-side control point. The host communicates with it over vsock/gRPC so it can set up mounts, networking, the runtime spec, and the target process inside the guest Linux system.
So Apple container is not “runc for macOS.” It is an OCI-oriented, macOS-native, VM-backed runtime.
How Docker starts containers
Docker’s model is older and more familiar:
docker CLI
-> Docker daemon
-> container runtime stack
-> isolated Linux process
On a Linux host, a container is an ordinary Linux process isolated with namespaces, cgroups, mounts, capabilities, and related kernel features. The image provides the root filesystem and metadata. The runtime prepares the environment from the OCI spec and then execs the application process.
On macOS, there is an extra boundary. The macOS kernel cannot directly run Linux containers, so Docker Desktop needs a Linux VM to host Docker Engine and Linux containers. Docker Desktop for Mac is also VM-backed, but it is usually a shared-Linux-VM model where many containers live inside the same VM.
The important contrast with Apple container is VM granularity:
| Point | Docker Desktop for Mac | Apple container |
|---|---|---|
| VM granularity | Usually one shared Linux VM hosts many containers | One lightweight VM per Linux container |
| Ecosystem | Mature Docker CLI, Dockerfile, Compose, registry, and BuildKit workflow | Apple Silicon and macOS-native experimental path, tightly integrated with Virtualization.framework |
| Isolation feel | Containers share the same Linux VM kernel | Containers have a harder VM boundary between them |
| Kubernetes relationship | Docker is not Kubernetes; dockershim used to bridge Docker Engine, but it is gone from modern Kubernetes | Not a Kubernetes runtime unless it grows a CRI-compatible layer |
Do not reduce the comparison to “does it use a VM?” On macOS, both paths need Linux somewhere. The deeper difference is who manages the VM, the VM granularity, the runtime API, and ecosystem compatibility.
Who starts containers in Kubernetes
Kubernetes is built around Pods, not raw containers.
A common startup path is:
kubectl apply / controller
-> API Server stores a Pod or Deployment
-> Scheduler selects a node
-> kubelet on that node observes the desired Pod
-> kubelet calls the CRI runtime
-> containerd or CRI-O creates the Pod sandbox
-> runtime creates and starts application containers
-> CNI plugin configures Pod networking
Kubernetes owns the desired state and orchestration:
- whether this Pod should exist;
- how many replicas should run;
- which nodes are eligible;
- whether failed containers should restart;
- how the workload is exposed through Services;
- which Secrets, ConfigMaps, volumes, and service accounts it needs.
The node runtime stack creates the actual container processes.
Modern Kubernetes talks to runtimes through CRI. Common choices are containerd and CRI-O. Docker Engine is not the direct built-in runtime path anymore. Kubernetes used to include dockershim as a bridge from kubelet to Docker Engine, but dockershim was removed in Kubernetes 1.24.
Containers in the same Pod usually share a network namespace, so they can talk to each other over localhost. That is why Kubernetes creates a Pod sandbox first: the sandbox owns the Pod-level network boundary, and application containers join it.
One mental model
Put all three on one map:
Kubernetes
-> kubelet on each node
-> CRI runtime: containerd / CRI-O
-> low-level runtime: runc / crun / VM-based runtime
-> Linux process
Docker Engine on Linux
-> dockerd
-> container runtime stack
-> Linux namespace + cgroup
-> Linux process
Docker Desktop on macOS
-> Docker Desktop VM
-> Docker Engine inside Linux VM
-> Linux container process
Apple container on macOS
-> container-apiserver
-> container-runtime-linux
-> Virtualization.framework
-> one lightweight Linux VM per container
-> vminitd
-> OCI process
This explains a few common confusions.
First, Kubernetes is not the same kind of product as Docker. Docker focuses on local building and running. Kubernetes focuses on orchestration across machines. In production, Kubernetes often runs OCI images through containerd, while a developer may build and test those images with Docker or Apple container.
Second, Pod is not a renamed container. A Pod is Kubernetes’ scheduling and shared-network unit. A container is a process unit inside the Pod. A Pod may have one application container or multiple containers such as sidecars.
Third, OCI compatibility does not automatically make Apple container a Kubernetes node runtime. Kubernetes needs CRI semantics: Pod sandbox lifecycle, container lifecycle, logs, exec, port forwarding, stats, image service, and more.
How I would choose
For quickly running a Linux image on a Mac, both Docker Desktop and Apple container are candidates. Docker has the mature ecosystem: Dockerfile, Compose, BuildKit, registries, and team workflows. Apple container is interesting because each container gets its own lightweight VM, the implementation is Swift, and it integrates deeply with macOS Virtualization.framework.
For long-running services in a test or production environment, the decision should move up a layer. You need Kubernetes or another orchestrator, plus a stable node runtime, CNI, CSI, registry, monitoring, and rollout strategy.
For understanding what “container” means, keep these boundaries separate:
The image format is OCI.
A local runtime turns the image into processes.
Kubernetes orchestrates Pods across machines.
macOS must use Linux VMs to run Linux containers.
Once those layers are separated, Apple container, Docker, and Kubernetes stop competing in your head. They all work around containers, but they are responsible for different parts of the system.
References
- Apple container README: https://github.com/apple/container
- Apple container technical overview: https://github.com/apple/container/blob/main/docs/technical-overview.md
- Apple containerization README: https://github.com/apple/containerization
- Apple containerization
VZVirtualMachineInstance: https://github.com/apple/containerization/blob/main/Sources/Containerization/VZVirtualMachineInstance.swift - Docker overview: https://docs.docker.com/get-started/docker-overview/
- Docker Desktop Mac settings: https://docs.docker.com/desktop/settings/mac/
- Kubernetes Container Runtime Interface: https://kubernetes.io/docs/concepts/architecture/cri/
- Kubernetes container runtimes: https://kubernetes.io/docs/setup/production-environment/container-runtimes/
- Kubernetes Pods: https://kubernetes.io/docs/concepts/workloads/pods/
- Kubernetes dockershim removal FAQ: https://kubernetes.io/blog/2022/02/17/dockershim-faq/