Run your Node.js app on Apple's container (a Docker Desktop alternative)
In June 2026 Apple released container 1.0, an official, open-source (Apache-2.0) command-line tool for running Linux containers on macOS. The twist versus Docker Desktop: instead of one big shared Linux VM hosting every container, Apple's Containerization framework gives each container its own lightweight micro-VM, booted from an optimized kernel for near-instant startup. It consumes standard OCI images, so your existing Dockerfiles just work. Let's prove it by building and running a real Node.js app — every command and its output below was captured on container 1.0.0, macOS 26, Apple silicon.
Requirements & install
container is Apple-silicon only and needs macOS 26 or later — Intel Macs are unsupported. Download the signed .pkg from the GitHub releases page, install it, and start the background service. The first start offers to download a default Linux kernel — say yes.
$ container system start
Verifying apiserver is running...
Installing base container filesystem...
$ container --version
container CLI version 1.0.0 (build: release, commit: ee848e3)The Node.js app
Nothing container-specific — a plain HTTP server and an ordinary Dockerfile, identical to what you'd feed Docker.
// server.js
const http = require('node:http');
const os = require('node:os');
http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
message: 'Hello from Node.js on Apple container!',
node: process.version,
arch: process.arch,
hostname: os.hostname(),
}, null, 2) + '\n');
}).listen(3000, () => console.log('listening on :3000'));# Dockerfile
FROM node:26-slim
WORKDIR /app
COPY package.json server.js ./
EXPOSE 3000
CMD ["node", "server.js"]Gotcha: your build context must live under your home directory
Here's the first surprise. The image builder itself runs in a VM that only mounts your home directory. Build from somewhere outside it — like /tmp — and the context arrives empty and COPY fails:
$ cd /tmp/demo && container build --tag node-demo:latest .
#6 [2/3] COPY package.json server.js ./
#6 ERROR: failed to calculate checksum of ref ...: "/package.json": not found
Error: ... "/package.json": not foundMove the project under ~ and the same command works. Keep your container projects somewhere inside your home folder.
Building the image
$ cd ~/apple-container-demo && container build --tag node-demo:latest .
#7 [linux/arm64/v8 2/3] COPY package.json server.js ./
#7 DONE 0.0s
#8 exporting to oci image format
#8 sending tarball 0.6s done
#8 DONE 0.6s
node-demo:latest
$ container image list
NAME TAG DIGEST
node 26-slim a1d9d671994f
node-demo latest e69b5a1a67f4Running it — and the one-VM-per-container model
Run it detached and list what's running. Notice the IP column: every container is its own VM with its own address on a private subnet (there's also a helper buildkit VM that backs the builder).
$ container run --detach --name node-demo node-demo:latest
node-demo
$ container list
ID IMAGE OS ARCH STATE IP CPUS MEMORY STARTED
node-demo node-demo:latest linux arm64 running 192.168.64.3/24 4 1024 MB 2026-07-06T11:26:42ZBecause the container has a real IP, you can reach it directly from macOS — no port publishing needed. You can even ping it:
$ curl http://192.168.64.3:3000/
{
"message": "Hello from Node.js on Apple container!",
"node": "v26.4.0",
"arch": "arm64",
"hostname": "node-demo"
}
$ ping -c 2 192.168.64.3
2 packets transmitted, 2 packets received, 0.0% packet lossYou can still publish ports to localhost
Some early write-ups claimed there's no -p port mapping. Not true in 1.0 — --publish works exactly like Docker's, forwarding a host port to the container:
$ container run --detach --name node-pub --publish 8099:3000 node-demo:latest
node-pub
$ curl http://localhost:8099/
{
"message": "Hello from Node.js on Apple container!",
"node": "v26.4.0",
"arch": "arm64",
"hostname": "node-pub"
}So you get both models: hit the container's own IP directly, or publish a port to localhost for tools and configs that expect it.
Running x86-64 images via Rosetta
On this Apple-silicon Mac, container can still run amd64 images by translating them with Rosetta — handy for reproducing an x86 production image locally. Pass --arch amd64:
$ container run --rm --arch amd64 node:26-slim node -e "console.log('reported arch:', process.arch)"
reported arch: x64The CLI is basically Docker's
If you know Docker, you already know this tool. The verbs line up almost one-to-one:
docker build -> container build
docker run -> container run
docker ps -> container list (alias: ls)
docker exec -> container exec
docker logs -> container logs
docker rm -> container delete (alias: rm)
docker images -> container image list
docker cp -> container cpCheat sheet: the commands you'll actually use
A quick reference for day-to-day work with container — all verified against the 1.0.0 CLI:
# --- service (the background apiserver) ---
container system start # start the service (installs a kernel on first run)
container system stop # stop it
container system status # is it running?
container system logs # apiserver logs
# --- images ---
container build --tag app:latest . # build from a Dockerfile
container image list # list local images (alias: container i ls)
container image pull node:26-slim # pull from a registry
container image push app:latest # push (after: container registry login)
container image delete app:latest # remove an image (alias: rm)
container image prune # remove dangling images
# --- running containers ---
container run --detach --name web app:latest # run in the background
container run --rm app:latest node -v # one-shot, auto-removed
container run --publish 8099:3000 app:latest # publish a host port
container run --arch amd64 app:latest # run an x86-64 image via Rosetta
container run --volume "$PWD:/app" app:latest # bind-mount a host dir
container run --env KEY=value app:latest # set an env var
# --- lifecycle & inspection ---
container list # running containers (alias: ls); --all for stopped
container logs web # view logs (add --follow to tail)
container exec -it web sh # shell into a running container
container inspect web # full JSON details (incl. its IP)
container stats web # live CPU/memory usage
container cp web:/app/out.txt . # copy a file out of a container
container stop web # stop it
container delete web # remove it (alias: rm); --force to kill+remove
container prune # remove all stopped containersWhat's missing versus Docker Desktop
- No
docker compose. This is the biggest practical gap — multi-service local stacks (app + database + Redis) that rely oncompose uphave no direct equivalent yet. - Not daemon-compatible. It's not a drop-in for the Docker socket, so tools that talk to
DOCKER_HOSTor the Docker API won't see it. - Apple silicon + macOS 26 only. No Intel Macs, no Linux, no Windows.
- Young. It's a 1.0 — expect rough edges (like the home-directory build-context rule above).
Should you switch?
For building and running individual Node.js services on a modern Mac, container is fast, native, free, and genuinely pleasant — and it reuses your existing OCI images and Dockerfiles. If your workflow is a single app or a couple of containers you wire together by IP or published ports, it's ready today. If your local dev is built around docker compose, keep Docker (or a compose-compatible tool) around until an equivalent lands. Either way, it's the most interesting thing to happen to containers on macOS in years — and it's from Apple, so it's not going anywhere.