Apple ships a native container CLI tool with macOS. The tool runs Linux containers without Docker Desktop. It uses Apple’s Containerization Swift package and Virtualization.framework to create lightweight Linux VMs that run OCI-compatible container images with sub-second startup times.

This guide covers installing the container CLI, running containers, building custom images, managing resources, and resolving a macOS firewall issue that blocks local connections.

mac os containers

Requirements

Ensure you have:

  • Apple silicon Mac (M1, M2, M3, M4)
  • macOS 26 (Tahoe) or later
  • Homebrew installed

Installation

Install the container CLI using Homebrew:

brew install container

Verify the installation by checking the version:

container --version

Starting the Container Service

Start the container runtime service:

container system start

If no Linux kernel is installed, the command prompts you to install one:

Verifying apiserver is running...
Installing base container filesystem...
No default kernel configured.
Install the recommended default kernel from [https://github.com/kata-containers/kata-containers/releases/download/3.17.0/kata-static-3.17.0-arm64.tar.xz]? [Y/n]: y
Installing kernel...

This starts the runtime daemon that pulls images, creates VMs, and manages networking.

Verify the service is running by listing containers:

container list --all

If no containers exist, the output shows an empty list:

ID  IMAGE  OS  ARCH  STATE  ADDR

CLI Help

Get help for any command with --help:

container --help

Available subcommands include: create, delete, exec, inspect, kill, list, logs, run, start, and stop. Image and system subcommands handle building images, managing registries, and system configuration.

Check system status and configuration:

container system status
container system property list

The status command shows whether the runtime is running, installed kernel version, and API server status. The property list shows current DNS domain, registry domain, and other system settings.

Abbreviations

Common commands support abbreviations:

  • container listcontainer ls
  • container list --allcontainer ls -a

Running Your First Container

Run an nginx web server to test the container CLI. Unlike Docker Desktop, this CLI provides per-container VM isolation with configurable CPU and memory allocation:

container run --name web --cpus 2 --memory 2g -p 8080:80/tcp nginx:latest

Flag definitions:

  • --name web — assigns a friendly name for referencing the container in other commands
  • --cpus 2 — allocates 2 CPU cores to the VM (default is 4 if unspecified)
  • --memory 2g — allocates 2GB of RAM (default is 1GB if unspecified)
  • -p 8080:80/tcp — maps port 8080 on the host to port 80 inside the container
  • nginx:latest — the OCI container image to run

Test the container with curl:

curl http://localhost:8080

If the nginx welcome page HTML appears, the container is running. An empty reply indicates a firewall issue; see the troubleshooting section below.

Detached Mode

Run the container in the background with --detach:

container run --name web --detach -p 8080:80/tcp nginx:latest

Use --rm to automatically remove the container after it stops:

container run --name web --detach --rm -p 8080:80/tcp nginx:latest

Additional Configuration Examples

Pass environment variables to containers:

container run --name env-test --detach -e "MY_VAR=hello" -e "DEBUG=true" alpine:latest env

Mount host directories as volumes:

container run --name volume-test --detach -v /Users/username/data:/data alpine:latest ls /data

Map multiple ports:

container run --name multi-port --detach -p 8080:80 -p 8443:443 nginx:latest

View all containers (running and stopped):

container list --all

Viewing Logs

View container output with the logs subcommand:

container logs -f web

The -f flag follows the log stream in real-time. Access http://localhost:8080 in another terminal and watch access log entries appear. Press Ctrl + C to stop following.

Resource Monitoring

Monitor resource usage with container stats:

container stats web

The output shows CPU usage, memory consumption, network traffic, disk I/O, and running processes:

Container ID    Cpu %   Memory Usage          Net Rx/Tx            Block I/O            Pids
web             0.23%   12.45 MiB / 1.00 GiB  856.00 KiB / 1.2 KiB 2.10 MiB / 512 KiB   2

Get a single snapshot with --no-stream:

container stats --no-stream web

Monitor all containers simultaneously:

container stats

The memory usage column displays usage against the limit (12.45 MiB used out of 1.00 GiB allocated). Network columns show received (Rx) and transmitted (Tx) bytes.

Executing Commands in Containers

Run commands inside a running container with the exec subcommand:

container exec web ls /usr/share/nginx/html

Get an interactive shell:

container exec --tty --interactive web sh

Shorthand form:

container exec -ti web sh

Type exit or press Ctrl + D to leave the shell.

Accessing the Container

Container IP Address

Each container receives an IP address on a private network. Retrieve the IP with the inspect subcommand:

container inspect web | jq '.[0].networks[0].ipv4Address'

Output:

"192.168.64.9/24"

Inspect container details including created time, status, and configuration:

container inspect web

The output includes the container ID, image, created timestamp, state (running/stopped), network settings, and resource allocation.

Access the container directly on port 80:

curl 192.168.64.9

The nginx welcome page appears.

The container’s IP is on a private network (192.168.64.0/24), reachable only from the host Mac.

Embedded DNS

The CLI includes an embedded DNS service for container naming. Create a local DNS domain:

sudo container system dns create dev.local
container system property set dns.domain dev.local

The first command requires admin privileges. Access containers by name after configuration:

curl web.dev.local

Building Custom Images

Build custom images using Dockerfiles. This example creates a Python HTTP server container.

Create a Project Directory

mkdir web-test
cd web-test

Create a Dockerfile

FROM docker.io/python:alpine
WORKDIR /content
RUN apk add curl
RUN echo '<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello, world!</h1></body></html>' > index.html
CMD ["python3", "-m", "http.server", "80", "--bind", "0.0.0.0"]

This Dockerfile uses Python Alpine as the base image, sets the working directory to /content, installs curl, creates a simple HTML page, and runs Python’s HTTP server on port 80.

Build the Image

container build --tag web-test --file Dockerfile .

Build without caching (forces fresh layer download):

container build --no-cache --tag web-test --file Dockerfile .

List available images:

container image list

The output shows both the base image and the newly built image:

NAME      TAG     DIGEST
python    alpine  b4d299311845147e7e47c970...
web-test  latest  25b99501f174803e21c58f9c...

Additional image operations:

# Pull an image from a registry
container image pull redis:alpine

# Remove an image
container image delete redis:alpine

# Tag an existing image with a new name
container image tag python:alpine my-python:custom

Run Your Custom Image

container run --name my-web-server --detach --rm -p 8080:80/tcp web-test

Access the server at http://localhost:8080

Inter-Container Communication

Containers can communicate with each other. Launch a second container to fetch content from the first:

container run -it --rm web-test curl http://192.168.64.3

Or use DNS for name-based communication:

container run -it --rm web-test curl http://my-web-server.dev.local

This feature requires macOS 26.

Publishing Images

Push to Registry

Sign in to your registry:

container registry login some-registry.example.com

Tag your image:

container image tag web-test some-registry.example.com/username/web-test:latest

Push the image:

container image push some-registry.example.com/username/web-test:latest

By default, container uses Docker Hub. Change the default registry with container system property set registry.domain some-registry.example.com.

Pull and Run

To validate a published image, stop and delete the local copy, then pull and run from the registry:

container stop my-web-server
container image delete web-test
container run --name my-web-server --detach --rm some-registry.example.com/username/web-test:latest

Troubleshooting: Empty Response from curl

After starting nginx and mapping the port, curl may return an empty reply:

curl localhost:8080
curl: (52) Empty reply from server

The container shows no errors, but the connection fails.

Cause

The macOS Local Network firewall blocks applications from communicating over the local network by default. This includes traffic between curl and the container runtime.

Solution

  1. Open System Settings → Privacy & Security → Local Network
  2. Locate the terminal app or browser in the list and enable it
  3. Locate container-runtime-linux in the list and enable it
  4. Fully quit the browser with Command ⌘ + Q and reopen it

Enable Local Network access for both the application making the request and the container runtime. Missing either one causes the empty reply error.

Verification

Test the fix:

curl localhost:8080

The nginx welcome page HTML appears.

Cleanup

Stop containers and the service when finished:

container stop web my-web-server
container system stop

Containers started with --rm are automatically removed. Manually remove other containers:

container delete web my-web-server

Additional lifecycle commands:

# Start a stopped container
container start web

# Stop a running container
container stop web

# Restart a container
container restart web

# Kill a container immediately
container kill web

Summary

Apple’s container CLI provides native Linux containers on macOS using Virtualization.framework. No Docker Desktop is required. The sub-second startup times and OS integration make it suitable for local development.

Key features:

  • Per-container VM isolation with configurable CPU and memory
  • Built-in DNS service for container naming
  • Resource monitoring with container stats
  • Docker-compatible Dockerfile syntax
  • Registry support for pushing and pulling images

The project is pre-1.0. Expect breaking changes in future releases.

Resources