The gap on every Mac developer’s machine

macOS is a Unix, and for years that was close enough. It stopped being close enough the moment your production target became a Linux container. The differences are small individually and exhausting collectively: BSD sed against GNU sed, no /proc, no systemctl, no apt, a linker that disagrees, and a library that isn’t glibc. Everything almost works, which is worse than nothing working, because the failure shows up in CI instead of on your desk.

Windows answered this in 2016 and then answered it properly: WSL gives you a named Linux distribution you switch on, keep, and treat as a computer, with your Windows files visible from inside it. The Mac’s answers were always adjacent to the question. Docker Desktop gives you containers — excellent for shipping images, wrong shape for living in, because a container is a process that ends, not a machine that persists. A full VM in UTM or Parallels gives you a real Linux with its own everything, including its own filesystem, which is exactly the wall you were trying not to build. Homebrew gives you GNU tools on macOS, which is a costume rather than a change of species.

The pieces existed. What didn’t exist was a thing you could name, switch on, and forget about — one that shared your files rather than copying them.

What Apple shipped, and what it left out

Apple’s container tool is a genuinely good foundation: an open-source CLI that runs OCI images on Apple Silicon, one lightweight VM per container, no Docker Desktop license and no always-on daemon eating a core. Version 1.1.0 added the piece that mattered — container machine, which runs the image’s own init system, mounts your Mac home inside at /Users/<you>, and creates a Linux account matching your host uid, gid and name.

That is the WSL shape, and it is Apple’s, not ours. But a bare machine is a rough landing. Three things stand between container machine create and something you’d actually work in:

Add the fact that the official archlinux image is amd64-only and simply will not run here, that Rocky publishes no latest tag, and that pacman 7’s downloader sandbox needs --disable-sandbox because it uses Landlock and the container kernel doesn’t support it — and you have an afternoon of archaeology before your first useful shell.

Why AppBox exists

Apple built the hard part — the virtualization, the init, the home mount, the uid mapping. AppBox is the missing hundred meters: a distro picker that only offers images verified to run on arm64, an init system baked into the ones that ship without it, the standard toolset, sudo, a bash login shell, dotfiles, and a persistent list of every box with a switch beside it. Every one of those is a thing you’d otherwise do by hand, once per box, forever.

The AppBox menu bar dropdown listing a running Ubuntu machine named test with its IP, CPU, memory and disk usage, above three stopped containers, each row with a state dot and an on/off switch

Every box, its state, and a switch — without leaving what you were doing. Machines and classic containers in one list; the container badge marks the exception, and the star marks the default machine.

A box is a machine, not a container

The unit AppBox manages is a box: a named, persistent Linux install that survives stop, start, and reboot. By default a box is one of Apple’s container machines, and the practical consequences of that are the whole point:

systemctl enable --now nginx works, and it comes back after a restart. Your Mac home is mounted inside at the same path, not copied — edit in your usual editor on the Mac, build in Linux, and it is the same file, not a synced approximation. Because your Linux account is created at your Mac account’s uid and gid, a file written inside the box is owned by you outside it, with no permission juggling on either side.

AppBox keeps the older shape too. A container is the original box: container run --init … sleep infinity, attached with exec. It has no init system, but it does have something machines can’t offer — a private per-box home and a /data directory, both on the host, both surviving destroy. Apple’s machine create takes no --volume, so if you need either of those, the older kind is still the right tool — a deliberate choice, not a leftover.

 Machine (default)Container
Init systemthe image’s own — systemd where it has onenone (sleep infinity)
Servicessystemctl works, survives restartstart daemons by hand
Your Mac homemounted at /Users/<you>not mounted
Private per-box homeno — dies with the machineyes, kept on the host
/data volumenoyes, kept on the host
Survives destroynothing — but your Mac home is untouchedhome and /data
Needscontainer 1.1.0+any container

Prior art: Linux calls these system containers

