Long-running processes that boot with the Mac
Web servers, databases, message brokers — they need to start when the machine boots and restart on crash. macOS uses launchd to manage them. Two convenient front ends sit on top:
brew services— for tools installed via Homebrew.launchctl— the raw command for any plist.
brew services
brew install postgresql@16
brew services start postgresql@16
brew services list
brew services stop postgresql@16
brew services restart postgresql@16
# Run without auto-start at login
brew services run postgresql@16Behind the scenes brew writes a LaunchAgent plist into ~/Library/LaunchAgents/ and loads it.
launchctl directly
# See what's loaded for your user
launchctl list | grep -v com.apple
# Bootstrap (load) a plist
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.me.svc.plist
# Bootout (unload)
launchctl bootout gui/$UID/com.me.svc
# Restart cleanly
launchctl kickstart -k gui/$UID/com.me.svcLaunchAgent vs LaunchDaemon
- LaunchAgent — runs as your user when you log in. Lives under
~/Library/LaunchAgents/or/Library/LaunchAgents/. - LaunchDaemon — runs as root at boot, before any user logs in. Lives under
/Library/LaunchDaemons/.
A user-level development service usually belongs in a LaunchAgent because it runs after login without root. Use a LaunchDaemon only when the service genuinely needs a system-wide, no-user-required start.
Logs
The plist's StandardOutPath and StandardErrorPath tell launchd where to send output. Without them, output is dropped. Always set them when debugging.
The service declaration owns lifecycle
A launchd plist must make program arguments, working directory, environment, logs, restart behavior, and stop semantics explicit. After a declaration changes, use the appropriate bootout/bootstrap or kickstart workflow for its domain and confirm the loaded job, PID, logs, and real health rather than starting a duplicate process by hand.