Chapter 33
Bundling doz in an app
If you want Dozable on your Mac, install the doz command with Homebrew (Installing, upgrading and uninstalling).
On this page
Most people do not need this page. If you want Dozable on your Mac, install the doz command with Homebrew (Installing, upgrading and uninstalling). This page is for people who build a Mac app that runs sandboxes through Dozable, and carries doz inside the app to do it.
Every release of doz comes in two flavours, built from the same source at the same version:
| file on the releases page | what it is | who it's for |
|---|---|---|
doz-VERSION-macos-arm64.tar.gz | the command-line release — what Homebrew installs | people |
doz-VERSION-macos-arm64-helper.tar.gz | the helper build — the same host, made to live inside an app | app authors |
Each has its .sha256 beside it, and a .dSYM.zip with its symbols (so a crash report from it can be symbolicated).
Concepts#
- The host is the
dozprocess that owns a store's virtual machines and answers on<store>/host.sock. The helper build runs the very same host as the command-line release, speaking the same protocol: an app talks to it over that socket, as thedozcommand itself does. See the host and its protocol (the section "The doz host": JSON lines, one request, progress events, one answer). - The helper build is that host with the parts a person uses at a terminal left out. The app is its interface.
- The layout contract is the folder an app copies whole into its bundle. Within a minor version (all of
0.37.x, say) nothing in it moves or is renamed.
How the helper build differs#
| command-line release | helper build | |
|---|---|---|
the host, host.sock, its protocol, every sandbox operation | ✓ | ✓ the same |
read-only commands (doz ls, doz inspect, doz doctor, doz host status, doz --version) | ✓ | ✓ |
the web dashboard (doz ui, doz ui --lan) | ✓ | — not built in: doz ui says it is "not in this build" |
the setup wizards (doz onboard, doz init) and doz try | ✓ | — not built in (the same answer) |
updates itself (doz upgrade, the daily check, updates.mode) | ✓ | — never: doz upgrade refuses, and it never looks for updates. Your app updates it |
anonymous usage statistics and doz signup | ✓ (you can turn them off) | — never: not built in, so it sends nothing. What your app sends is your app's choice |
RELEASE file says | public | helper |
the host's status reports flavour | cli | helper |
| the pinned Linux kernel inside | ✓ | ✓ |
stripped, with its symbols published as a .dSYM.zip | ✓ | ✓ |
signed with Dozable's Developer ID (team KJ8QMLWB97), notarised, the virtualization entitlement | ✓ | ✓ the same |
Both carry the pinned Linux kernel, so a new Mac's first sandbox needs no kernel download (it used to fetch a release archive of about 0.6 GB to keep 16 MB of it). The first start of a store copies the kernel into the store and checks it; nothing is downloaded from kata-containers.
Get it and verify it#
From the project's GitHub releases page (dozable/dozable), take the helper tarball and its .sha256, then:
shasum -a 256 -c doz-VERSION-macos-arm64-helper.tar.gz.sha256
tar -xzf doz-VERSION-macos-arm64-helper.tar.gz
codesign --verify --strict doz-VERSION-helper/libexec/doz/doz
codesign -dv doz-VERSION-helper/libexec/doz/doz 2>&1 | grep TeamIdentifier=KJ8QMLWB97Pin the version AND the sha256 in your build: the helper runs with the virtualization entitlement, so a tarball whose checksum or team does not match must stop your build.
The layout contract#
doz-VERSION-helper/
├── README.md what this is, and a link to this page
└── libexec/doz/ copy this folder WHOLE into your app
├── doz the program (signed, with the virtualization entitlement)
├── VERSION its version, one line
├── RELEASE helper
├── DozableKit_DozableKit.bundle/ the small Linux programs each sandbox uses (deckhold, doznet, dozview, tmux)
├── kernels/vmlinux-6.18.15-186 the pinned Linux kernel (GPL-2.0, unmodified)
└── licences/ LICENSE, NOTICE, THIRD-PARTY-LICENSES.txt; kernel/: the kernel's source offerdoz finds everything beside its own (resolved) executable, so the folder works wherever it lands, as long as it stays whole. The command-line release has the same libexec/doz/ plus bin/doz (a link) and the dashboard's bundle, DozableKit_DozableWeb.bundle.
Rules for an app that carries it#
- Copy
libexec/doz/whole toYourApp.app/Contents/Resources/Helpers/doz/. NotContents/MacOS/: the host starts itself again from its own executable, and must find its parts beside it. - Never re-sign it. Its signature carries the virtualization entitlement; your app does not need that entitlement at all. Sign your app WITHOUT
--deep(which would re-sign the helper and drop the entitlement). - Replace it by rename, never in place. Copy the new folder beside the old one and rename it over: a host still running the previous copy keeps its own file. Overwriting a running host's program makes macOS refuse its VMs.
- Give it its own store and its own settings. Pass
--store(or setDOZ_STORE) to a folder of your app's (~/Library/Application Support/<your bundle id>/doz-store, say), and setXDG_CONFIG_HOMEto a folder of your app's, so it never shares a store ordoz.tomlwith a person's owndoz. Keep the store's path short: the socket path<store>/host.sockmust fit in 103 bytes. - One host per store. The host holds
<store>/host.lockfor its whole life. Start it withdoz host start --store DIR: it starts one detached (or finds the one already running) and answers with its status. While the lock is held, a host is there or is exiting: wait forhost.sockto answer rather than start another process. - Check whom you are talking to. The status the host answers (the
pingrequest, ordoz host status --json) carriesversion,protocolVersionandflavour. Refuse a host whose version is not the one you carry, or whoseflavouris nothelper(a person's command-linedozpointed at your store), and say so. - Updates are yours. Ship a new helper with a new version of your app. The helper never replaces itself.
A minimal handshake over the socket:
→ {"v":1,"op":"ping"}
← {"ok":true,"result":{"version":"0.37.41","protocolVersion":1,"flavour":"helper","pid":4242,…}}The kernel and its source#
Both tarballs carry Linux vmlinux-6.18.15-186 exactly as kata-containers 3.28.0 builds it, checked against its sha256 on every start. It is GPL-2.0: its provenance and Dozable's written offer of its source are in the tarball (libexec/doz/licences/kernel/) and in the repository (Guest/kernel/PROVENANCE.md). If you redistribute the helper, keep licences/ with it — it is part of the layout for that reason.
Settings#
The helper reads the same settings as the command-line release (see Settings reference), from $XDG_CONFIG_HOME/doz/doz.toml. updates.mode and updates.channel have no effect: it never updates itself.
Troubleshooting#
| symptom | what to do |
|---|---|
"cannot boot a VM", or doz doctor says the entitlement is missing | The helper was re-signed (an app signed with --deep?). Copy it again from the tarball. |
doz upgrade says "this doz belongs to …" | That is the helper build: the app that carries it updates it. |
doz ui says it is "not in this build" | The helper has no dashboard. The doz command from Homebrew has it. |
| A wake fails with "this host's program was updated underneath it" | The helper's file was overwritten while its host ran. Replace it by rename (rule 3); doz host stop, then retry. |
- ← PreviousClaude Code with other models — a gateway, or a model server on your Mac
- Next →Sessions on this Mac
Edit this page on GitHub · Chapter 33 of 34