Nothing about the box idea is new. Linux has had it since 2008, when LXC first wrapped the kernel’s namespaces and cgroups into something you could hold, and it has been pleasant to live with since 2015, when LXD added a daemon, a REST API and a decent CLI on top. (Incus is the Linux Containers project’s fork of LXD, made after Canonical took the project in-house in 2023; the two are still near-identical to use.)

The distinction this whole article turns on — machine versus container — is the same one LXD draws between a system container and an application container. A system container boots the distribution’s own init, runs many processes, keeps its state, and is something you log into. An application container runs one process and exits. Docker won so completely at the second that plenty of people have forgotten the first is a category at all.

A box is a system container. If you’ve run LXD, you already know what AppBox is, and most of the vocabulary maps straight across:

ConceptLXD / IncusAppBox
Full-OS, persistent instancesystem containermachine (default)
Single-process instanceapplication containercontainer
Createlxc launch ubuntu:24.04 devappbox create-ubuntu dev 24.04
Interactive shelllxc shell devappbox shell dev
One-off commandlxc exec dev — cmdappbox exec dev cmd
Lifecyclelxc start|stop|deleteappbox start|stop|destroy
Share host filesdisk device, plus an idmapyour home, mounted automatically
Runs onLinuxmacOS on Apple Silicon

The differences fall where you’d expect. LXD carries a decade of operational surface AppBox has no answer to: snapshots and rollback, profiles, storage pools on zfs, btrfs, lvm or ceph, clustering across hosts, live migration, and the option of running a real VM under the same CLI. It is serious infrastructure software. AppBox is a menu bar app.

Two things run the other way, though. The first is that LXD doesn’t run on macOS — which, for this article, is the entire problem. You can run it inside a Linux VM on your Mac, and if you want profiles and snapshots that is the right answer, but now you are managing a VM in order to manage containers, and your Mac files sit two boundaries away from the thing using them.

The second is file ownership, and it’s the one that surprises people. LXD’s unprivileged containers map the container’s root to a high host uid range, so a directory shared in from the host arrives owned by nobody useful — and fixing it means raw.idmap entries, or shiftfs, or idmapped mounts, depending on the year and the kernel. AppBox never has that conversation: container creates the Linux account at your Mac account’s uid and gid, so a file written inside is owned by you outside. By construction, not by configuration.

The short version

AppBox is LXD’s system container, minus the operations toolkit, plus the one thing LXD can’t do — run on a Mac and share your actual home directory without an idmap argument. If your workstation is Linux, use Incus. If it’s a Mac, this is the nearest thing to it that exists natively.

Creating one

The New Box window explains every choice in place, because the answers have consequences you shouldn’t have to discover afterwards. The version field adapts per distribution — it tells you Ubuntu’s latest means the newest LTS, and that Rocky has no latest tag at all. The Mac home setting says, in words, where your files will appear inside. Full Linux install is on by default.

The New Box window with fields for name and distribution, a Machine or Container kind picker, a Mac home read/write setting, CPU and memory, and a Full Linux install toggle

Name, distro, version, kind. Each choice explains itself — including exactly where /Users/daver lands inside the box, and what the resolved image will be.

The AppBox dropdown showing a progress row that reads: Creating test… (the first machine of a distro builds an image)

The first machine of a distro builds a cached image, and says why while it does. Every later machine of that distro is created in seconds.

That one-minute first build is doing the work described above. Because most images can’t boot as a machine, AppBox composes one that can — the upstream image plus an init system plus the toolset — and caches it as appbox-machine/<distro>. Each recipe ends with RUN test -x /sbin/init, so a mistake fails the build rather than the boot, where it would have surfaced as that unhelpful sync-pipe error.

Distro token
ubuntu · debian · alpine
fedora · rocky · arch
resolve
arm64-verified
Cached image
upstream + init system
+ standard toolset
create
+ wait for boot
Machine running
systemd as PID 1
account at your uid
polish
Box
sudo · bash login shell
dotfiles · your home
The last step runs after first boot rather than being baked into the image — a second account at the same uid would collide with the one container creates itself. It is idempotent, which is what makes "Install Standard Toolset" safe to re-run on any box.

