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:
- Most images cannot boot as a machine at all. A machine runs the image’s init, and
ubuntu:latesthas no/sbin/init. The machine gets created and then dies with “no PID data from sync pipe” — a message that tells you nothing about the cause. (The package that provides it on Debian and Ubuntu issystemd-sysv, notsystemd. Alpine is the only distro we offer that boots as shipped, on BusyBox init.) - The account you get is unfurnished. No
sudo, no bash login shell, no dotfiles of any kind. - The image is a base image. A fresh Ubuntu has no
curl, nogit, novim, and — the one that gets everybody — noping.
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.
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.
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 system | the image’s own — systemd where it has one | none (sleep infinity) |
| Services | systemctl works, survives restart | start daemons by hand |
| Your Mac home | mounted at /Users/<you> | not mounted |
| Private per-box home | no — dies with the machine | yes, kept on the host |
/data volume | no | yes, kept on the host |
Survives destroy | nothing — but your Mac home is untouched | home and /data |
| Needs | container 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:
| Concept | LXD / Incus | AppBox |
|---|---|---|
| Full-OS, persistent instance | system container | machine (default) |
| Single-process instance | application container | container |
| Create | lxc launch ubuntu:24.04 dev | appbox create-ubuntu dev 24.04 |
| Interactive shell | lxc shell dev | appbox shell dev |
| One-off command | lxc exec dev — cmd | appbox exec dev cmd |
| Lifecycle | lxc start|stop|delete | appbox start|stop|destroy |
| Share host files | disk device, plus an idmap | your home, mounted automatically |
| Runs on | Linux | macOS 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.
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.
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 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.
fedora · rocky · arch
arm64-verified
+ standard toolset
+ wait for boot
account at your uid
dotfiles · your home
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 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.
The prompt says it all: daver@test:/Users/daver. Same user, same path, same files — running on Linux.
Five minutes from nothing
- Install Apple’s
container— the signed.pkgfrom its releases page. There is no Homebrew cask. AppBox drives this tool; it is not optional. - 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.
- Install the CLI — menu bar icon → gear → Install, which symlinks the
appboxbinary from inside the bundle onto yourPATH. Do it after moving the app, since the symlink points at where the app was at install time. The installer lists everyappboxon yourPATHin resolution order, so a shadowed install is visible rather than mysterious — and anything in the way is renamed toappbox.previous, never deleted. - Create a box —
appbox create-ubuntu dev 26.04, or the New Box window. Name and version may be given in either order. - Work —
appbox 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
| Capability | WSL 2 | LXD / Incus | Docker Desktop | Full VM | AppBox |
|---|---|---|---|---|---|
| Named, persistent Linux you switch on | yes | yes | no — images, not machines | yes | yes |
| Real init, services survive restart | yes | yes | no | yes | yes — systemd |
| Host files shared, not copied | /mnt/c | disk devices | bind mounts | shared folders, with friction | home, same path |
| Host uid/gid ownership matches | metadata opts | idmap config | a recurring nuisance | no | yes, by construction |
| Usable in seconds after create | yes | yes | image-dependent | no — full OS install | yes, after first per-distro build |
| Tools present on first login | distro defaults | distro defaults | no — base images | yes | ~28 pkgs + sudo, bash, dotfiles |
| Multiple distros side by side | yes | yes | yes | one VM each | six, one command each |
| Snapshots, profiles, clustering | no | yes | no | snapshots only | no |
| Idle cost | low | low — one daemon | daemon + license | a whole VM | low |
| GUI apps, drive-wide mapping, host/guest interop | yes (WSLg) | no | no | yes | no |
| Runs on | Windows | Linux | anywhere | anywhere | Apple Silicon, macOS 15+ |
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.