Everything you need to run Morbstack today.
Prerequisites, the morb CLI, exactly what "drop-in Docker compatible" means right now, and the gotchas that will otherwise cost you an afternoon.
Getting started
Prerequisites
Apple silicon, macOS 26 (Tahoe) or later to run it, Xcode 26 or later to build it (Swift 6.3, Virtualization.framework). See system requirements for the full detail and citations.
Build and first run
The full, real build-from-source walkthrough — clone, mise trust && mise install, mise run build, fetching guest assets, assembling the guest image, and starting the daemon — lives on the Download page, since there is no packaged build to fetch instead. It ends with:
mise run run-daemon # in another terminal: export DOCKER_HOST=unix://$HOME/.morbstack/run/docker.sock docker run --rm hello-world
Connecting the Docker CLI
Morbstack does not register a Docker context or take over the conventional socket path automatically (that's tracked as a gap, not an oversight — see docs/compat.md). Note also that Morbstack does not install a docker client itself today — the commands below assume one is already on your PATH; shipping a bundled, hash-verified client is the "single-path install" gate tracked for the first public release (docs/roadmap.md, L1). Point whatever stock docker client you have at it one of two ways:
# one-off, per shell session export DOCKER_HOST=unix://$HOME/.morbstack/run/docker.sock # or persistent, via a docker context docker context create morbstack --docker host=unix://$HOME/.morbstack/run/docker.sock docker context use morbstack
The CLI also has its own context helper: morb context status / create / use — see the CLI reference below. morb context use always asks before changing your default context, and refuses to replace an existing explicit default without --force.
The Compose plugin
Fetched and hash-verified by scripts/fetch-guest-assets.sh, but not installed into ~/.docker/cli-plugins automatically:
mkdir -p ~/.docker/cli-plugins ln -sf "$(pwd)/dist/host-bin/docker-compose" ~/.docker/cli-plugins/docker-compose docker compose version
Or use morb install-cli-plugins, which symlinks whatever plugin binaries it finds next to the CLI or under dist/host-bin — it always prints its plan and asks first unless --force is passed.
CLI reference
Every command below is read directly from mac/Sources/morb/main.swift's own usage text and argument switch. morb <command> --help prints the canonical version; treat this table as a mirror of it, not a superset.
| Command | Description |
|---|---|
status | Show daemon and VM state, published ports, and any ports that failed to forward. |
start | Boot the VM. |
stop | Shut the VM down. --force skips asking the guest first. |
suspend | Save the VM to disk and free its memory. |
resume | Restore a suspended VM. |
shares | List configured shared host paths and whether the guest actually has them mounted. Never starts a daemon. |
rosetta | Show Rosetta status. rosetta install installs and enables it — always asks first, no --force. |
k8s | Run a local Kubernetes cluster, off by default. |
version | Print CLI and daemon versions. |
doctor | Diagnose host readiness (signing, kernel/initramfs, disk persistence, docker-credentials). Works without the daemon. |
reset-disk | Delete the Docker data disk and start over. Destructive; refuses while the VM is running. |
mcp | Model Context Protocol server, read-only unless granted otherwise. |
migrate | Import images, volumes, and config from another runtime. Never destructive, never writes to another runtime's state. |
bench | Run the open benchmark suite and report the numbers — the morb bench tool referenced in the roadmap's performance targets. |
scan | SBOM and CVE scan an image using optional syft/grype tools you supply yourself (not bundled); building the CVE database requires a network fetch. |
debug | Open a toolbox shell in a container, even a distroless one. |
context | Manage the morbstack docker context — status / create / use. |
install-cli-plugins | Symlink docker-compose/docker-buildx into ~/.docker/cli-plugins. |
Notable subcommands
| Subcommand | What it does |
|---|---|
rosetta install --print-plan | Prints exactly what installing Rosetta would do, and stops — no system change. |
k8s status | Whether the cluster is installed, on, and Ready. |
k8s enable | Installs the payload if needed, then starts the cluster. |
k8s disable | Stops the cluster; the payload and its state are kept. |
k8s kubeconfig | Writes ~/.morbstack/kubeconfig. |
k8s kubeconfig --merge | Merges the morbstack context into ~/.kube/config, after asking, after a timestamped backup. |
context status | Whether the context is registered and current. |
context create | Registers the morbstack docker context. Asks first. |
context use | Makes it the default context. Always asks; refuses to replace another explicit default without --force. |
Global options
--json- Emit raw JSON instead of human-readable output.
--force- Skip Morbstack's confirmation prompt (
reset-disk,context create,install-cli-plugins), replace another explicit default context (context use), or stop the VM without asking the guest first (stop). Deliberately refused byrosetta install— that installs system software under Apple's licence, so it always asks the person at the keyboard. --print-plan- Print what a command would do and exit without doing it (
rosetta install,install-cli-plugins). --help- Print the full usage text.
Compatibility
The contract
From docs/compat.md — the compatibility contract Morbstack commits to, so "does my existing workflow keep working" has a precise answer:
- Unmodified
dockerCLI. Any stockdockerbinary works against Morbstack's relayed Engine API — no patched client, ever. - Docker contexts are respected, never stomped. Morbstack adds itself as a context; it never silently rewrites your existing default.
- Engine API surface = upstream moby, verbatim. No added, removed, or reshaped endpoints — whatever the bundled, unmodified
dockerdversion exposes is what you get.dockerdandcontainerdboth carry no Morbstack patch; published ports reach the Mac through a small userland proxy dockerd execs via its own stock--userland-proxy-pathflag, which is outside the API surface entirely. - Compose v2 and buildx are bundled, from M1 onward. As of today, Compose already works end-to-end with a one-time manual plugin symlink; buildx is not fetched or tested yet at any milestone so far.
~/.docker/config.jsonis honored — credential helpers, the macOS keychain helper, and registry mirrors, the same way stock Docker tooling reads it.
Explicit non-goals
Stated up front so they are not mistaken for gaps to be filled later — from docs/compat.md:
- Windows containers. Never supported — Morbstack targets Linux containers only.
- CRIU (checkpoint/restore). Off, not a target for 1.0 or the 1.x line as currently scoped.
- CUDA / GPU passthrough. Not supported, no commitment to add it.
- Docker Desktop Extensions. Not supported and not planned — Extensions are web-view based, which conflicts with Morbstack's zero-web-view host integration principle. A native plugin SDK is a post-1.0 idea, not a compatibility shim for existing Extensions.
The 29-check parity matrix
Every row below is transcribed directly from docs/parity.md's own Result column — a one-time audit run against a real, cold-booted VM, git HEAD 081aa29. Status is shown as colour, shape, and text together, never colour alone.
Core CLI
| # | Test | Result | Notes |
|---|---|---|---|
| 1 | docker version | PASS | Client 27.4.0 talks to Server 29.7.1 over the relay. Cold VM boot to a usable API: 2.16s. |
| 2 | docker info | PASS | Full, plausible output — overlay2/extfs, cgroup v2, standard drivers listed. |
| 3 | ps/images/pull/run/exec/logs/inspect/cp/stats/events/rm/rmi | PASS | Every one produced the same shape of output real Docker would. |
| 4 | docker system df / prune -f | PASS | Correct reclaimable accounting; a -af reclaimed 2.134GB cleanly. |
Compose
| # | Test | Result | Notes |
|---|---|---|---|
| 5 | 4-service stack: healthchecks, named volume, custom networks | PASS | All services healthy in dependency order; service-name DNS resolved; down -v cleaned up fully. |
| 6 | WordPress + MySQL, persistent volumes | PASS | Both services healthy in ~24s; the install wizard genuinely rendered. |
| 7 | docker compose build | PASS | Multi-stage build ran to completion via the classic (non-BuildKit) builder. |
Bind mounts
| # | Test | Result | Notes |
|---|---|---|---|
| 8 | Fresh content on re-read after a host-side edit | PASS | Every re-read reflected new content immediately. |
| 9 | Single-file mount via an unresolved /tmp/... path | FAIL | Silently creates an empty directory in the guest instead of binding the file. /private/tmp/... works. |
| 10 | inotify across a VirtioFS mount | FAIL | Documented gap: zero restarts after an 8s wait with a plain nodemon watch. |
| 11 | Polling-based watch workaround | PASS | nodemon --legacy-watch picked up the change and restarted correctly. |
Build
| # | Test | Result | Notes |
|---|---|---|---|
| 12 | Multi-stage Dockerfile, cache reuse on rebuild | PASS | Every step CACHED; final image built in 0.44s. |
| 13 | docker build / buildx out of the box | HISTORICAL | The original live audit found no plugin. The signed runtime now bundles Buildx; clean-profile revalidation is still required before this becomes a PASS. |
| 14 | BuildKit itself, once a stock docker-buildx binary is placed manually | PASS | Multi-platform build, cache mounts, all work perfectly once the client binary exists. |
AMD64
| # | Test | Result | Notes |
|---|---|---|---|
| 15 | mysql:5.7 (genuinely single-arch) end to end | PASS | SHA-256 computed inside the emulated container is bit-identical to the Mac's own. |
| 16 | Compute-heavy slowdown ratio | PASS | SHA-256 ~5.1x slower, RSA-2048 sign ~3.8x, verify ~2.3x vs native arm64 — matches the documented range. |
Ecosystem
| # | Test | Result | Notes |
|---|---|---|---|
| 17 | Zero-config socket discovery at conventional paths | HISTORICAL | The audited build lacked discovery. The consented setup transaction now creates it only when safe; clean-profile revalidation remains required. |
| 18 | host.docker.internal resolution | HISTORICAL | The audited build lacked an answer. Guest split DNS is implemented; live revalidation remains required. |
| 19 | gateway.docker.internal resolution | HISTORICAL | Same historical result as #18; the current guest implementation also needs a fresh live run. |
| 20 | --network host | PARTIAL | Works inside the guest; unreachable from the Mac — matches Docker Desktop's own long-standing behavior. |
| 21 | Docker-in-Docker via the bind-mounted socket | PASS | docker:cli talked straight to the guest's real dockerd. |
| 22 | Container-to-host connections | PARTIAL | Possible via the guest VM's own gateway IP, but not exposed as any documented hostname. |
| 23 | docker context integration | PASS | Creation, switching, and use all worked exactly like any other Docker host context. |
Restart / persistence
| # | Test | Result | Notes |
|---|---|---|---|
| 24 | Restart policy survives morb stop + morb start | PASS | stop 10.7s; the container came back Up on its own after start. |
| 25 | Images and named-volume data survive the same cycle | PASS | Both images and the volume's contents were intact after the restart. |
Failure modes
| # | Test | Result | Notes |
|---|---|---|---|
| 26 | Disk full | PASS | Standard dd: No space left on device; the daemon stayed fully responsive afterward. |
| 27 | Port already bound on the Mac | HISTORICAL | The original run succeeded incorrectly. The recognized fixed loopback TCP create/start path now retains a listener and rejects a conflict before the container is reported running; dynamic and nonstandard cases need their own contract. |
| 28 | Image that does not exist | PASS | Byte-identical to real Docker Hub errors. |
| 29 | OOM | PASS | OOMKilled=true, exit code 137 — the standard Docker contract. |
Historical tally: 20 PASS, 3 PARTIAL, 6 FAIL out of 29 checks — counted directly from the original live audit. Rows marked HISTORICAL have a later implementation follow-up but remain pending a fresh live run.
Troubleshooting
docker pull/docker run hangs with no output for minutesRun morb doctor. This is very likely the docker-credentials check: Docker Desktop writes "credsStore": "desktop" into ~/.docker/config.json, and the standard docker CLI shells out to docker-credential-desktop for every registry auth lookup — which only answers while Docker Desktop itself is running. With Desktop absent, the CLI hangs waiting on a process that never answers. Fix: remove the credsStore line from ~/.docker/config.json, or point DOCKER_CONFIG at a directory with a config that omits it. Morbstack never edits that file for you.
Run morb shares. A current Engine request canonicalizes macOS /tmp to /private/tmp, checks the configured share that is actually mounted in the guest, and rejects unshared, missing explicit, or symlink-escaping bind sources with a Docker error before forwarding. The old silent-empty-directory behavior is retained only as historical audit evidence.
VirtioFS bind mounts don't forward inotify events across the host/guest boundary — a file is correct the instant you read it, but the change-notification event a watcher (nodemon, webpack --watch, vite) depends on never arrives. The working mitigation is a polling watcher: nodemon --legacy-watch (or your tool's equivalent) picks up changes correctly. There is no fix short of polling today.
docker ps insists it's publishedFor an ordinary fixed loopback TCP publication, Morbstack now holds the host listener before create and returns a Docker-style conflict before the container is reported running. Dynamic/range ports, unsupported request framing, and VM-stop recovery have their own explicit capability boundaries; Morbstack must reject or diagnose them rather than show a successful-looking unreachable port.
Further reading
The deeper design documents, each a primary source for something summarized above:
architecture.md
How the pieces fit together and why the load-bearing decisions were made that way.
protocol.md
The vsock wire protocols Morbstack speaks between host and guest.
sharing.md
How a directory on your Mac becomes a directory inside the VM, and what to do when it doesn't.
amd64.md
How docker run --platform linux/amd64 works, how to turn it on, and what it doesn't cover.
k8s.md
The local single-node Kubernetes cluster: how it works and what was measured.
compat.md
The full compatibility contract and the CI-enforced ecosystem matrix.
parity.md
The complete 29-check drop-in parity audit this page's matrix is built from.