In WWDC 2025 (June 2025) Apple announced container, it’s a tool optimized for Apple silicon that you can use to create and run Linux containers as lightweight virtual machines on your Mac. The Containerization Swift package is used for low level container, image, and process management.

The tool consumes and produces OCI-compatible container images, so you can pull and run images from any standard container registry. You can push images that you build to those registries as well, and run the images in any other OCI-compatible application.

Quoting from the container Technical Overview to highlight the main architecture differences:

Many operating systems support containers, but the most commonly encountered containers are those that run on the Linux > operating system. With macOS, the typical way to run Linux containers is to launch a Linux virtual machine (VM) that hosts all of your containers.

container runs containers differently. Using the open source Containerization package, it runs a lightweight VM for each container that you create. This approach has the following properties:

  • Security: Each container has the isolation properties of a full VM, using a minimal set of core utilities and dynamic libraries to reduce resource utilization and attack surface.
  • Privacy: When sharing host data using container, you mount only necessary data into each VM. With a shared VM, you need to mount all data that you may ever want to use into the VM, so that it can be mounted selectively into containers.
  • Performance: Containers created using container require less memory than full VMs, with boot times that are comparable to containers running in a shared VM.

I was curious to see how Apple Native Containers fared against Docker so I ran a few tests. Here’s the TL;DR on what I found:

  • Design choices translate into very real differences that might make the tool useful for you, or not… the more permissive license can also be a decisive factor.
  • Apple Native Containers work as expected and provide the basic functionality to run and manage containers in a modern Mac, however they’re not a drop-in replacement for Docker.
  • Ecosystem support is still not up to par, for instance Container-compose is an Open Source project that brings (limited) Docker Compose support, and there’s only experimental support for using the tool with the VS Code Dev Containers extension.

Installing native containers

It’s possible to use brew for the installation, run the following command to install the Apple Container command-line tool.

brew install container

The CLI manages its own service; run this command to initialize and start the background system:

container system start

Check the status of the service, and run hello-world.

container system status
container run hello-world

So far, so good!

Hello world

Comparing the interface

Docker Desktop provides a lot of functionality and unsurprisingly container covers only a fraction of it. Comparing the output of docker help and container help makes that immediately obvious. The key question however is whether the essential functions are there and working as expected.

Let’s compare the container subcommands with those exposed by docker:

Apple Containers Docker Desktop Compatible Remarks
build: Build an image from a Dockerfile or Containerfile build: Build an image from a Dockerfile  
builder: Manage an image builder instance builder: Manage builds Totally different purpose
create: Create a new container create: Create a new container  
delete, rm: Delete one or more containers rm: Remove one or more containers  
exec: Run a new command in a running container exec: Execute a command in a running container  
image, i: Manage images image: Manage images The images subcommand doesn’t exist tough
inspect: Display information about one or more containers inspect: Return low-level information on Docker objects Container info only, no other objects
kill: Kill or signal one or more running containers kill: Kill one or more running containers  
list, ls: List running containers ps: List containers Different subcommand, but similar options
logs: Fetch container logs logs: Fetch the logs of a container  
network, n: Manage container networks network: Manage networks  
registry, r: Manage registry logins login: Authenticate to a registry, logout: Log out from a registry Registry manages logins, similar to login/logout
run: Run a container run: Create and run a new container from an image  
start: Start a container start: Start one or more stopped containers  
stats: Display resource usage statistics for containers stats: Display a live stream of container(s) resource usage statistics  
stop: Stop one or more running containers stop: Stop one or more running containers  
system, s: Manage system components system: Manage Docker  
volume, v: Manage container volumes volume: Manage volumes  

The standard options are there, but there’s some minor differences in the interface. It might be necessary to re-learn some syntax, but it’s nice to see that some container subcommands have a shorter alias.

Building and running a non-trivial workload

Time to build an run a containerized workload. I got inspired by a Christmas post I’ve seen on another blog , and decided to pick up something fun. Let’s start with the QuakeJS Rootless Project from JackBrenn. The project enables playing multiplayer Quake III Arena in a browser with Podman / Docker. Let’s see if it works with container. The command syntax to build and start the container was exactly the same as the Docker instructions.

git clone https://github.com/JackBrenn/quakejs-rootless.git
cd quakejs-rootless
container build -t quakejs-rootless:latest .
container run -d \
  --name quakejs \
  -e HTTP_PORT=8080 \
  -p 8080:8080 \
  -p 27960:27960 \
  quakejs-rootless:latest