The tools, which are the actual product

A “full Linux install” here means roughly 28 packages that base images don’t ship: curl, wget, git, vim, nano, sudo, dig, ip, ping, htop, tmux, jq, tree, rsync, less, man, unzip, ca-certificates and friends — installed with the right package manager for the distro, plus sudo, a bash login shell and dotfiles for your account.

It sounds mundane. It is the difference between a box and a chore — and harder to get right across six distributions than it looks: dnf aborts an entire transaction over one unavailable package, and htop lives in EPEL rather than Rocky’s default repositories — so a plain install of the toolset on Rocky installed nothing at all. AppBox probes for --skip-unavailable (dnf5) and falls back to --setopt=strict=0 --skip-broken (dnf4, which Rocky 10 still ships), so a package it can’t find is skipped rather than fatal.

Living in it

The management window gives per-box detail adapted to what the box actually is. A machine shows the image it was built from, its DHCP address, how much disk it really occupies, your Mac home with a reveal-in-Finder button, the separate Linux home on the machine’s own disk, and whether it’s the default machine. A container shows its persistent home and /data instead. The console below carries the boot log — a real systemd boot, on a machine.

The AppBox management window showing the test machine running, with distribution, image, IP address, resources, user, disk, Mac home, Linux home, default status, creation time, and a live boot console

The detail that answers the questions you actually ask: what is this, where are my files, how much disk is it really using, and did it boot cleanly. Disk reads 533.1 MB used because the volume inside reports ~500G and is sparse — the honest number is the one shown.

Clicking a row — or Open Shell — starts the box if it’s stopped and drops you into a real Terminal window as your own user, not as root. On a machine you land in your Mac home, mounted at the same path inside, so the files you were editing thirty seconds ago are right there. Your terminal profile, your scrollback, your window management. No embedded terminal emulator, no compromises about which one you like.

A macOS Terminal window titled daver@test running container machine run, with the shell prompt reading daver@test:/Users/daver, beside the AppBox menu bar dropdown

The prompt says it all: daver@test:/Users/daver. Same user, same path, same files — running on Linux.

Five minutes from nothing

  1. Install Apple’s container — the signed .pkg from its releases page. There is no Homebrew cask. AppBox drives this tool; it is not optional.
  2. Install AppBox — open the DMG, drag it to Applications. It’s signed with a Developer ID certificate and notarized, so it opens normally with nothing to work around.
  3. Install the CLI — menu bar icon → gear → Install, which symlinks the appbox binary from inside the bundle onto your PATH. Do it after moving the app, since the symlink points at where the app was at install time. The installer lists every appbox on your PATH in resolution order, so a shadowed install is visible rather than mysterious — and anything in the way is renamed to appbox.previous, never deleted.
  4. Create a boxappbox create-ubuntu dev 26.04, or the New Box window. Name and version may be given in either order.
  5. Workappbox shell dev, or click the row. It’s already provisioned.

The CLI is the same engine as the app, not a reimplementation of it, so the two faces cannot drift:

appbox create-ubuntu web 24.04       # latest = newest LTS
appbox create dev ubuntu --container # the isolated-home kind
appbox create ci alpine --home-mount ro --cpus 8 --memory 8G

appbox shell   web                   # auto-starts the box
appbox exec    web 'systemctl is-active nginx'
appbox exec --root web 'apt-get install -y postgresql'
appbox use     web                   # make it the default machine
appbox list                          # both kinds, * marks the default
appbox start|stop|restart|ip|info|provision|destroy <name>

Flags come before the box name on exec; everything after the name passes through untouched, quoting included. A bare appbox create <name> never silently picks a distro for you — it uses $APPBOX_IMAGE, then the distro saved by set-default, and otherwise prints a menu and exits. Six distributions are offered with verified arm64 images, and any other value is passed through as a raw image reference, so appbox create tiny busybox:latest works too.

How it compares

