Introduction
launchd is macOS’s service management framework โ the equivalent of systemd on Linux. It starts and manages background processes, daemons, and agents. To run a program automatically at startup (before or after login), you create a property list (.plist) file and register it with launchctl.
LaunchDaemons vs LaunchAgents
| LaunchDaemons | LaunchAgents | |
|---|---|---|
| Location | /Library/LaunchDaemons/ |
/Library/LaunchAgents/ or ~/Library/LaunchAgents/ |
| Runs as | root (or specified user) | Logged-in user |
| Starts when | System boot (before login) | User logs in |
| Requires sudo | Yes | No |
| Use for | System services, servers | User-level apps, tools |
Creating a LaunchDaemon (System-Wide Service)
Step 1: Create the plist file
sudo vim /Library/LaunchDaemons/com.example.myprogram.plist
<?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>
<!-- Unique identifier (reverse domain notation) -->
<key>Label</key>
<string>com.example.myprogram</string>
<!-- Program and arguments -->
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/myprogram</string>
<string>--config</string>
<string>/etc/myprogram/config.yaml</string>
</array>
<!-- Start at boot -->
<key>RunAtLoad</key>
<true/>
<!-- Restart if it crashes -->
<key>KeepAlive</key>
<true/>
<!-- Log stdout and stderr -->
<key>StandardOutPath</key>
<string>/var/log/myprogram.log</string>
<key>StandardErrorPath</key>
<string>/var/log/myprogram.error.log</string>
<!-- Working directory -->
<key>WorkingDirectory</key>
<string>/var/lib/myprogram</string>
</dict>
</plist>
Step 2: Set correct permissions
sudo chown root:wheel /Library/LaunchDaemons/com.example.myprogram.plist
sudo chmod 644 /Library/LaunchDaemons/com.example.myprogram.plist
Step 3: Load the service
# Load and start immediately
sudo launchctl load /Library/LaunchDaemons/com.example.myprogram.plist
# macOS 10.11+ (El Capitan and later) โ preferred
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.myprogram.plist
Creating a LaunchAgent (User-Level Service)
For user-level services (no sudo required):
mkdir -p ~/Library/LaunchAgents
vim ~/Library/LaunchAgents/com.example.mytool.plist
<?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.example.mytool</string>
<key>ProgramArguments</key>
<array>
<string>/Users/username/bin/mytool</string>
<string>--daemon</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/username/Library/Logs/mytool.log</string>
<key>StandardErrorPath</key>
<string>/Users/username/Library/Logs/mytool.error.log</string>
</dict>
</plist>
# Load (no sudo needed for user agents)
launchctl load ~/Library/LaunchAgents/com.example.mytool.plist
Managing Services with launchctl
# Load a service (registers and starts it)
sudo launchctl load /Library/LaunchDaemons/com.example.myprogram.plist
# Unload a service (stops and unregisters it)
sudo launchctl unload /Library/LaunchDaemons/com.example.myprogram.plist
# Start a loaded service
sudo launchctl start com.example.myprogram
# Stop a running service
sudo launchctl stop com.example.myprogram
# List all loaded services
launchctl list
# Check a specific service
launchctl list | grep myprogram
# Print service info
sudo launchctl print system/com.example.myprogram
Modern launchctl (macOS 10.11+)
# Bootstrap (load + enable)
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.myprogram.plist
# Bootout (unload + disable)
sudo launchctl bootout system/com.example.myprogram
# Enable/disable without starting
sudo launchctl enable system/com.example.myprogram
sudo launchctl disable system/com.example.myprogram
# Kickstart (force start)
sudo launchctl kickstart -k system/com.example.myprogram
Viewing Logs
# Follow the log in real-time
tail -f /var/log/myprogram.log
tail -f /var/log/myprogram.error.log
# View system logs for the service
log show --predicate 'process == "myprogram"' --last 1h
# Stream live logs
log stream --predicate 'process == "myprogram"'
Advanced plist Options
Run on a Schedule (Cron-like)
<!-- Run every hour -->
<key>StartInterval</key>
<integer>3600</integer>
<!-- Run at specific times (like cron) -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
<!-- Run at multiple times -->
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>0</integer>
</dict>
<dict>
<key>Hour</key><integer>17</integer>
<key>Minute</key><integer>0</integer>
</dict>
</array>
Environment Variables
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin</string>
<key>APP_ENV</key>
<string>production</string>
<key>LOG_LEVEL</key>
<string>info</string>
</dict>
Conditional KeepAlive
<!-- Only restart on non-zero exit code -->
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<!-- Restart with throttle (wait 10s between restarts) -->
<key>ThrottleInterval</key>
<integer>10</integer>
Run as Specific User
<!-- For LaunchDaemons only -->
<key>UserName</key>
<string>www</string>
<key>GroupName</key>
<string>www</string>
Troubleshooting
Service won’t start
# Check exit code (non-zero = error)
launchctl list | grep myprogram
# Output: PID ExitCode Label
# -1 means not running, exit code shows why
# Check error log
cat /var/log/myprogram.error.log
# Validate plist syntax
plutil -lint /Library/LaunchDaemons/com.example.myprogram.plist
Permission issues
# Daemon plist must be owned by root:wheel
sudo chown root:wheel /Library/LaunchDaemons/com.example.myprogram.plist
sudo chmod 644 /Library/LaunchDaemons/com.example.myprogram.plist
# The program itself must be executable
chmod +x /usr/local/bin/myprogram
Common exit codes
| Exit Code | Meaning |
|---|---|
| 0 | Success (normal exit) |
| 1 | General error |
| 78 | Configuration error |
| 126 | Permission denied |
| 127 | Command not found |
Resources
- Apple: launchd Documentation
- launchd.info โ comprehensive launchd reference
- launchctl man page
Comments