Skip to content

macOS Installation Guide ​

Step-by-step guide for installing plexd on a Mac, where it runs as a launchd daemon.

Prerequisites ​

  • A Mac on Apple silicon or Intel. CI runs the test suite on macOS 26 arm64; the amd64 binary is cross-compiled and not exercised there.
  • An administrator account. plexd runs as root: creating a utun device, programming routes and loading a pf anchor are all privileged.
  • Network connectivity to the control plane API.
  • A bootstrap token from the control plane.
  • An untouched /etc/pf.conf, if policy enforcement stays on. plexd loads its rules into the anchor com.apple/plexd, which only works while the main ruleset still references anchor "com.apple/*" and nat-anchor "com.apple/*". Apple's default file does.

Quick start ​

sh
curl -fsSL https://get.plexsphere.com/install.sh | sudo sh -s -- \
  --token <YOUR_BOOTSTRAP_TOKEN> \
  --api-url https://api.plexsphere.com

This downloads plexd-darwin-<arch>, verifies its checksum, runs plexd install, and bootstraps the daemon into launchd.

Install script flags ​

FlagDescription
--token VALUEBootstrap token for enrollment
--api-url URLControl plane API URL
--version VERSIONVersion to install (default: latest)
--no-startDon't start the daemon after install

The script does not create the plexd and plexd-secrets groups on macOS, which has no groupadd. It warns and carries on; see Verification.

Manual installation ​

1. Download and verify the binary ​

sh
# Apple silicon
curl -fsSL -o /tmp/plexd https://artifacts.plexsphere.com/plexd/latest/plexd-darwin-arm64

# Intel
curl -fsSL -o /tmp/plexd https://artifacts.plexsphere.com/plexd/latest/plexd-darwin-amd64

chmod +x /tmp/plexd

Verify the checksum:

sh
curl -fsSL -o /tmp/checksums.sha256 https://artifacts.plexsphere.com/plexd/latest/checksums.sha256

EXPECTED=$(grep ' plexd-darwin-arm64$' /tmp/checksums.sha256 | awk '{print $1}')
ACTUAL=$(shasum -a 256 /tmp/plexd | awk '{print $1}')
[ "$EXPECTED" = "$ACTUAL" ] && echo "checksum ok" || echo "CHECKSUM MISMATCH"

Every release binary also carries a Sigstore bundle. Verifying it needs cosign and proves the binary came out of this repository's release workflow:

sh
curl -fsSL -O https://artifacts.plexsphere.com/plexd/latest/plexd-darwin-arm64.sigstore.json

cosign verify-blob --bundle plexd-darwin-arm64.sigstore.json \
  --certificate-identity-regexp '^https://github\.com/plexsphere/plexd/\.github/workflows/release\.yml@refs/tags/v.+$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  /tmp/plexd

Those two values are the defaults upgrade.signing_identity_regexp and upgrade.signing_issuer carry, which is what plexd itself checks before an in-place upgrade.

Gatekeeper

A binary fetched with curl carries no com.apple.quarantine attribute and runs as it is. One saved by a browser does; clear it with xattr -d com.apple.quarantine /tmp/plexd.

2. Install as a launchd daemon ​

sh
sudo /tmp/plexd install --token <YOUR_BOOTSTRAP_TOKEN> --api-url https://api.plexsphere.com

This creates:

PathDescription
/usr/local/bin/plexdplexd binary
/Library/Application Support/plexd/Configuration directory
/Library/Application Support/plexd/config.yamlService configuration
/Library/Application Support/plexd/bootstrap-tokenBootstrap token (0600)
/Library/Application Support/plexd/hooks/Hook scripts directory
/Library/Application Support/plexd/data/Data directory (0700)
/var/run/plexd/Runtime directory
/Library/LaunchDaemons/com.plexsphere.plexd.plistLaunchDaemon definition, root:wheel
/Library/Logs/plexd/plexd.logDaemon stdout and stderr
/etc/newsyslog.d/com.plexsphere.plexd.confLog rotation rule

The install registers the daemon; it does not start it.

3. Start the daemon ​

sh
sudo launchctl bootstrap system /Library/LaunchDaemons/com.plexsphere.plexd.plist

Automated installation ​

Pre-provision the token before running the installer, then leave --token off:

sh
sudo mkdir -p "/Library/Application Support/plexd"
printf '%s' "<TOKEN>" | sudo tee "/Library/Application Support/plexd/bootstrap-token" >/dev/null
sudo chmod 600 "/Library/Application Support/plexd/bootstrap-token"

launchd has no EnvironmentFile, so PLEXD_* overrides do not work the way they do under systemd. Put the settings in config.yaml, or add an EnvironmentVariables dict to the plist by hand.

Verification ​

Check the daemon ​

sh
sudo launchctl print system/com.plexsphere.plexd

Expect a line reading state = running.

Check plexd ​

sh
sudo plexd status

sudo is needed because the node API socket is owner-only until the plexd group exists. macOS keeps groups in Open Directory and nothing creates them for you; the local node API guide has the dscl and dseditgroup commands, and the daemon must be restarted afterwards for the new mode to apply.

View logs ​

sh
plexd logs -f

That tails /Library/Logs/plexd/plexd.log, which is where launchd sends the daemon's output. Reading the file directly works too:

sh
tail -f /Library/Logs/plexd/plexd.log

A plexd up started by hand in a terminal writes to that terminal instead, so the file stays empty on a host where the daemon was never installed.

Uninstall ​

sh
sudo plexd uninstall

This boots the daemon out, removes the plist, the newsyslog rule and the binary, and keeps /Library/Application Support/plexd/.

sh
sudo plexd uninstall --purge

This removes the configuration and data directories as well.

Troubleshooting ​

The daemon restarts every five seconds ​

launchd has no counterpart to systemd's StartLimitBurst, so a daemon that exits on a configuration error is restarted forever. Read /Library/Logs/plexd/plexd.log for the reason, fix the configuration, then:

sh
sudo launchctl kickstart -k system/com.plexsphere.plexd

To stop it entirely while you work:

sh
sudo launchctl bootout system/com.plexsphere.plexd

wireguard setup failed, continuing without WireGuard ​

The log carries creating a utun device requires root. plexd was started without root, so no tunnel was built and the agent is running without a mesh. The launchd daemon runs as root; a hand-started plexd up needs sudo.

plexd exits before it registers, naming pf ​

policy enforcement needs root and a pf main ruleset that references anchor "com.apple/*"

plexd checks that it can enforce policy before it spends its one-shot bootstrap token. Either the process is not root, or /etc/pf.conf no longer references the wildcard anchors. Restore the file and reload it:

sh
sudo pfctl -f /etc/pf.conf

To run the node without enforcement instead, set policy.enabled: false in config.yaml.

launchctl bootstrap refuses the plist ​

launchd loads a daemon only when its plist is owned by root:wheel and writable by nobody else. That is what sudo plexd install produces; a plist copied into place by hand may not be.

sh
sudo chown root:wheel /Library/LaunchDaemons/com.plexsphere.plexd.plist
sudo chmod 644 /Library/LaunchDaemons/com.plexsphere.plexd.plist

An ssh session client times out ​

The control plane got the session_started row, so the listener is up, but the client never connects. The node's pf policy ends in a default deny and allows no inbound mesh TCP to the node, so it drops the client's connection attempt. plexd opens no port itself.

Add a policy rule that allows the client's mesh traffic to the node, or set policy.enabled: false. See What the rules govern and the SSH Sessions deployment notes.

See also ​