Links
Summary
Apple just released the first stable version of its native container product. Until now I used Docker Desktop to run containers on my Mac. After I tried it I now completely uninstalled this and am using the native version.
I will show you how to install this on your Mac. I will also cover running your first containers, bringing in DNS in order to be able to reach your workloads locally and configuring so that your containers survive restarts.
In order to setup everything correctly we also need to talk about some security settings on your Mac, some DNS stuff and tinkering around with your autostart.
There are also some tools to give you an UI to monitor and control your containers visually. I will use Davit.
Prerequisites
- MacOS 26 or newer.
- Apple Silicon as your hardware layer.
My situation
So I am working both on Windows and Mac. As previously stated I need a solution which works on both platforms. My team at devdeer (shoutout, guys!) is split into Windows- and Mac-users. So we are all currently running the following images on Mac/Windows at least:
- corentinth/it-tools:latest
- mcr.microsoft.com/dotnet/aspire-dashboard:latest
- mcr.microsoft.com/azure-storage/azurite
- mcr.microsoft.com/azure-sql-edge:latest (mcr.microsoft.com/mssql/server:2022-latest on Windows)
The point is that everyone of us uses the same parameters to run these containers so that our local development is harmonized and we can rely on having the same ports, connection strings, settings and so on. I want to keep this intact. So - a classical switch I would say.
container basics
So the first big difference between container and other solutions is that it runs natively in MacOS. It is developed in Swift. So because of that it is pretty lightweight by design. There is no single big VM running in the background and basically running the container stack - no matter if you currently use these containers or if they are basically idling.
Instead Apple decided to put every container in a single virtual environment which only consumes the assigned hardware resources if the workload in it needs it. This is a huge benefit on Mac. I cannot confirm it yet but energy consumption probably is a lot better than lets say with Docker Desktop. This mainly applies to the CPU though because consumed memory rarely is given back by the system.
This major difference can also confuse you a lot when you come from other solutions. The 2 big differences for me where a) there are no port mappings and b) there is no restart-control for containers.
The port-thing is logical now because (as we will later discuss in more detail) if container spins up a single little VM for every container than there is no need to map the ports because you never interfere with localhost. It is as if you are spinning the containers up somewhere else instead. This brings other issues (namely DNS) which we also will discuss later.
The missing “—restart” option means that we have to take care of the automatic start of container itself and then we need to tell it to spin up all containers it had on the last run (also later).
As for now these are the main differences I could see.
Installation
Lets start by installing container. I will use brew to do this:
brew install containerThat is pretty much it. After this is done your should be able to run:
container --versionand retrieve some response from the CLI.
Commands
container names subcommands a little bit different. So here are some of them:
| docker | container |
|---|---|
docker ps |
container ls |
docker ps -a |
container ls -a |
Spinning up your first container
First we need to start the container service. This is done by:
container system startNow you should be able to query for all currently running containers with:
container lsIt should give you an empty table. This is logical because you did not start any container yet. Let`s start simple using a hello world web-server:
container run --rm --name hello -d nginxdemos/helloThis should pull the image from Docker hub (yes, container uses the same repos your are used to and every OCI compliant image will do). This command ensures that the container runs in the background (“-d”) and gets removed as soon as we stop it.
First ensure that the listing works by repeating container ls.
Now try to stop the container and immediately repeat the listing with:
container stop hello && container lsIt should stop your container and clean it up (“—rm”) and the table should be empty again. Like in other container technologies the image is still there. Try container image ls to check. This means that your next container run --rm --name hello -d nginxdemos/hello should run almost immediately.
Your container listing should show you that your simple hello container takes 4 CPUs and 1GB of memory. This is absolutely too much for a simple container like this. So stop the container again and now run:
container run --rm --name hello -d -c 1 -m 200M nginxdemos/helloSo I am assigning 1 CPU and a maximum of 200 MBytes of memory to my container. Those values represent the minimums available.
System settings
Before we proceed to go deeper on the config we should set some system security settings in order to not hit authorization walls while using container. Open your system settings on the Mac and got to “Privacy & Security” > “Local Network”. Ensure that you check every important system. In my case I checked “iTerm”, “Rider”, “Visual Studio Code”, “container-runtime-linux” and “com.docker.backend”. If you don’t see any of these options just forget them. The most important one for this tutorial is the terminal application.
No ports but IPs
container handles the complete networking totally different than other container solutions. Every container you run gets its own IP address assigned. In my case my host is running at 192.168.178.53. When you now create a container lets say with:
container run --rm --name hello -d -c 1 -m 200M nginxdemos/hello && container lsYou will see something like:
hello docker.io/nginxdemos/hello:latest linux arm64 running 192.168.64.10/24 1 200 MB 2026-10-10T14:09:22ZThis means you can open your browser and browse http://192.168.64.10. This should give you a website. The 192.168* has nothing to do with my actual IP in my LAN. It comes from the fact that container uses the vmnet framework which has this range.
Configuring the network
Like in the other tools there is a container network subcommand. So container network list shows you all the networks available and the default one happens to be the one we got:
NETWORK SUBNET
default 192.168.64.0/24Simply use these steps to put a container somewhere else:
networkName='backend'
container network create $networkName --subnet 192.168.100.0/24
container run --rm --name hello --network $networkName -d -c 1 -m 200M nginxdemos/hello
container lsYou should see a 192.168.100.* now.
Configuring DNS
As easy as it is to access an IP it is kind of quirky. First of all the IP can change on any container run. So it is naturally far better to reference your containers using DNS. Turns out that this is pretty simple.
domain='local'
sudo container system dns create $domain
container stop -a
container rm hello
container run --rm --name hello --dns-domain $domain -d -c 1 -m 200M nginxdemos/hello
curl hello.localYou could also simple browse http://hello.local in your browser of cause.
Nice! So the container name gets the host name and then you just defined a domain which you append to it. This is far better than the IP approach.
Keeping the domain
The only problem now is that we have to remember this on every restart. Lets make it persistent:
Just create a new directory with
mkdir -p ~/.config/containerIt will generate this directory if it does not exist yet. Now lets create a file in there giving it some content (replace the domain “local” with the one you want):
cat >> ~/.config/container/config.toml <<'EOF' [dns] domain = "test" EOFIf you prefer to write the file with your editor this is the content it should have:
[dns]
domain = "local"Now we need to restart our container system so that it can read in this file:
container stop -a
container system stop
container system start
container run --rm --name hello -d -c 1 -m 200M nginxdemos/hello
curl hello.localBe aware that I did not specify the “—dns-domain” parameter and the container still is in it. This is the effect of having it defined in ~/.config/container/config.toml.
Automatic start of container on system-start
First we need a script which runs all of the available containers. Lets put it in ~/.local/bin/container-start.sh:
mkdir -p ~/.local/bin
touch ~/.local/bin/container-start.sh
chmod +x ~/.local/bin/container-start.shNow open this file with an editor and give it this content (replace the first line with #!/bin/bash if you run bash):
#!/bin/zsh
container=/opt/homebrew/bin/container
$container system start --enable-kernel-install || exit 1
for id in $($container ls -a -q); do
$container start "$id"
done%As stated earlier I used brew to install container. This is why on my system it lives at opt/homebrew/bin/container. Just run which container to retrieve your path and replace it in the script if it differs.
This script will start the container service and cycle through all containers starting them at once.
You can try this script out:
container system stop
~/.local/bin/container-start.sh
container lsCool. Now the system needs to execute this script on every boot. Under MacOS we need to create a so called “LaunchAgent” for this.
First create a file at ~/Library/LaunchAgents/com.user.container.plist and open it in your editor (I am using VS Code here):
touch ~/Library/LaunchAgents/com.user.container.plist
code ~/Library/LaunchAgents/com.user.container.plistNow give it this content:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.container</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOURUSER/.local/bin/container-start.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/container-agent.log</string>
<key>StandardErrorPath</key>
<string>/tmp/container-agent.log</string>
</dict>
</plist>Be sure to replace “YOURUSER” by your user name! Find it by executing
whoami.
Now you can test this with:
launchctl bootout gui/$(id -u)/com.user.container 2>/dev/null
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.container.plist
launchctl kickstart -k gui/$(id -u)/com.user.container
cat /tmp/container-agent.log
container lsIf the this runs without errors restart your Mac and test if everything runs with container ls.
Syncing my new stuff on Windows
Now my containers on my Mac (and of all the Macs of my colleagues) will run on something like mssql-dev.local, tools.local and so on. This is now different from Windows and a development connection string to my containerized database wouldn’t work.
Simple solution is to override those host names in the hosts file under Windows:
127.0.0.1 mssql.host
127.0.0.1 tools.host
127.0.0.1 aspire.host
127.0.0.1 azurite.hostThat way I can keep my host names no matter at which system I work. Of cause I still need to run a containerization solution on Windows as before.
Summary
I think Apple did a great job here and this is something I really miss from Microsoft nowadays. Instead of spending 99% of it’s energy for stuff like Copilot, OpenAI and Power* they should have let 20% of their teams do stuff like this. If all of the new shiny AI stuff does not play out like currently suggested we will see, if professionals will still be on the MS platform. Simple stuff like container can still raise some eyebrows!



