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.

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 list→container lscontainer list --all→container 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 containernginx: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,
containeruses Docker Hub. Change the default registry withcontainer 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
- Open System Settings → Privacy & Security → Local Network
- Locate the terminal app or browser in the list and enable it
- Locate
container-runtime-linuxin the list and enable it - 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.