macOS Service Management with Launchd¶
This guide demonstrates how to use Mooncake to manage macOS services using launchd.
Table of Contents¶
Quick Start¶
Simple User Agent¶
- name: Start my application
service:
name: com.example.myapp
state: started
enabled: true
unit:
content: |
<?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.myapp</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/myapp</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Using Templates¶
- name: Deploy service from template
service:
name: com.example.{{ app_name }}
state: started
enabled: true
unit:
src_template: templates/service.plist.j2
Service Types¶
User Agents¶
User agents run in the user's context (no sudo required):
- Path:
~/Library/LaunchAgents/ - Domain:
gui/<uid> - Permissions: Current user
- When: When user logs in
- name: User agent
service:
name: com.example.myapp
state: started
enabled: true
unit:
content: |
<!-- plist content here -->
System Daemons¶
System daemons run as root (require sudo):
- Path:
/Library/LaunchDaemons/ - Domain:
system - Permissions: root (requires
become: true) - When: At system boot
- name: System daemon
service:
name: com.example.daemon
state: started
enabled: true
unit:
dest: /Library/LaunchDaemons/com.example.daemon.plist
content: |
<!-- plist content here -->
become: true
Complete Examples¶
1. Node.js Web Server¶
See: macos-nodejs-app.yml
Complete example showing:
- Directory setup
- Dependency installation
- Service configuration
- Logging
- Environment variables
- Health checks
Run with:
2. Service Management Operations¶
See: macos-service-management.yml
Examples of:
- Starting/stopping services
- Restarting services
- Updating configuration
- Enabling/disabling
- Dry-run mode
Run with:
3. Various Service Types¶
See: macos-launchd-service.yml
Demonstrates:
- User agents
- System daemons
- Scheduled tasks
- Resource limits
- Keep-alive configuration
Common Patterns¶
Auto-Restart on Crash¶
Scheduled Task (Cron-like)¶
<!-- Run every hour -->
<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>0</integer>
</dict>
<!-- Run every day at 2:30 AM -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
Environment Variables¶
<key>EnvironmentVariables</key>
<dict>
<key>PORT</key>
<string>8080</string>
<key>NODE_ENV</key>
<string>production</string>
</dict>
Logging¶
<key>StandardOutPath</key>
<string>/var/log/myapp/stdout.log</string>
<key>StandardErrorPath</key>
<string>/var/log/myapp/stderr.log</string>
Prevent Rapid Restarts¶
Plist Properties¶
Essential Properties¶
| Key | Type | Description |
|---|---|---|
Label |
String | Service identifier (required) |
ProgramArguments |
Array | Command and arguments to run (required) |
Execution Control¶
| Key | Type | Description |
|---|---|---|
RunAtLoad |
Boolean | Start when loaded |
KeepAlive |
Boolean/Dict | Auto-restart configuration |
StartCalendarInterval |
Dict | Schedule (cron-like) |
StartInterval |
Integer | Run every N seconds |
Process Management¶
| Key | Type | Description |
|---|---|---|
WorkingDirectory |
String | Working directory |
EnvironmentVariables |
Dict | Environment variables |
UserName |
String | Run as specific user |
GroupName |
String | Run as specific group |
Logging¶
| Key | Type | Description |
|---|---|---|
StandardOutPath |
String | Stdout log file |
StandardErrorPath |
String | Stderr log file |
Resource Limits¶
| Key | Type | Description |
|---|---|---|
SoftResourceLimits |
Dict | Soft resource limits |
HardResourceLimits |
Dict | Hard resource limits |
Nice |
Integer | Process priority (-20 to 20) |
Network¶
| Key | Type | Description |
|---|---|---|
Sockets |
Dict | Socket activation |
Service States¶
Available States¶
| State | Description | Action |
|---|---|---|
started |
Start the service | launchctl bootstrap (if not loaded)launchctl kickstart (if loaded) |
stopped |
Stop the service | launchctl kill SIGTERM |
restarted |
Restart the service | launchctl kickstart -k |
reloaded |
Reload configuration | Same as restarted |
Enabled Status¶
| Status | Description | Action |
|---|---|---|
enabled: true |
Load service (persistent) | launchctl bootstrap |
enabled: false |
Unload service | launchctl bootout |
Idempotency¶
Mooncake automatically ensures idempotent operations:
- Plist Updates: Only writes if content changed
- Service State: Checks current state before changing
- Load Status: Only loads/unloads if needed
Example:
# First run: Creates plist, loads service, starts it
# Second run: No changes (plist unchanged, service already running)
- name: Deploy service
service:
name: com.example.app
state: started
enabled: true
unit:
content: |
<!-- plist content -->
Troubleshooting¶
Check Service Status¶
# List all loaded services
launchctl list
# Check specific service
launchctl list | grep com.example.myapp
# Print service details
launchctl print gui/$(id -u)/com.example.myapp
View Logs¶
# If using StandardOutPath/StandardErrorPath
tail -f /path/to/stdout.log
tail -f /path/to/stderr.log
# System logs
log stream --predicate 'processImagePath contains "myapp"' --info
Unload Service Manually¶
# User agent
launchctl bootout gui/$(id -u)/com.example.myapp
# System daemon
sudo launchctl bootout system/com.example.daemon
Load Service Manually¶
# User agent
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myapp.plist
# System daemon
sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.daemon.plist
Common Issues¶
Issue: Service not starting
- Check plist syntax:
plutil -lint ~/Library/LaunchAgents/com.example.myapp.plist - Check logs:
tail -f /path/to/error.log - Verify program path exists
Issue: Permission denied
- User agents: Don't use
become: true - System daemons: Must use
become: true
Issue: Service keeps restarting
- Check exit code:
launchctl print gui/$(id -u)/com.example.myapp - Review logs for errors
- Add
ThrottleIntervalto prevent rapid restarts
Dry-Run Mode¶
Preview changes without applying them:
Output shows:
- What plist files would be created/updated
- What services would be started/stopped
- What operations would be performed
Template Variables¶
Use variables for flexibility:
vars:
app_name: myapp
app_path: /usr/local/bin/myapp
port: 8080
log_dir: /var/log/myapp
steps:
- name: Deploy {{ app_name }}
service:
name: com.example.{{ app_name }}
unit:
content: |
<?xml version="1.0" encoding="UTF-8"?>
<!-- ... -->
<key>ProgramArguments</key>
<array>
<string>{{ app_path }}</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PORT</key>
<string>{{ port }}</string>
</dict>
References¶
- launchd.info - Comprehensive launchd documentation
- Apple Developer Documentation
- launchctl man page
- plist man page
Testing¶
All launchd functionality is tested:
- ✅ Plist creation (inline and template)
- ✅ Service state management
- ✅ Load/unload operations
- ✅ Idempotency checks
- ✅ Platform detection
- ✅ Dry-run mode
Tests automatically skip on non-macOS platforms.
Run tests: