DevOps & Platform

Run Linux Containers on Your Mac With Apple's container Tool

Beginner25 min to complete9 min readJuly 16, 2026

Quick answer

Apple ships an official, open-source way to run Linux containers on your Mac — no Docker Desktop, no license conversation, and every container gets its own IP address. Install it, run your first container, build an image from a Dockerfile you already have, and learn the day-to-day commands.

beginner · 25 min

Before you begin

  • An Apple silicon Mac (M1 or later)
  • macOS 26 (Tahoe) — the tool installs on macOS 15 but key networking features need 26
  • Basic familiarity with the terminal
  • A Dockerfile or two you'd like to try (optional — we build one from scratch)
Apple
Containers
macOS
Docker
OCI
Virtualization
DevOps

Here's the thing nobody tells you about running containers on a Mac: Docker doesn't run on macOS. It never has. Every tool you've used — Docker Desktop, Colima, OrbStack — quietly runs a Linux virtual machine in the background and hides it from you. Your containers live inside that hidden VM.

Apple's container tool plays the same trick with two differences: it's built by Apple on the macOS Virtualization framework, and instead of one big shared VM, every container gets its own tiny virtual machine — its own kernel, booted fresh in under a second, thrown away when the container stops. Think of it as each container getting its own studio apartment instead of a bunk in a shared dorm: better isolation, and nobody fights over the network ports.

I wrote up the architecture, the trade-offs, and how it stacks up against Docker Desktop in a full review of the 1.0 release — this tutorial is the hands-on companion. By the end you'll have the tool installed and be building and running containers with the same Dockerfiles you use everywhere else.

What You'll Build

  • Apple's container tool installed and its system service running
  • A running nginx container with its own IP address — no port mapping needed
  • A custom image built from a Dockerfile and running as a container
  • A local DNS domain so web.test works instead of memorizing IPs
  • The everyday workflow: exec, logs, stats, copy files, set CPU/memory limits, and run amd64 images via Rosetta

Step 1: Check Your Mac Meets the Requirements

Two hard requirements: Apple silicon, and ideally macOS 26 (Tahoe). Verify both:

bash
uname -m     # must print: arm64
sw_vers      # ProductVersion should be 26.x

Intel Macs are not supported at all. On macOS 15 (Sequoia) the tool installs and runs single containers, but container-to-container networking and several other features need vmnet APIs that only exist in macOS 26 — and the project only prioritizes issues on 26. If you're on Sequoia, expect rough edges.

Step 2: Install the Signed Package

Grab the latest installer from the GitHub releases page — you want the container-<version>-installer-signed.pkg asset. It's a normal, Apple-signed macOS package: double-click, click through, done. (As of this writing the current release is 1.1.0, from July 2026.)

Verify from a fresh terminal:

bash
container --version

Everything installs under /usr/local/, including two maintenance scripts you'll want to know exist: update-container.sh and uninstall-container.sh (used in Tear Down below).

One nicety worth doing now — shell completions:

bash
container --generate-completion-script zsh > ~/.container-completion.zsh
echo 'source ~/.container-completion.zsh' >> ~/.zshrc

Step 3: Start the System Service

container runs a lightweight background service that manages networking, DNS, and the API. Start it:

bash
container system start

On first start it notices there's no Linux kernel yet and offers to fetch one:

