DPF Install Guide — macOS (Apple Silicon)

This is the end-user install guide for the Open Digital Product Factory on Apple Silicon Macs (M1 or later).

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-run path (the macos-14 GHA 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 for the application images; buy at least 1 TB, or 2 TB when local models will run on the host.
RAM 32 GB recommended with an external AI provider; 64 GB practical for local-first use; choose 128 GB for larger models or combined operations and development.

The installer refuses to run on unsupported hosts (Intel Mac, older macOS) unless you pass --force-unsupported-host — see Preflight refusals.

Choose the profile that matches where primary AI inference will run:

Deployment model Recommended Apple Silicon configuration Best fit
Provider-assisted operations 32 GB unified memory, 1 TB SSD Routine DPF operations using approved external AI providers
Local-first operations 64 GB unified memory practical; 128 GB for larger models or combined operations and development; 2 TB SSD Local tool-using coworkers, larger models, and longer context
Contributor/development workstation 128 GB unified memory, 4 TB SSD Operations plus source work, builds, tests, browser automation, and local model evaluation

Apple Silicon lets the CPU and GPU share one memory pool. DPF’s planning rule reserves 25% for macOS and the application stack, leaving about 48 GB on a 64 GB Mac and 96 GB on a 128 GB Mac for local AI workloads. Memory cannot be upgraded after purchase, so buy 128 GB when the Mac must handle larger local models or serve as both the DPF host and a development workstation.

See Choosing Hardware for DPF for current Mac and Windows machine examples, NVIDIA speed tradeoffs, model sizing, and purchase checks.

For a compact business host, consider a Mac mini with M6, 32 GB unified memory and a 1 TB SSD for provider-assisted operations, or a Mac mini with M5 Pro, 64 GB unified memory and a 2 TB SSD for local-first operations. These are planning configurations, not exact-model certifications; validate the actual business workload and any enabled build workload before purchase. Embedded Build Studio supports governed changes on customizable installs without separate Claude or Codex apps, but builds, tests, previews, and local coding models need additional capacity wherever they execute.

For larger local models or document-heavy professional work, including law-firm workloads, evaluate a Mac Studio with M5 Max, 128 GB unified memory and a 2–4 TB SSD. Apple offers 128 GB with the 40-core GPU configuration. Choose an M5 Ultra only when measured model capacity or throughput needs justify it. See the hardware guide for sizing and data-handling checks.

Prerequisites

Docker memory during installation

The installer sizes local Docker Desktop memory before dependency and image work: one quarter of physical RAM rounded down to a GiB, with an 8 GiB floor and 32 GiB cap. For example, a 64 GiB Mac gets a 16 GiB upper allocation and a 128 GiB Mac gets 32 GiB. A saved allocation already between 8 GiB and that upper allocation is preserved. Hosts below 16 GiB are refused. Native AI models share the Mac’s memory and need their own capacity allowance.

Changing the allocation requires a Docker Desktop restart, which interrupts all Docker containers and host model runner work. The installer asks permission when containers are running or Docker AI is enabled. Unattended installation requires --allow-docker-restart for that interruption; --headless alone uses the default answer of no. Empty setup with Docker AI disabled converges automatically. Previously running containers are restored, including containers whose restart policy is no.

The installer checks both the saved limit and the running VM, so a saved change that has not taken effect also requires restart. It backs up Docker’s settings before replacing the allocation and reports the backup path. Malformed, symlinked, missing or administrator-managed settings stop setup before shutdown. Remote engines and non-Desktop runtimes are untouched; dry-run only describes the plan. The Docker settings documentation describes the settings file and VM memory limit.

This applies to new installations and explicit installer re-runs using the updated source. It does not silently resize existing installations during normal portal upgrades. Restarting Docker normally keeps its saved allocation.

The installer auto-installs Docker Desktop. You bring:

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:

Office document conversion works in both modes with no extra step. A Customizable install does not build the converter image: the portal pins the dpf-doctools image published for the release your clone descends from, by digest, and pins it again after every upgrade. Only when no published image can be reached (offline or air-gapped) does it build Dockerfile.doctools from your clone instead.

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

  1. Preflight — refuses to run on Intel Macs / older macOS / WSL2 / rootless Docker / Podman.
  2. ~/.dpf/install-state.json — initializes or migrates the install state file (schema-versioned).
  3. Install mode — prompts for customer vs contributor (or honors --customer/--contributor), saves it to install-state.json, and derives the compose mode (customer → release images; contributor → source build) unless --release/--dev forces one.
  4. Compose chain — assembles docker-compose.yml + docker-compose.macos.yml (+ docker-compose.release.yml in release/customer mode). The Edge Node is opt-in: pass --with-edge to also bundle a local docker-compose.edge.yml node. By default no Edge Node is installed — map a network from another machine instead via Admin > Platform Development > Edge Nodes.
  5. Docker Desktop — installs the .dmg if missing (hdiutil attach + cp -R /Applications), then starts it and waits for the daemon.
  6. Node / pnpm sanity check — refuses if Node < 20 or pnpm missing.
  7. 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 your PATH, then runs the agent-toolchain bootstrap so Claude Code / Codex are wired up. It does not sign you in — finish with gh auth login --git-protocol https --web (the OAuth flow, which avoids the fine-grained-PAT lifetime limits some orgs enforce) and gh auth setup-git. Customer mode skips all of this.
  8. Host hardware profile — runs scripts/detect-hardware-host.ts and emits DPF_HOST_PROFILE (Apple Silicon reports architecture: "unified" for memory). Model selection reads the shared scripts/installer/local-model-policy.json policy. A 32 GB unified Mac selects the curated ai/qwen3-coder tier; fresh installs do not auto-provision mutable third-party model references.
  9. .env generation — only on first install; existing .env is preserved. A re-run only adds generated secrets the file is missing: DPF_GIT_WEBHOOK_SECRET, the GPP permit signing key DPF_GPP_PERMIT_SECRET, the reach-link and delegation-receipt signing keys DPF_ATTENTION_REACH_SECRET and DPF_DELEGATION_RECEIPT_SECRET, the self-upgrade target-binding and delivery-task cursor signing keys DPF_SELF_UPGRADE_TARGET_BINDING_SECRET and DPF_DELIVERY_TASK_CURSOR_SECRET (so AUTH_SECRET no longer signs any of them; anything signed with AUTH_SECRET before the upgrade keeps verifying until 9 November 2026), and the Inngest INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY. A real value already set is never rotated. The installer also replaces an Inngest key that still holds the old public default, because anyone who knows the signing key can start background jobs. A self-upgrade does the same for an install that is never re-installed.
  10. 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.io before bring-up.
  11. HTTPS at the canonical address — the install becomes its own certificate authority and serves the portal over https at one address: https://localhost when it serves only this machine, or the machine’s DNS name when the network resolves it here. It writes PUBLIC_URL and trusts the certificate for your user (macOS asks for your keychain password once). AI clients such as Claude Code sign in with OAuth only over https. It also saves the AI client address (DPF_MCP_URL) and the certificate bundle (NODE_EXTRA_CA_CERTS) in ~/.dpf/agent-toolchain.env, loaded by your shell profile, on every run; apps opened from the Dock see the same values until the Mac restarts. If this step fails, the portal stays at http://localhost:3000 and the installer says so.
  12. docker compose up -d on the macOS overlay.
  13. Health check — polls http://localhost:3000/api/health for up to 5 minutes (configurable via DPF_HEALTH_TIMEOUT).
  14. Edge Node bootstrap (only with --with-edge) — mints a single-use auto-approve token and installs the checksum-verified Go Edge Node as local.dpf-edge-node, a launchd-supervised host process. It enrolls as a trusted native node and owns the Mac’s physical multicast interfaces, so nearby DPF discovery is no longer trapped inside Docker Desktop’s Linux VM. The installer uses the active private IPv4 portal address by default. Discovery works over HTTP; automatic pairing remains blocked until DPF_LAN_AUTHORITY_URL names a certificate-valid HTTPS origin trusted by the other installation. That certificate is necessary but not sufficient: an install pairs a peer automatically only when the chain validates against the pinned organization root, this install has completed an organization join import, both installs name the same estate, and the relationship is same-organization. Otherwise pairing proceeds through the short code both operators confirm, and the screen names the condition that could not be proved.
  15. Voice / TTS sidecar (Apple Silicon only) — runs scripts/tts/setup-chatterbox-tts-macos.sh to provision the native-host text-to-speech sidecar (port 8771) and wire its TTS_PROVIDER / DPF_TTS_URL / DPF_TTS_REFERENCE_HOST_ROOT values 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 bundled dpf-tts container. See Voice (STT + TTS).
  16. Persist state — records lastSuccessfulInstallVersion and lastHealthCheck.
  17. LaunchAgent — installs ~/Library/LaunchAgents/local.dpf-autostart.plist so 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:

Network exposure after install

A default install listens on this machine only. Every published port (the portal on 3000 and 1455, postgres 5432, redis 6379, inngest 8288, the sandbox on 3035) binds to 127.0.0.1, so nothing on your LAN can reach the portal or its admin login. This is set by one value in the install’s .env:

DPF_HOST_BIND_ADDRESS=127.0.0.1

To serve the LAN deliberately, set it to 0.0.0.0 and run docker compose up -d again. An install created before this key existed keeps the exposure it already had: the upgrade writes 0.0.0.0 into its .env and says so in a comment, so LAN access does not vanish silently. Remote access without LAN exposure goes through PUBLIC_URL and the edge path.

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. After a model pull is verified, first boot discovers that model and enables both the local provider and its routing connection automatically; no provider toggle is required before using an AI coworker.

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) — connect a provider. DPF ships no speech container. Voice input becomes available as soon as you configure a provider that can transcribe, under Platform Tools → Communications. Any provider serving an OpenAI-compatible /v1/audio/transcriptions endpoint works, including OpenAI and Groq. If you would rather audio never left your own machine, run your own speech server — speaches and whisper.cpp server are both MIT-licensed — and give DPF its address under the self-hosted speech provider. DPF stopped bundling one so that a third party’s registry housekeeping could no longer block platform releases. This 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 bundled dpf-tts Docker 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:

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.

/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):

  1. Paste the output of:
    sw_vers && uname -srm
    docker --version 2>/dev/null
    echo "DPF version: $(grep DPF_INSTALLER_VERSION install-dpf.sh)"
    
  2. Note which steps you completed from the verification runbook §2 and which (if any) failed.
  3. Attach the doctor bundle:
    bash install-dpf.sh doctor
    # Attach ~/.dpf/doctor-<timestamp>.tar.gz to the issue.
    

The verification runbook also covers:

Any subset is useful. We don’t need every checkbox before reading your report — we’ll integrate findings as they arrive.

Going further

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.