macOS Setup
This guide covers running PizzaPi as a persistent background service on macOS so the runner starts automatically on login, survives reboots, and has access to the macOS keychain for gh, SSH keys, and other credentials.
Why a LaunchAgent?
Section titled “Why a LaunchAgent?”macOS has two ways to run background services via launchd:
| Type | Location | Runs as | Keychain access | GUI access |
|---|---|---|---|---|
| LaunchAgent | ~/Library/LaunchAgents/ | Current user, inside login session | ✅ Yes | ✅ Yes |
| LaunchDaemon | /Library/LaunchDaemons/ | System-level (can specify user) | ❌ No | ❌ No |
Always use a LaunchAgent for PizzaPi. The runner needs keychain access for:
ghCLI — GitHub authentication tokens are stored in the login keychain- SSH keys — if your keys use a passphrase managed by the macOS keychain agent
- API credentials — any tool that stores secrets via
security add-generic-password
A LaunchDaemon runs outside your login session, so the keychain is locked and inaccessible — any tool that needs it (like gh auth) will fail with errSecInteractionNotAllowed.
Setting Up the LaunchAgent
Section titled “Setting Up the LaunchAgent”-
Install PizzaPi and connect to a relay
Section titled “Install PizzaPi and connect to a relay”If you haven’t already, install the CLI and run setup:
Terminal window npm install -g @pizzapi/pizzapizzapi setupSee the Quick Setup guide if this is your first time.
-
Find your binary paths
Section titled “Find your binary paths”/Users/yourname/.bun/bin/bun which bunwhich pizza# /Users/yourname/.bun/bin/pizzaNote these paths — you’ll need them for the plist file.
-
Create the LaunchAgent plist
Section titled “Create the LaunchAgent plist”Terminal window mkdir -p ~/Library/LaunchAgentsCreate
~/Library/LaunchAgents/com.pizzapi.runner.plistwith the following content. Replace the paths and environment variables with your own:<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.pizzapi.runner</string><key>ProgramArguments</key><array><string>/Users/yourname/.bun/bin/bun</string><string>/Users/yourname/.bun/bin/pizza</string><string>runner</string></array><key>EnvironmentVariables</key><dict><key>PIZZAPI_API_KEY</key><string>your-api-key-here</string><key>PIZZAPI_RELAY_URL</key><string>https://your-relay-server.example.com/</string><key>PATH</key><string>/Users/yourname/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string></dict><key>WorkingDirectory</key><string>/Users/yourname</string><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>/Users/yourname/.pizzapi/logs/runner.log</string><key>StandardErrorPath</key><string>/Users/yourname/.pizzapi/logs/runner-error.log</string></dict></plist> -
Create the log directory
Section titled “Create the log directory”Terminal window mkdir -p ~/.pizzapi/logs -
Load the agent
Section titled “Load the agent”Terminal window launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist -
Verify it’s running
Section titled “Verify it’s running”Terminal window launchctl list | grep pizzapi# 12345 0 com.pizzapi.runner# Check the logstail -20 ~/.pizzapi/logs/runner.logA
0in the second column means the service started successfully. The runner should now appear in your relay’s web UI.
Managing the Service
Section titled “Managing the Service”Stop the runner
Section titled “Stop the runner”launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plistRestart the runner
Section titled “Restart the runner”launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plistlaunchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plistView logs
Section titled “View logs”# Stdouttail -f ~/.pizzapi/logs/runner.log
# Stderrtail -f ~/.pizzapi/logs/runner-error.logCheck status
Section titled “Check status”launchctl list | grep pizzapiThe output columns are: PID, exit code, label. A - for PID means the service isn’t currently running.
Plist Reference
Section titled “Plist Reference”| Key | Required | Description |
|---|---|---|
Label | ✅ | Unique identifier for the service |
ProgramArguments | ✅ | Command to run — must be full paths (no ~ expansion) |
EnvironmentVariables | ✅ | Must include PIZZAPI_API_KEY, PIZZAPI_RELAY_URL, and PATH |
WorkingDirectory | — | Default working directory for spawned sessions |
RunAtLoad | — | Start automatically when the agent is loaded (including on login) |
KeepAlive | — | Restart automatically if the process exits |
StandardOutPath | — | File path for stdout logging |
StandardErrorPath | — | File path for stderr logging |
Migrating from a LaunchDaemon
Section titled “Migrating from a LaunchDaemon”If you previously set up PizzaPi as a LaunchDaemon (in /Library/LaunchDaemons/), migrate to a LaunchAgent:
-
Stop and unload the daemon
Terminal window sudo launchctl unload /Library/LaunchDaemons/com.pizzapi.runner.plist -
Remove the daemon plist
Terminal window sudo rm /Library/LaunchDaemons/com.pizzapi.runner.plist -
Create the LaunchAgent following the steps above.
Troubleshooting
Section titled “Troubleshooting”| Problem | Cause | Fix |
|---|---|---|
gh auth fails with “token is invalid” | Runner is a LaunchDaemon — no keychain access | Migrate to a LaunchAgent (see above) |
errSecInteractionNotAllowed | Keychain is locked — process has no login session | Ensure you’re using a LaunchAgent, not a LaunchDaemon |
| Runner exits immediately (exit code 1) | Missing env vars or wrong paths | Check ~/.pizzapi/logs/runner-error.log and verify all paths in the plist |
bun: command not found | PATH in plist doesn’t include Bun | Add /Users/yourname/.bun/bin to the PATH in EnvironmentVariables |
| Runner doesn’t start on reboot | Plist not loaded | Run launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist |
| Runner starts but doesn’t connect | Wrong PIZZAPI_RELAY_URL or PIZZAPI_API_KEY | Check the values in the plist match ~/.pizzapi/config.json |
| Sessions spawn but tools can’t access GitHub | gh token in keychain but login keychain locked | This shouldn’t happen with a LaunchAgent — check security list-keychains includes login keychain |
Verifying keychain access
Section titled “Verifying keychain access”You can test whether the runner process has keychain access:
# Should return the token (not an error)security find-generic-password -s "gh:github.com" -wIf this returns errSecInteractionNotAllowed, the keychain is locked — you’re likely running as a LaunchDaemon.