CapabilityWSL 2LXD / IncusDocker DesktopFull VMAppBox
Named, persistent Linux you switch onyesyesno — images, not machinesyesyes
Real init, services survive restartyesyesnoyesyes — systemd
Host files shared, not copied/mnt/cdisk devicesbind mountsshared folders, with frictionhome, same path
Host uid/gid ownership matchesmetadata optsidmap configa recurring nuisancenoyes, by construction
Usable in seconds after createyesyesimage-dependentno — full OS installyes, after first per-distro build
Tools present on first logindistro defaultsdistro defaultsno — base imagesyes~28 pkgs + sudo, bash, dotfiles
Multiple distros side by sideyesyesyesone VM eachsix, one command each
Snapshots, profiles, clusteringnoyesnosnapshots onlyno
Idle costlowlow — one daemondaemon + licensea whole VMlow
GUI apps, drive-wide mapping, host/guest interopyes (WSLg)nonoyesno
Runs onWindowsLinuxanywhereanywhereApple Silicon, macOS 15+
Honesty clause

This is not a WSL clone and it is not an Incus replacement. There’s no WSLg forwarding Linux GUI apps onto your desktop, no drive-wide /mnt/c equivalent (your home is mounted; the rest of the disk isn’t), no interop layer running Linux binaries from the macOS side, and none of LXD’s operational machinery — no snapshots, no profiles, no clustering, no storage pools. What it reproduces is the operating model those two share — a named Linux you switch on, keep, and share your files with — on the one platform that had no native answer for it.

What it costs you to be wrong

Two things are worth understanding before you commit, and both come from Apple’s design rather than ours.

A machine’s disk always goes when the machine does. There is no keeping it. That is less of a loss than it sounds, because the files you care about live in your Mac home, which was mounted rather than copied — destroying a machine costs you installed packages, not your work. But it does mean a machine is the wrong place to keep anything you haven’t put under /Users/<you>.

Every machine sees that same one home, with no isolation between them. rm -rf ~ inside a machine reaches your real files. That is the price of the shared-files model, and the mitigation is offered at create time for exactly that reason: --home-mount ro, or none. If you’re going to point something untrusted at a box, that’s the moment to reach for them.

A container inverts both trade-offs — its home and /data live on the host and survive destroy, so you can destroy one, recreate it on a newer distro release with different CPU and memory, and your dotfiles, keys, shell history and checkouts come back with it. It just has no systemd.

The rest of the sharp edges are documented rather than hidden: IP addresses are DHCP and change across restarts, so use the box name or appbox ip and never hardcode one. Upgrading Apple’s container CLI while the old daemon keeps running breaks networking with a spectacularly unhelpful error — AppBox detects that specific case and offers to restart the service. And id may report an odd group name inside, because your box user takes your Mac gid of 20, which Debian, Ubuntu and Alpine all already call dialout; ownership is numeric and correct, only the label is strange, and renaming a system group would be the worse fix.

Who this is for

If you develop on a Mac and deploy to Linux, the pitch is short: stop testing on an approximation. Build against the same libc, the same package manager and the same init system your production host runs, using the editor and the files you already have open.

If you’re a sysadmin or a homelabber, a box is a scratch machine that costs one command and half a gigabyte. Try the playbook, break the config, destroy, start again — across Ubuntu, Debian, Alpine, Fedora, Rocky and Arch, without a hypervisor UI or a single ISO download. If lxc launch is already muscle memory on your Linux boxes, this is that reflex, on the laptop.

If you’re learning Linux, the case is strongest of all. The reason WSL mattered wasn’t technical; it was that it removed the ceremony between wanting a Linux and having one. That’s what an icon by the clock with a switch beside each box is for.

Closing thought

None of this required writing a hypervisor or forking a container runtime. Apple built the hard part and shipped it for free. What was missing was the last hundred meters — knowing which images actually boot, which packages actually exist, what an unhelpful error message actually means, and doing all of it once so nobody has to do it again. That’s the whole project: not a new virtualization stack, just the difference between a platform capability and something you’d use on a Tuesday.