Documentation

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.

morb top-level commands
CommandDescription
statusShow daemon and VM state, published ports, and any ports that failed to forward.
startBoot the VM.
stopShut the VM down. --force skips asking the guest first.
suspendSave the VM to disk and free its memory.
resumeRestore a suspended VM.
sharesList configured shared host paths and whether the guest actually has them mounted. Never starts a daemon.
rosettaShow Rosetta status. rosetta install installs and enables it — always asks first, no --force.
k8sRun a local Kubernetes cluster, off by default.
versionPrint CLI and daemon versions.
doctorDiagnose host readiness (signing, kernel/initramfs, disk persistence, docker-credentials). Works without the daemon.
reset-diskDelete the Docker data disk and start over. Destructive; refuses while the VM is running.
mcpModel Context Protocol server, read-only unless granted otherwise.
migrateImport images, volumes, and config from another runtime. Never destructive, never writes to another runtime's state.
benchRun the open benchmark suite and report the numbers — the morb bench tool referenced in the roadmap's performance targets.
scanSBOM and CVE scan an image using optional syft/grype tools you supply yourself (not bundled); building the CVE database requires a network fetch.
debugOpen a toolbox shell in a container, even a distroless one.
contextManage the morbstack docker context — status / create / use.
install-cli-pluginsSymlink docker-compose/docker-buildx into ~/.docker/cli-plugins.

Notable subcommands

morb subcommands
SubcommandWhat it does
rosetta install --print-planPrints exactly what installing Rosetta would do, and stops — no system change.
k8s statusWhether the cluster is installed, on, and Ready.
k8s enableInstalls the payload if needed, then starts the cluster.
k8s disableStops the cluster; the payload and its state are kept.
k8s kubeconfigWrites ~/.morbstack/kubeconfig.
k8s kubeconfig --mergeMerges the morbstack context into ~/.kube/config, after asking, after a timestamped backup.
context statusWhether the context is registered and current.
context createRegisters the morbstack docker context. Asks first.
context useMakes 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 by rosetta 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 docker CLI. Any stock docker binary 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 dockerd version exposes is what you get. dockerd and containerd both carry no Morbstack patch; published ports reach the Mac through a small userland proxy dockerd execs via its own stock --userland-proxy-path flag, 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.json is 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

#TestResultNotes
1docker versionPASSClient 27.4.0 talks to Server 29.7.1 over the relay. Cold VM boot to a usable API: 2.16s.
2docker infoPASSFull, plausible output — overlay2/extfs, cgroup v2, standard drivers listed.
3ps/images/pull/run/exec/logs/inspect/cp/stats/events/rm/rmiPASSEvery one produced the same shape of output real Docker would.
4docker system df / prune -fPASSCorrect reclaimable accounting; a -af reclaimed 2.134GB cleanly.

Compose

#TestResultNotes
54-service stack: healthchecks, named volume, custom networksPASSAll services healthy in dependency order; service-name DNS resolved; down -v cleaned up fully.
6WordPress + MySQL, persistent volumesPASSBoth services healthy in ~24s; the install wizard genuinely rendered.
7docker compose buildPASSMulti-stage build ran to completion via the classic (non-BuildKit) builder.

Bind mounts

#TestResultNotes
8Fresh content on re-read after a host-side editPASSEvery re-read reflected new content immediately.
9Single-file mount via an unresolved /tmp/... pathFAILSilently creates an empty directory in the guest instead of binding the file. /private/tmp/... works.
10inotify across a VirtioFS mountFAILDocumented gap: zero restarts after an 8s wait with a plain nodemon watch.
11Polling-based watch workaroundPASSnodemon --legacy-watch picked up the change and restarted correctly.

Build

#TestResultNotes
12Multi-stage Dockerfile, cache reuse on rebuildPASSEvery step CACHED; final image built in 0.44s.
13docker build / buildx out of the boxHISTORICALThe original live audit found no plugin. The signed runtime now bundles Buildx; clean-profile revalidation is still required before this becomes a PASS.
14BuildKit itself, once a stock docker-buildx binary is placed manuallyPASSMulti-platform build, cache mounts, all work perfectly once the client binary exists.

AMD64

#TestResultNotes
15mysql:5.7 (genuinely single-arch) end to endPASSSHA-256 computed inside the emulated container is bit-identical to the Mac's own.
16Compute-heavy slowdown ratioPASSSHA-256 ~5.1x slower, RSA-2048 sign ~3.8x, verify ~2.3x vs native arm64 — matches the documented range.

Ecosystem

#TestResultNotes
17Zero-config socket discovery at conventional pathsHISTORICALThe audited build lacked discovery. The consented setup transaction now creates it only when safe; clean-profile revalidation remains required.
18host.docker.internal resolutionHISTORICALThe audited build lacked an answer. Guest split DNS is implemented; live revalidation remains required.
19gateway.docker.internal resolutionHISTORICALSame historical result as #18; the current guest implementation also needs a fresh live run.
20--network hostPARTIALWorks inside the guest; unreachable from the Mac — matches Docker Desktop's own long-standing behavior.
21Docker-in-Docker via the bind-mounted socketPASSdocker:cli talked straight to the guest's real dockerd.
22Container-to-host connectionsPARTIALPossible via the guest VM's own gateway IP, but not exposed as any documented hostname.
23docker context integrationPASSCreation, switching, and use all worked exactly like any other Docker host context.

Restart / persistence

#TestResultNotes
24Restart policy survives morb stop + morb startPASSstop 10.7s; the container came back Up on its own after start.
25Images and named-volume data survive the same cyclePASSBoth images and the volume's contents were intact after the restart.

Failure modes

#TestResultNotes
26Disk fullPASSStandard dd: No space left on device; the daemon stayed fully responsive afterward.
27Port already bound on the MacHISTORICALThe 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.
28Image that does not existPASSByte-identical to real Docker Hub errors.
29OOMPASSOOMKilled=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 minutes

Run 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.

My bind mount is empty, or a single-file mount reads as a directory

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.

My dev server's hot reload doesn't fire on a host-side edit

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.

A published port answers nothing, and docker ps insists it's published

For 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.