Apple open-sourced a native container runtime for macOS (announced WWDC 2025, written in Swift). A few things that make it interesting:
One lightweight VM per container
Unlike Docker Desktop, which runs everything in a single shared Linux VM, each container gets its own micro-VM that boots in under a second. Stronger isolation, and no big always-on VM eating RAM.
Optimized for Apple Silicon
It's built on Apple's Virtualization framework, so it's fast and battery-friendly on M-series Macs.
Fully OCI-compatible
It pulls standard images from Docker Hub or any registry — container run nginx:alpine just works. Dockerfiles build as usual.
Each container gets its own IP
(on macOS 26), so you often don't even need port mapping — no more -p 8080:80 conflicts.
But there's no docker compose?
This turns out we don't really need it for local dev. Compose mostly gives us service discovery + one-command startup, and both are easy here:
sudo container system dns create test
container system property set dns.domain test
Now every named container resolves as <name>.test — e.g. the API just talks to db.test. A ~10-line shell script starts the stack in order, replacing compose up. And if we ever want real compose files, community tools (Container-Compose, socktainer) bridge that.
Free, open source, no Docker Desktop license. Could be worth trialing on a non-critical project. Repo: github.com/apple/container
Two Services Talking to Each Other with Apple container (A minimal tutorial)
An api service and a web service, running in Apple's native container tool, calling each other by DNS name (api.test) — no Docker, no Compose.
┌─────────────┐ ┌──────────────────┐
│ Browser │ ──────▶ │ web (port 8080) │
│ localhost │ │ fetches from: │
└─────────────┘ │ http://api.test │
└────────┬─────────┘
│ DNS: api.test
┌────────▼─────────┐
│ api (port 8000) │
└──────────────────┘
Prerequisites
- Apple Silicon Mac, macOS 26 (Tahoe) recommended for full container-to-container DNS
- Apple
containerv1.0+ installed (container --version)
Step 1 — One-time DNS setup
Create a local DNS domain so containers can resolve each other as <name>.test:
sudo container system dns create test
Set it as the default domain. Since v1.0 this lives in a TOML config file (the old container system property set command was removed):
mkdir -p ~/.config/container
cat >> ~/.config/container/config.toml << 'EOF'
[dns]
domain = "test"
EOF
Restart the service so the config is picked up:
container system stop
container system start
Verify:
container system property list
# [dns]
# domain = "test"
Step 2 — Start the API service
A tiny HTTP API that returns a JSON message on port 8000:
container run -d --name api python:3-alpine sh -c '
echo "{\"message\": \"hello from api\"}" > /tmp/index.html &&
python3 -m http.server 8000 -d /tmp
'
The container is now reachable from other containers at http://api.test:8000. No port publishing needed — every container gets its own IP.
Step 3 — Start the web service (calls the API by name)
A web app that fetches from api.test and serves the result to your browser:
container run -d --name web -p 8080:80 python:3-alpine sh -c '
pip install --quiet flask requests &&
cat > /app.py << "PY"
from flask import Flask
import requests
app = Flask(__name__)
@app.route("/")
def home():
r = requests.get("http://api.test:8000") # <-- DNS name, not an IP!
return f"<h1>web service</h1><p>api.test said: {r.text}</p>"
app.run(host="0.0.0.0", port=80)
PY
python3 /app.py
'
The key line is requests.get("http://api.test:8000") — the web container finds the api container purely by its DNS name, exactly like Compose service names.
Step 4 — Test it
From your Mac:
curl http://localhost:8080
# <h1>web service</h1><p>api.test said: {"message": "hello from api"}</p>
Or open http://localhost:8080 in your browser.
You can also poke around from inside a throwaway container:
container run -it --rm alpine sh
/ # wget -qO- http://api.test:8000
{"message": "hello from api"}
Step 5 — Day-to-day commands
container ls # running containers
container ls -a # including stopped ones
container logs web # view output
container stop api web # stop
container start api web # start again (after a reboot or system restart)
container rm -f api web # remove completely
Note: containers started with
--rmare deleted when stopped — re-run the
container runcommand instead ofcontainer start.
Optional — up/down scripts (your "compose" replacement)
up.sh:
#!/bin/bash
set -e
container run -d --name api python:3-alpine sh -c \
'echo "{\"message\": \"hello from api\"}" > /tmp/index.html && python3 -m http.server 8000 -d /tmp'
# wait until api is answering before starting web
until container run --rm alpine wget -qO- http://api.test:8000 >/dev/null 2>&1; do
sleep 1
done
container run -d --name web -p 8080:80 python:3-alpine sh -c \
'pip install -q flask requests && python3 -c "
from flask import Flask; import requests
app = Flask(__name__)
@app.route(\"/\")
def home(): return \"api said: \" + requests.get(\"http://api.test:8000\").text
app.run(host=\"0.0.0.0\", port=80)"'
echo "stack up → http://localhost:8080"
down.sh:
#!/bin/bash
container rm -f api web
echo "stack down"
Why this works without Compose
| Compose feature | Apple container equivalent |
|---|---|
| Service names on a shared network |
--name api + DNS domain → api.test
|
docker compose up |
./up.sh |
docker compose down |
./down.sh |
depends_on |
readiness loop in the script |
| Port publishing |
-p 8080:80 (same flag) |
For larger stacks, community tools like Container-Compose or socktainer (Docker-socket compatibility) can bring real compose files to Apple's runtime.
Top comments (0)