DPF Install Guide — macOS (Apple Silicon)
This is the end-user install guide for the Open Digital Product Factory on Apple Silicon Macs (M1 / M2 / M3 / M4).
Status: GA — validated on real Apple Silicon hardware.
The macOS installer has been run end-to-end on a real Apple Silicon Mac (M-series, macOS 14+) and troubleshot to a working portal, including the voice (STT + native-host TTS) path. The findings from that validation are folded into the steps and the Troubleshooting section below.
CI still only exercises the
--dry-runpath (themacos-14GHA runner can’t nest-virtualize Docker Desktop), so additional verification reports on other Mac models / macOS versions remain welcome — see Verify your install. Both Windows and macOS Apple Silicon are GA install surfaces today.
For the architectural background, see the installer-parity roadmap and the deployment doctrine.
Supported environment
| Component | Required |
|---|---|
| OS | macOS 14 (Sonoma) or newer |
| Architecture | Apple Silicon (arm64). Intel Macs are not supported. |
| Disk | ~10 GB free (Docker Desktop + multi-arch GHCR images) |
| RAM | 16 GB recommended for the local-LLM tier; 8 GB works with an external LLM_BASE_URL |
The installer refuses to run on unsupported hosts (Intel Mac, older
macOS) unless you pass --force-unsupported-host — see
Preflight refusals.
Prerequisites
The installer auto-installs Docker Desktop. You bring:
- Xcode Command Line Tools —
xcode-select --install(forgit,make,curl). - Node.js 20+ and pnpm — install via
brew install node pnpm,nvm, or your preferred manager. The installer refuses to run with older Node or no pnpm; it does not auto-install Node-runtime tooling (out of scope per Contract 3).
That’s it. No Homebrew dependency for Docker Desktop itself — the
installer downloads the official .dmg directly from desktop.docker.com.
Quick start
Clone the repo and run the installer:
git clone https://github.com/OpenDigitalProductFactory/opendigitalproductfactory ~/dpf
cd ~/dpf
bash install-dpf.sh
The installer is interactive by default and first asks how you want to use DPF:
[1] Ready to go(customer) — runs the pre-built release images. No contributor tooling. This is the default.[2] Customizable(contributor) — builds the full stack from your local source, enables the in-repo git hooks, and runs the agent-toolchain bootstrap so Claude Code / Codex are wired up.
Your choice is saved to ~/.dpf/install-state.json (installMode) and reused
on re-runs without re-prompting. To skip the prompt, pass --customer or
--contributor. For an unattended (CI / scripted) install:
# Customer (release images) is the headless default:
bash install-dpf.sh --headless
# Or pick explicitly:
bash install-dpf.sh --headless --contributor
--customer/--contributor set the compose mode automatically (release vs
source build); --release/--dev still override it if you need to force one.
Contributor: separate dev workspace from install (recommended, BI-0856A4CE Phase 1)
Contributor installs default to single-tree mode — the cloned repo at the
install path doubles as your dev workspace where git worktree add creates
feature branches. That works, but the dev tree can collide with the running
portal and the self-upgrade merge loop (BI-A8A7CCFD).
Pass --dev-workspace-path to register a separate clone as the dev
workspace. The dev-loop scripts (scripts/new-dev-worktree.sh) then branch
new worktrees from THERE instead of the install path, so the production
install is left alone:
# 1. Install (production tree at $REPO_ROOT — the cwd):
bash install-dpf.sh --headless --contributor --dev-workspace-path ~/dpf-dev
# 2. Clone the dev workspace separately (one-time):
git clone git@github.com:OpenDigitalProductFactory/opendigitalproductfactory.git ~/dpf-dev
# 3. From now on, run dev-loop scripts from the dev workspace:
cd ~/dpf-dev
bash scripts/new-dev-worktree.sh my-feature
# → creates ~/dpf-dev-worktrees/my-feature
When --dev-workspace-path is omitted (or resolves to the install path),
single-tree mode persists — current behavior, full back-compat.
What the installer does
- Preflight — refuses to run on Intel Macs / older macOS / WSL2 / rootless Docker / Podman.
~/.dpf/install-state.json— initializes or migrates the install state file (schema-versioned).- Install mode — prompts for customer vs contributor (or honors
--customer/--contributor), saves it toinstall-state.json, and derives the compose mode (customer → release images; contributor → source build) unless--release/--devforces one. - Compose chain — assembles
docker-compose.yml+docker-compose.macos.yml(+docker-compose.release.ymlin release/customer mode). The Edge Node is opt-in: pass--with-edgeto also bundle a localdocker-compose.edge.ymlnode. By default no Edge Node is installed — map a network from another machine instead via Admin > Platform Development > Edge Nodes. - Docker Desktop — installs the
.dmgif missing (hdiutil attach+cp -R /Applications), then starts it and waits for the daemon. - Node / pnpm sanity check — refuses if Node < 20 or pnpm missing.
- Workspace dependencies —
pnpm install. In contributor mode the installer also enables the in-repo.githooks/(git config core.hooksPath .githooks), auto-installs the GitHub CLI (gh) into~/.dpf/tools/bin(no Homebrew/sudo — downloaded from the official release and checksum-verified) and adds it to yourPATH, then runs the agent-toolchain bootstrap so Claude Code / Codex are wired up. It does not sign you in — finish withgh auth login --git-protocol https --web(the OAuth flow, which avoids the fine-grained-PAT lifetime limits some orgs enforce) andgh auth setup-git. Customer mode skips all of this. - Host hardware profile — runs
scripts/detect-hardware-host.tsand emitsDPF_HOST_PROFILE(Apple Silicon reportsarchitecture: "unified"for memory). .envgeneration — only on first install; existing.envis preserved.- Release image availability (customer mode only) — probes the GHCR
portal image and, if it’s gated behind early-access auth, points you at
docker login ghcr.iobefore bring-up. docker compose up -don the macOS overlay.- Health check — polls
http://localhost:3000/api/healthfor up to 5 minutes (configurable viaDPF_HEALTH_TIMEOUT). - Edge Node bootstrap (only with
--with-edge) — mints a single-use auto-approve bootstrap token, writes it to.envasDPF_BOOTSTRAP_TOKEN, restarts theedge-nodecontainer so it enrolls. The new EdgeNode lands directly intrustState=trustedper spec § Approval policy. macOS limitation: the Edge Node runs in bridge mode (Docker Desktop doesn’t honornetwork_mode: hostthe way Linux does), so its discovery output sees the Docker Desktop VM’s interfaces rather than your Mac’s real NICs. That’s a known constraint resolved by the future native macOS Edge Node binary (T3) — until then, the Edge Node here demonstrates the enrollment + heartbeat + submission path but not L2 host-network discovery. - Voice / TTS sidecar (Apple Silicon only) — runs
scripts/tts/setup-chatterbox-tts-macos.shto provision the native-host text-to-speech sidecar (port8771) and wire itsTTS_PROVIDER/DPF_TTS_URL/DPF_TTS_REFERENCE_HOST_ROOTvalues into.env, so spoken output works out of the box. Idempotent and non-fatal (warns and continues if it fails); skipped on Linux/Windows, which use the bundleddpf-ttscontainer. See Voice (STT + TTS). - Persist state — records
lastSuccessfulInstallVersionandlastHealthCheck. - LaunchAgent — installs
~/Library/LaunchAgents/local.dpf-autostart.plistso the stack auto-starts at login (skip with--no-autostart).
Total wall time: ~10 minutes including the AI-model download (varies with model size and connection).
Login
Login credentials are written to .env in the install directory:
- Email:
admin@dpf.local - Password:
ADMIN_PASSWORDin.env(randomly generated on first install). Change it after first login.
Day-to-day
| Task | Command |
|---|---|
| Start the stack | bash dpf-start.sh |
| Stop the stack | bash dpf-stop.sh |
| Tail logs | docker compose -f docker-compose.yml -f docker-compose.macos.yml logs -f |
| Diagnostic bundle | bash install-dpf.sh doctor |
| Wipe + reinstall (destructive) | bash dpf-reinstall.sh |
| Tag + push a release | bash dpf-release.sh --bump minor |
| Soft uninstall (keep data) | bash uninstall-dpf.sh |
| Full uninstall (wipe data) | bash uninstall-dpf.sh --purge |
bash install-dpf.sh --help (and the other scripts) document every flag.
LLM provider
On Apple Silicon, Docker Desktop ships Docker Model Runner, which
hosts the local LLM behind an OpenAI-compatible endpoint at
http://model-runner.docker.internal/engines/v1. The installer
auto-detects this and sets DPF_LLM_PROVIDER=model-runner per the
provider contract.
To use an external endpoint (Anthropic, OpenAI, hosted Ollama, etc.)
instead, set LLM_BASE_URL in .env before re-running the installer:
LLM_BASE_URL=https://api.example.com/v1
DPF_LLM_PROVIDER=external
Voice (STT + TTS)
DPF coworkers support both voice input (speech-to-text) and voice output (text-to-speech). On macOS the two halves are provisioned differently because Docker Desktop can’t reach the Apple Neural Engine.
Speech-to-text (STT) — works out of the box. A bundled speaches
(faster-whisper / distil-whisper) service runs as the dpf-stt
container. It’s CPU-friendly and needs no GPU, so the mic button in the
coworker panel works immediately after install — click it, speak, and
your words land in the message box. STT is identical across macOS,
Linux, and Windows.
Text-to-speech (TTS) — provisioned automatically on Apple Silicon.
Spoken output needs a synthesis engine with hardware acceleration.
Docker Desktop can’t expose the Mac’s Neural Engine to a container, so
on Apple Silicon the TTS engine runs as a native-host sidecar on
the Mac instead of in Docker. The docker-compose.macos.yml overlay is
already wired to talk to it (TTS_PROVIDER=mlx,
DPF_TTS_URL=http://host.docker.internal:8771).
bash install-dpf.sh provisions this sidecar for you — no manual
step. On an Apple Silicon host the installer runs
scripts/tts/setup-chatterbox-tts-macos.sh, which installs a launch
agent so the sidecar restarts at login, listens on port 8771, and
writes the matching TTS_PROVIDER / DPF_TTS_URL /
DPF_TTS_REFERENCE_HOST_ROOT values into .env. A founder seed
voice ships in the install seed (with a consent record), so spoken
output works as soon as the install finishes — no voice recording
required to get started.
Both customer (
bash install-dpf.sh) and contributor (scripts/setup.sh) installs provision the sidecar automatically on Apple Silicon; the step is idempotent and skips cleanly on re-runs. If the sidecar setup fails the install still completes — it just warns and points you at the manual command below. On Linux / Windows hosts TTS uses the bundleddpf-ttsDocker container instead and needs no host-side step.To (re-)provision by hand at any time:
bash scripts/tts/setup-chatterbox-tts-macos.sh
Once the sidecar is running, voice output features — per-profile speed / enthusiasm tuning and sentence-level streaming for low time-to-first-audio — are available in the coworker panel. Real-person voice cloning always requires an explicit consent record; voice never changes approval, confidence, or audit rules.
If the coworker transcribes but won’t speak back, the TTS sidecar isn’t running. Re-run the setup script and confirm the agent loaded:
bash scripts/tts/setup-chatterbox-tts-macos.sh
launchctl list | grep -E 'mlx-tts|chatterbox-tts'
curl -fsS http://localhost:8771/health # sidecar health
Autostart
The installer registers a LaunchAgent at:
~/Library/LaunchAgents/local.dpf-autostart.plist
It invokes a generated launch script (~/.dpf/dpf-autostart.sh) that
embeds the exact compose -f chain captured at install time, so future
overlay edits don’t silently break autostart.
To inspect or disable the agent:
launchctl list | grep dpf-autostart # is it loaded?
launchctl bootout gui/$UID/local.dpf-autostart # stop the agent
rm ~/Library/LaunchAgents/local.dpf-autostart.plist
bash uninstall-dpf.sh removes the agent and stops the stack but
preserves volumes / .env / ~/.dpf state for re-install. --purge
nukes those too.
Preflight refusals
The installer refuses to proceed on configurations that don’t match the supported matrix:
- Intel Mac. Force with
--force-unsupported-host, but Phase 0’s multi-arch GHCR images targetlinux/arm64for Apple Silicon — Intel runs under Rosetta with a hard performance hit. - macOS 13 or older. Older Docker Desktop versions miss
host-gatewayand Model Runner. - Docker Desktop < 4.40. Refused because Model Runner isn’t available; upgrade Docker Desktop first.
- Podman / rootless Docker / Colima. Not on the supported matrix;
the bind-mounts and
host-gatewaymode the platform relies on aren’t validated against these runtimes.
Troubleshooting
Portal didn’t come up after --headless install.
Run bash install-dpf.sh doctor to capture a diagnostic bundle at
~/.dpf/doctor-<timestamp>.tar.gz and check
docker compose -f docker-compose.yml -f docker-compose.macos.yml logs portal --tail 100.
Docker Desktop quarantined the LaunchAgent plist.
Strip the quarantine xattr: xattr -d com.apple.quarantine ~/Library/LaunchAgents/local.dpf-autostart.plist.
The installer attempts this automatically.
Model Runner not detected even though Docker Desktop ≥ 4.40 is
installed.
Open Docker Desktop → Settings → Features in development → enable
“Use Docker Compose v2” and “Docker Model Runner”, then re-run
bash install-dpf.sh.
Install log says “Docker Model Runner isn’t available … skipping the AI
model download”.
Expected on Docker Desktop older than 4.40 (the docker model CLI isn’t
present). The portal installs and runs normally — only the local AI model
is skipped, and the installer no longer leaks a raw docker: 'model' is not
a docker command error. Update Docker Desktop and re-run
bash install-dpf.sh to pull the model, or point the portal at an external
LLM provider under Admin → Providers.
Install log says an “optional sidecar … image is unavailable upstream”.
The bundled voice speech-to-text sidecar (dpf-stt) is pulled from a
third-party registry that occasionally prunes its image tag. When that
happens the installer brings the platform up without voice input rather
than failing the whole install — everything else works. Re-run
bash install-dpf.sh later to pick the image up once it’s available again.
/api/health returns 500.
The portal’s database migrations may not have completed. Tail the
portal-init container:
docker compose -f docker-compose.yml -f docker-compose.macos.yml logs portal-init.
Browser doesn’t open after bash dpf-start.sh.
Pass --no-browser (or set NO_BROWSER=1) for SSH / headless sessions.
The portal is still running at http://localhost:3000.
Uninstall
| Command | What it removes |
|---|---|
bash uninstall-dpf.sh |
LaunchAgent, running containers. Preserves volumes, .env, ~/.dpf state. Re-install resumes cleanly. |
bash uninstall-dpf.sh --purge |
Above + all DPF docker volumes (filtered by the com.docker.compose.project=dpf label so other stacks on the same host are untouched), .env, ~/.dpf. Destructive — irreversible. |
bash uninstall-dpf.sh --purge --keep-env |
Purge but retain .env. |
bash uninstall-dpf.sh --purge --keep-state |
Purge but retain ~/.dpf install history. |
Verify your install
macOS Apple Silicon is GA, but CI still can’t run the installer on real
hardware (the GHA macos-14 runner can’t nest-virtualize Docker
Desktop), so verification reports from additional Mac models / macOS
versions remain valuable for widening the tested matrix. If you ran the
install above, a quick report is appreciated — happy paths and
failures are equally useful.
One-command report (fastest path):
# Already installed — just run the verifier:
bash scripts/verify-install-edge.sh
# Fresh host — bootstrap + verify in one shot:
bash scripts/verify-install-edge.sh --bootstrap
The script captures a ~/.dpf/verify-bundle-<timestamp>.tar.gz with a
host fingerprint, portal health, Prometheus targets, Edge Node lifecycle
log, and a paste-able summary — including macOS-specific checks for
architecture (arm64), LaunchAgent autostart, and Docker Model Runner.
Open a new GitHub issue
titled Install verification — macOS <version> <arch>
(example: Install verification — macOS 14.5 arm64 (M2 Pro)) and
attach the tarball. Secrets in the bundle are redacted automatically.
Manual report (if the verifier itself fails to run):
- Paste the output of:
sw_vers && uname -srm docker --version 2>/dev/null echo "DPF version: $(grep DPF_INSTALLER_VERSION install-dpf.sh)" - Note which steps you completed from the verification runbook §2 and which (if any) failed.
- Attach the doctor bundle:
bash install-dpf.sh doctor # Attach ~/.dpf/doctor-<timestamp>.tar.gz to the issue.
The verification runbook also covers:
- LaunchAgent surviving an actual reboot
- Docker Desktop
.dmginstall flow on a Gatekeeper-fresh machine - Discovery collectors emitting real
pkgutil/brewdata - LLM provider round-trip (Model Runner)
Any subset is useful. We don’t need every checkbox before reading your report — we’ll integrate findings as they arrive.
Going further
- Linux install guide — same platform, different host.
- Installer-parity roadmap
- Deployment doctrine — the 10 canonical contracts every install path wraps.
- CONTRIBUTING.md — for contributing back to the platform.
Connecting Claude Code or VS Code via MCP
Once the platform is running, you can connect Claude Code, Codex CLI, or VS Code to your install’s MCP server at /api/mcp/v1.
Option A — CLI (fastest, no browser needed):
pnpm --filter web exec tsx apps/web/scripts/issue-mcp-token.ts > .mcp.json
This issues a read-only token with the coding-agent scope set and writes a ready-to-paste .mcp.json in one step. Restart Claude Code to pick up the dpf connector.
# VS Code instead:
pnpm --filter web exec tsx apps/web/scripts/issue-mcp-token.ts --format vscode > .vscode/mcp.json
Option B — Admin UI: Log in → Admin > Platform Development > MCP Token Manager → generate a token → paste the displayed snippet into .mcp.json.