HomeRun your Node.js app on Apple's container (a Docker Desktop alternative)

Run your Node.js app on Apple's container (a Docker Desktop alternative)

By · Node.js & JavaScript developer
Published July 6, 2026

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 found

Move 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   e69b5a1a67f4

Running 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:42Z

Because 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 loss

You 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: x64

The 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 cp

Cheat 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 containers

What's missing versus Docker Desktop

  • No docker compose. This is the biggest practical gap — multi-service local stacks (app + database + Redis) that rely on compose up have no direct equivalent yet.
  • Not daemon-compatible. It's not a drop-in for the Docker socket, so tools that talk to DOCKER_HOST or 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.

Sources & further reading

About Code with Node.js

This is a personal blog and reference point of a Node.js developer.

I write and explain how different Node and JavaScript aspects work, as well as research popular and cool packages, and of course fail time to time.