Starting from scratch to compare the build times between docker and container both clocked very similar times, around 90 seconds. Which makes sense, given they’re both using the same engine to build their images. (BuildKit) is an improved backend that replaced the legacy Docker builder and it’s the default builder for users on Docker Desktop, and Docker Engine as of version 23.0. The only difference I was able to spot in the output is that container pulled the BuildKit image before starting to build the image.

The run step will start a completely local QuakeJS server running in a container in the background. Besides running the server engine which hosts the gameworld for several players, it will serve a WASM version of Quake III compiled using emscripten. No external dependencies, no content servers, no proxies - just pure Quake III Arena gaming in your browser. You can use this to host a LAN party anywhere! Point to localhost:8080 and accept the EULA:

QuakeJS EULA

And after downloading the necessary data files you’ll be ready for fragging your friends or co-workers.

QuakeJS in the browser

Checking the stats in Activity Monitor the observations seems to be consistent with what we now about each tool’s architecture. It seems Docker reserved a lot of memory for one big virtual machine.

Process Real Memory Size CPU usage
Virtual Machine Service for container-runtime-linux 1.00 Gb ~28%
Virtual Machine Service for Docker 6.85 Gb ~28%

As expected, when running more containers the Docker Virtual Machine service grows, when using Apple Native Containers more Virtual Machines will be spawned.

Funny story: one morning I opened my laptop just to see I still had QuakeJS running on the background. It was great to see the Apple native container barely made a dent in battery charge while the OS was sleeping. Anyway, it’s always a good idea to actively manage your containers and images, below are the stop and clean-up commands I used with container. They’re very similar to what you would expect with Docker, there’s one difference: you use docker ps to list containers.

container ls
container stop quakejs
container rm quakejs
container image ls
container image rm quakejs-rootless:latest

Running containerized ollama

Docker Desktop doesn’t support GPUs natively on Mac. Apple Native containers on the other hand should provide direct access to the hardware on Apple Silicon (M-series) for acceleration via Metal. So I decided to compare Docker, container, and a local install of ollama.

To get started I installed the ollama via brew:

brew install ollama
ollama serve

Then on another terminal I ran the llama2 model with the --verbose option to get some performance information and provided a very simple test prompt…

ollama run llama2 --verbose

I used a very simple prompt:

>>> tell me a joke

Why don't scientists trust atoms? Because they make up everything! 😂

The ollama serve logs and Activity Monitor confirmed the GPU and the metal API were being used.

To compare the docker and container performance I used the official ollama container image. Notice that I’m mapping a volume with ollama’s data folder to avoid downloading the model several times. The command line options to do this are the same in both tools (see below). Keep in mind this will download the model and start from scratch for the created container, this was intentional as I was comparing performance. The default folder for ollama files is ${HOME}/.ollama and for normal usage you should map it as a volume (-v ${HOME}/.ollama:/root/.ollama) so model definitions and settings are reused.

container run -d \
  -p 11434:11434 \
  --memory 6g \
  --name ollama \
  ollama/ollama
container exec -it ollama ollama run llama2 --verbose

Although I achieved better performance with Apple Native Containers, ollama wasn’t using the GPU either, according the the logs only the CPU was used. The stats seems to confirm this as well:

Metric Container Docker Native
Total Duration 15.231285424s 20.899204093s 3.471359458s
Load Duration 42.563833ms 39.974667ms 36.232667ms
Prompt Eval Count 26 token(s) 26 token(s) 26 token(s)
Prompt Eval Duration 3.866680543s 3.866771585s 692.442834ms
Prompt Eval Rate 6.72 tokens/s 6.72 tokens/s 37.55 tokens/s
Eval Count 58 token(s) 22 token(s) 32 token(s)
Eval Duration 11.307924547s 16.984443761s 2.736183s
Eval Rate 5.13 tokens/s 1.30 tokens/s 11.70 tokens/s

Take these numbers with a grain of salt, LLM’s are not deterministic and the generated output won’t always be the same, but the Eval Rate should give us a good reference of the performance for each scenario. It’s possible to disable GPU usage on native ollama by using /set parameter num_gpu 0 before your prompt, for me this yielded similar results to Apple Native Container.

Clearly the best option in Mac is still using ollama natively!

Apple native devcontainers

There’s a open VS code issue to Support for the Containerization Framework on macOS, and it’s easy to find the option to enable experimental support in the devcontainers extension. I briefly tested with this blog’s devcontainer.json , Jekyll was up and running and the interactive shell session worked as expected, but strangely the container output was not being captured.

Apple native containers experimental support

So in conclusion, Apple Native containers felt useful and performant, and I will probably be using them for some tasks. However I am not uninstalling Docker for the time being.