No default kernel configured.
Install the recommended default kernel from [https://github.com/kata-containers/...]? [Y/n]:

Say yes. This downloads an optimized kernel from the Kata Containers project — the same minimal kernel used for microVMs in multi-tenant clouds. It's the kernel every one of your container VMs will boot.

Sanity check:

bash
container ls

An empty table means the service is up and listening. (container ls and container list are the same command — most subcommands have short aliases; --help lists them.)

Step 4: Run Your First Container

Pull and run nginx, detached, with a name:

bash
container run --detach --name web docker.io/nginx:latest

Behind that one command: the image is pulled from Docker Hub, unpacked into an ext4 filesystem, a fresh VM boots a Linux kernel, and nginx starts as the VM's only process. Now look at what you got:

bash
container ls
# NAME  IMAGE                    OS     ARCH   STATE    ADDR
# web   docker.io/nginx:latest   linux  arm64  running  192.168.64.2

That ADDR column is the headline feature. Your container has its own IP address. Curl it directly:

bash
curl http://192.168.64.2

No -p 8080:80, no port collisions, no "bind: address already in use" because another project's container squats on the port. Run five things that all listen on port 80 and they coexist — each is its own host as far as the network is concerned. (If some tool insists on localhost, traditional port publishing exists too: container run -p 127.0.0.1:8080:80 ... forwards host port 8080 to the container.)

Step 5: Build Your Own Image

container build consumes ordinary Dockerfiles. Make a tiny web app:

bash
mkdir hello-container && cd hello-container

Create a Dockerfile:

dockerfile
FROM docker.io/python:alpine
WORKDIR /srv
RUN echo '<h1>Built and served by Apple container</h1>' > index.html
CMD ["python3", "-m", "http.server", "80", "--bind", "0.0.0.0"]

Build and run it:

bash
container build --tag hello --file Dockerfile .
container run --detach --rm --name hello hello
container ls        # note the new container's ADDR, then curl it

The first build starts a dedicated builder VM (2 GB memory, 2 CPUs by default) that sticks around for subsequent builds. Multi-stage Dockerfiles, build args, and build secrets all work as you'd expect. When you're ready to publish, it's the standard registry flow:

bash
container registry login ghcr.io --username <you>
container image tag hello ghcr.io/<you>/hello:latest
container image push ghcr.io/<you>/hello:latest

Anything OCI-compatible can pull the result — this is a normal image, nothing Apple-specific about it.

Step 6: Stop Memorizing IPs — Create a Local DNS Domain

The system service includes an embedded DNS server. Give it a domain once:

bash
sudo container system dns create test

(The sudo is for writing a resolver file under /etc/resolver so macOS knows to route *.test queries to the embedded DNS.) From now on, every named container is reachable at <name>.<domain>:

bash
curl http://web.test        # resolves to the nginx container's IP
curl http://hello.test      # your custom image

Names survive container restarts even when the IP changes, which makes them the right thing to put in config files. This also works between containers — the foundation for running multi-service stacks, which is exactly what the follow-up tutorial builds.

Step 7: The Everyday Commands

Everything maps closely to the Docker CLI you know, with delete instead of rm:

bash
1container exec -ti web sh            # interactive shell inside a container
2container logs web                   # application logs
3container logs --boot web           # the VM's kernel boot log — useful when a container dies instantly
4container cp web:/etc/nginx/nginx.conf ./nginx.conf   # copy files host <-> container
5container stats web                  # live CPU/memory usage
6container inspect web                # full JSON state (pipe to jq)
7container stop web && container delete web
8container image list                 # local images

Two logs --boot words of appreciation: because each container is a VM, when something goes wrong before your entrypoint runs, the kernel boot log tells you why. Docker has no equivalent.

Step 8: Resource Limits and amd64 Images

Each container VM gets 4 CPUs and 1 GB of memory by default. That's per container — twenty containers means twenty small VMs — so tune the ones that need more (or less):

bash
container run --cpus 8 --memory 4g --detach --name pg docker.io/postgres:17-alpine

Defaults live in ~/.config/container/config.toml if you want to change them globally:

toml
[container]
cpus = 4
memory = "1gb"

[dns]
domain = "test"

And for images that only ship x86_64 — still common for internal tooling — Rosetta 2 translates the binaries inside the Linux VM, which is dramatically faster than the QEMU emulation Docker Desktop falls back to:

bash
container run --arch amd64 --rm docker.io/amd64/alpine uname -m
# x86_64

Common Issues

  • container system start can't download the kernel. Corporate proxies and VPN split-tunnel rules are the usual culprits — the kernel comes from GitHub releases. Fix connectivity and rerun; the prompt reappears until a kernel is installed.
  • Containers can't reach each other on macOS 15 (Sequoia). Container-to-container networking requires vmnet APIs that only exist in macOS 26. Single containers work on 15; anything multi-service needs the OS upgrade. This is a platform limitation, not a bug you can configure around.
  • web.test doesn't resolve. Check the domain exists (container system dns list) and that the container was started with --name. If macOS itself won't resolve it, the /etc/resolver/test file may be missing — delete and recreate the domain with sudo container system dns create test.
  • A big image takes minutes to unpack. Known weak spot: images with hundreds of thousands of small files (looking at you, ML kitchen-sink images) unpack slowly through the userspace ext4 writer. Slim images unpack fast — one more reason to keep your images small.
  • Muscle memory sends you to docker ... or container rm. The verb differences are small (delete for rm, container image list for docker images). A alias d=container plus the completion script from Step 2 smooths the transition. What will not work is anything that talks to the Docker socket — see the FAQ.

Frequently Asked Questions

Does this replace Docker Desktop?

For building and running individual containers — yes, completely, and without licensing fees. What it doesn't replace is the Docker ecosystem: there's no Docker API socket, so Docker Compose, testcontainers, kind, and devcontainers don't work with it. Many developers run both side by side: container for daily builds and sandboxing, Docker Desktop or Colima kept around for compose-based projects. The 1.0 deep dive covers this trade-off honestly, and the three-way comparison with OrbStack and Colima helps you pick.

Can I use docker-compose with it?

No — Compose speaks the Docker API, which container doesn't implement. But per-container IPs plus DNS names remove much of what Compose exists to solve, and a small Makefile covers the rest. The multi-service stack tutorial shows the full pattern with Postgres, Redis, and an app container.

Why does every container get its own VM? Isn't that heavy?

Each VM boots a stripped-down Kata Containers kernel with a minimal init written in Swift — cold start is sub-second, so you don't feel it. The cost is memory: every container carries its own kernel, so the per-VM default of 1 GB matters more as container count grows. What you get in exchange is hardware-virtualization isolation per container — a container escape must beat a hypervisor, not just Linux namespaces. For running code you don't fully trust (AI-generated code, third-party tooling), that's the killer feature.

Do my existing Dockerfiles and registries work?

Yes. container builds standard OCI images from standard Dockerfiles and talks to any OCI registry — Docker Hub, GHCR, ECR, your private Harbor. Images built here run on Linux servers and vice versa. Only Docker-API-dependent tooling is affected.

Does it work on Intel Macs or older macOS?

No Intel support at all — the whole stack is built for Apple silicon. macOS 15 can install it, but the good parts (notably container-to-container networking) need macOS 26, and that's the only version the project actively supports.

Tear Down

Stop and remove what you created:

bash
container stop web hello
container delete web hello
container image delete hello docker.io/nginx:latest
sudo container system dns delete test
container system stop

To remove the tool entirely, the installer ships an uninstall script — -d deletes your user data (images, containers), -k keeps it for a later reinstall:

bash
/usr/local/bin/uninstall-container.sh -d

Official References

Next steps: replace your docker-compose workflow with the multi-service dev stack tutorial, read why the architecture matters, or brush up container fundamentals with the Docker cheat sheet.

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.