If you develop on a Mac but ship to Linux, you've lived with some version of the same friction for years: you run a Linux container or VM to build and test, and there's always a gap between "I built it over here" and "I'm inspecting it over there." Files have to be copied, paths don't match, your editor is on one side and your binary is on the other.
Apple's container tool added a feature that closes that gap neatly: container machine. It's not a container in the usual sense — it's a persistent Linux environment that shares your Mac home directory and runs a real init system, so your repos and dotfiles are simply there, and systemctl start postgresql actually works. Here's what it is, how it's different from a normal container, and how to drive it.
A container vs a "container machine"
The distinction is the whole point. A normal container is modeled after an application: one main process, ephemeral, built to do one job and exit. That's perfect for running a service, less so for "give me a Linux box to work in."
A container machine is modeled after an environment. It boots the image's init system — systemd, typically — so you can register long-running services, run things under a process supervisor, and generally treat it like a small, well-behaved Linux machine. It's persistent (state survives across runs), it's built from standard OCI images you can build and share, and it integrates tightly with macOS. Think of it as a lightweight Linux VM that you create from a Docker image and that already knows who you are.
(For context: container is Apple's open-source tool for running Linux containers on Apple Silicon Macs, where each workload runs in its own lightweight virtual machine. container machine is the "I want a whole Linux environment, not just one process" mode of it. It needs an Apple Silicon Mac and a recent macOS, and it's a standalone tool you install separately — not part of Docker.)
The feature that sells it: your Mac home is already inside
Create one and look around:
container machine create alpine:latest --name dev
container machine run -n dev whoami # your macOS username — not root
container machine run -n dev pwd # your home directory, mounted in from the Mac
Two things just happened that don't happen with a plain container. You're yourself inside — whoami returns your host username, not root — and your home directory is your Mac home folder, mounted straight in. Your repositories, your dotfiles, your .gitconfig and shell setup are all right there, on both sides.
That changes the workflow. You edit on the Mac, build inside: open the repo in your macOS editor or IDE, and compile and run it in the Linux environment, with no copy step between the two. And because the files are shared, your macOS-native tools see the Linux artifacts — profilers, screenshot tools, browsers, GUI debuggers on the Mac all look at the exact same files the machine built. The "I built it" and "I'm looking at it" steps collapse into one.
You can also spin up one machine per target distro — an alpine, an ubuntu, a debian — each with the same home directory and the same dotfiles from your Mac, so testing your app across distributions is just switching which machine you run in.
The quickstart
The core verb is run. With no command it opens an interactive shell as your matching user; if the machine is stopped, run boots it first:
container machine run -n dev # interactive shell
container machine run -n dev uname -a # run one command and exit
container machine run -n dev -- cat /proc/cpuinfo # use -- before flags
Typing -n dev every time gets old, so set a default and drop it:
container machine set-default dev
container machine run # operates on "dev"
And there's an alias, m, for the whole thing — so m run, m ls, and friends all work. That's most of the day-to-day: create once, then m run to drop into your Linux box.
Managing your machines
The lifecycle commands are what you'd expect:
container machine ls # list them all
container machine inspect dev # JSON detail for one
container machine stop dev # stop it
container machine rm dev # delete it, including its persistent storage
Note that rm removes the machine's persistent storage too — it's a real delete, not just a stop. You can also resize a machine's resources; changes are written to disk and take effect after the next stop/start cycle:
container machine set -n dev cpus=4 memory=8G
container machine stop dev
container machine run -n dev -- nproc
A couple of defaults worth knowing: memory defaults to half of your host's memory, and the home-directory mount can be rw (the default), ro, or none if you'd rather not share your home folder into a particular machine.
What "persistent" actually buys you
The word "persistent" is doing real work here, and it's the other big departure from ordinary containers. A normal container is ephemeral: stop it, and anything you changed outside a mounted volume is gone. A container machine keeps its state — packages you apt install, service configuration, files you create outside your shared home — across stop and start. It behaves like a machine you own, not a fresh-every-time sandbox. The one command that does wipe it is rm, which the docs are explicit about: it deletes the machine including its persistent storage. stop pauses; rm destroys.
Real Linux services, because it runs init
This is the capability a plain container can't easily give you. Because a container machine boots the image's init system, system services work normally. On an image with systemd installed, you just:
container machine run -n dev
# inside the machine:
systemctl start postgresql
systemctl status nginx
That means you can run a database, a message broker, or whatever your stack needs as an actual managed service, and test your application against it under a real process supervisor — not as a hand-started background process you have to babysit. For integration testing against the same services you run in production, this is a much closer match than "docker run a single process."
A day in the machine
Here's how the pieces fit into an actual workflow. Say you're building a Go service that needs PostgreSQL and targets Ubuntu. You create an Ubuntu machine once and make it the default:
container machine create ubuntu:latest --name work
container machine set-default work
From then on, your repo — sitting in your Mac home directory — is already inside. You open it in your macOS editor and write code there. When you want to build and test, you drop into the machine and run it on Linux:
m run # shell, already in your home directory
systemctl start postgresql # a real service, supervised by init
go test ./... # build and test against it, on Linux
Nothing got copied. The binary your Linux build produced is visible to your Mac the instant it's written, so a macOS profiler or browser can inspect it directly. And when you stop the machine and come back tomorrow, Postgres, your installed packages, and your data are all still there. It's the develop-on-Mac, run-on-Linux loop with the seams taken out.
Bring your own image
You're not limited to stock images. Any Linux image that includes /sbin/init works as a container machine, so you can bake your ideal environment into a Dockerfile. Here's the shape of an Ubuntu image with systemd and a sensible toolset (trimmed):
FROM ubuntu:24.04
ENV container=container
RUN apt-get update && apt-get install -y \
dbus systemd openssh-server iproute2 iputils-ping \
curl wget vim-tiny sudo && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
yes | unminimize
# reset machine identity so each machine is unique
RUN >/etc/machine-id && >/var/lib/dbus/machine-id
# boot to a normal multi-user environment, mask units that don't apply
RUN systemctl set-default multi-user.target
Build it with container and create a machine from it:
container build -t local/ubuntu-machine:latest .
container machine create local/ubuntu-machine:latest --name ubuntu
On first boot, container runs a built-in script to provision the user that matches your Mac account. If you want control over that, drop an executable /etc/machine/create-user.sh into your image; it runs once, as root, on first boot, with CONTAINER_USER, CONTAINER_UID, CONTAINER_GID, CONTAINER_HOME, and CONTAINER_MACHINE_ID set — enough to create the account exactly how you want it.
Where it fits next to Docker Desktop and friends
It helps to place this against what you're probably using now.
| Plain container | container machine | Docker Desktop | |
|---|---|---|---|
| Models | one application/process | a whole Linux environment | a container engine + VM |
| Init system | usually none (one process) | yes — systemd works |
per-container (usually none) |
Your $HOME
|
bind-mount if you set it up | auto-shared, as your user | bind-mount if you set it up |
| Persistence | ephemeral by default | persistent | per-container/volume |
| Best for | running a service | a Linux dev box on your Mac | building/running app containers |
It's also not the first tool to put Linux on a Mac. Lima and Colima, Canonical's Multipass, and OrbStack all run Linux VMs or container engines on macOS, and each is good at what it does. What sets container machine apart is the depth of the host integration — automatic user and home mapping out of the box, OCI images as the unit of environment, and Apple's own virtualization stack underneath — rather than raw capability. If you already live in one of those tools and it works for you, there's no urgency to switch; if you're starting fresh on Apple Silicon and want the most native-feeling option, this is a strong default.
For the specific job of "I want a Linux environment on my Mac that feels native, keeps my files, and can run real services," container machine is purpose-built and lighter than reaching for a full VM manager. It isn't a replacement for a container orchestrator — you're not running production workloads on it — and it's Apple-Silicon-and-macOS only. But for the daily develop-on-Mac-target-Linux loop, it removes the copy step and the "where did my file go" confusion that every other approach leaves in.
The bottom line
container machine is a persistent Linux environment that shares your Mac home directory and boots a real init system, so your repos and dotfiles are simply there and systemctl start postgresql actually works. For the everyday develop-on-Mac, target-Linux loop it's lighter than standing up a full VM, and it collapses the copy step every other approach leaves behind. Just know its edges: it's Apple-Silicon-and-macOS only, and it complements a real orchestrator rather than replacing one — this is your dev box, not where production runs.
Top comments (0)