Skip to content

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.


macOS has two ways to run background services via launchd:

TypeLocationRuns asKeychain accessGUI 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:

  • gh CLI — 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.


The quickest setup is:

Terminal window
pizzapi runner install --activate

Use pizzapi runner install --dry-run to preview the generated ~/Library/LaunchAgents/com.pizzapi.runner.plist without writing it. The manual steps below are useful when you need to customize the plist.

  1. If you haven’t already, install the CLI and run setup:

    Terminal window
    npm install -g @pizzapi/pizza
    pizzapi setup

    See the Quick Setup guide if this is your first time.

  2. /Users/yourname/.bun/bin/bun
    which bun
    which pizzapi
    # /Users/yourname/.bun/bin/pizzapi # if installed with Bun globally
    # /usr/local/bin/pizzapi # if installed with npm globally

    Paste the real paths from your machine into the plist below. The exact location depends on whether you installed via npm or bun.

  3. Terminal window
    mkdir -p ~/Library/LaunchAgents

    Create ~/Library/LaunchAgents/com.pizzapi.runner.plist with 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/pizzapi</string>
    <string>runner</string>
    </array>
    <key>EnvironmentVariables</key>
    <dict>
    <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>
    <dict>
    <key>SuccessfulExit</key>
    <false/>
    <key>Crashed</key>
    <true/>
    </dict>
    <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>
  4. Terminal window
    mkdir -p ~/.pizzapi/logs
  5. Terminal window
    launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist
  6. Terminal window
    launchctl list | grep pizzapi
    # 12345 0 com.pizzapi.runner
    # Check the logs
    tail -20 ~/.pizzapi/logs/runner.log

    A 0 in the second column means the service started successfully. The runner should now appear in your relay’s web UI.


The preferred way to stop is the graceful CLI command:

Terminal window
pizzapi runner stop

This SIGTERMs the supervisor, which forwards the signal to the daemon so it releases its lock and disconnects cleanly.

Reserve launchctl unload for when you need to edit the plist itself — it disconnects all active sessions:

Terminal window
launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plist

Inside a running session you can use /restart, which triggers the supervisor to respawn the daemon. To restart the whole service from the shell:

Terminal window
pizzapi runner stop
launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist

Or, if you only changed environment variables that the daemon reads from ~/.pizzapi/config.json, a /restart is enough — no launchctl needed.

Terminal window
# Stdout
tail -f ~/.pizzapi/logs/runner.log
# Stderr
tail -f ~/.pizzapi/logs/runner-error.log
Terminal window
launchctl list | grep pizzapi

The output columns are: PID, exit code, label. A - for PID means the service isn’t currently running.


KeyRequiredDescription
LabelUnique identifier for the service
ProgramArgumentsCommand to run — must be full paths (no ~ expansion)
EnvironmentVariablesMust include PATH (launchd does not inherit your shell path). PIZZAPI_API_KEY and PIZZAPI_RELAY_URL are optional — the daemon falls back to ~/.pizzapi/config.json when they are absent.
WorkingDirectoryDefault working directory for spawned sessions
RunAtLoadStart automatically when the agent is loaded (including on login)
KeepAliveRestart automatically if the process exits
StandardOutPathFile path for stdout logging
StandardErrorPathFile path for stderr logging

If you previously set up PizzaPi as a LaunchDaemon (in /Library/LaunchDaemons/), migrate to a LaunchAgent:

  1. Stop and unload the daemon

    Terminal window
    sudo launchctl unload /Library/LaunchDaemons/com.pizzapi.runner.plist
  2. Remove the daemon plist

    Terminal window
    sudo rm /Library/LaunchDaemons/com.pizzapi.runner.plist
  3. Create the LaunchAgent following the steps above.


ProblemCauseFix
gh auth fails with “token is invalid”Runner is a LaunchDaemon — no keychain accessMigrate to a LaunchAgent (see above)
errSecInteractionNotAllowedKeychain is locked — process has no login sessionEnsure you’re using a LaunchAgent, not a LaunchDaemon
Runner exits immediately (exit code 1)Missing env vars or wrong pathsCheck ~/.pizzapi/logs/runner-error.log and verify all paths in the plist
bun: command not foundPATH in plist doesn’t include BunAdd /Users/yourname/.bun/bin to the PATH in EnvironmentVariables
Runner doesn’t start on rebootPlist not loadedRun launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist
Runner starts but doesn’t connectWrong PIZZAPI_RELAY_URL or PIZZAPI_API_KEYCheck the values in the plist match ~/.pizzapi/config.json
Sessions spawn but tools can’t access GitHubgh token in keychain but login keychain lockedThis shouldn’t happen with a LaunchAgent — check security list-keychains includes login keychain

You can test whether the runner process has keychain access with the security command. The exact service name depends on your gh version and how the token was stored; for example:

Terminal window
security find-generic-password -s 'gh:github.com' -w

If this returns errSecInteractionNotAllowed, the keychain is locked — you’re likely running as a LaunchDaemon.