Friday night, webhooks go red. You SSH into the remote Mac and find the gateway process is long gone — last time you started it manually with openclaw gateway run, and nobody re-ran that command after the Mac rebooted at 3 a.m. The Discord bot shows online, but port 18789 is empty, and GitHub callbacks have piled up 47 HTTP 502s.
This is not an OpenClaw bug. It is an ops model still stuck in dev-machine thinking: interactive starts, hand-edited plists, a different config on every host. In 2026 you treat the gateway as infrastructure, and on macOS the right tool is launchd — boot auto-start, crash recovery, logs on disk, config in Git.
This article delivers a zero-to-acceptance automation path: onboard registration, a production-grade plist template, a one-click bootstrap script, health probes, and an upgrade rollback order. For troubleshooting see the companion OpenClaw Gateway launchd Stability Handbook; for multi-machine CI orchestration see OpenClaw Hands-On Deployment & GitHub Actions Automation.
Quick Answer: three most common questions
| Question | Direct answer | Watch out for |
|---|---|---|
| Fastest way to survive a reboot? | openclaw onboard with launchd, or run the bootstrap script at the end of this article |
Run doctor clean before registering — otherwise you get a crash loop |
Can manual gateway run coexist with launchd? |
No — double port binding | launchctl bootout + lsof to clear the port before switching |
| How do you prove it is actually HA? | reboot → wait 2 minutes → gateway probe + external curl |
Do not test localhost only |
1. Why you must stop manual configuration
On a remote always-on Mac, manual configuration fails in predictable ways:
| Approach | Looks easy | Real cost |
|---|---|---|
nohup openclaw gateway run & over SSH |
Service up in 5 seconds | Gone after reboot; no log rotation; exit code invisible |
| Hand-edit plist on each machine | «Just change the port» | Label collisions, PATH drift, no diff on upgrade |
| Docs say «click Start after login» | Avoids TCC hassle | Holiday power outage → guaranteed outage |
| Docker + launchd both running | «Extra safety layer» | Fight over 18789; state directory double-write locks |
launchd owns process supervision; you focus on config and probes — that is automation.
2. What gateway «high availability» means on a Mac
A single Mac cannot do Kubernetes-style multi-replica HA, but you can reach operations-grade HA:
- Survive reboot: Gateway listening within 2 minutes of boot, no manual SSH.
- Survive crash:
KeepAlive+ThrottleInterval— abnormal exit triggers backoff restart without pegging CPU. - Survive config drift: plist, environment variables, and
~/.openclawbacked up and tracked in Git. - Survive silent failure: Outer healthcheck catches «port listening but probe fails».
- Survive upgrade: Fixed order:
backup → doctor --fix → gateway restart, scriptable every time.
In practice: an M4 Mac mini (24 GB) running OpenClaw gateway plus light plugins idles around 4–6 W; gateway process memory is typically 200–450 MB. Putting the machine on the remote Mac native deployment path is more predictable than leaving an iMac powered on at the office.
3. LaunchAgent or LaunchDaemon
| Dimension | LaunchAgent (user domain) | LaunchDaemon (system domain) |
|---|---|---|
| Path | ~/Library/LaunchAgents/ |
/Library/LaunchDaemons/ |
| Start timing | After user login | At system boot, no GUI login required |
| TCC / keychain | Inherits user grants | Restricted — suits pure network daemons |
| Best for | Browser automation, plugins reading user directories | Pure HTTP/Webhook gateway, no GUI dependency |
2026 default: Start with a LaunchAgent — run onboard and doctor through it. Move to a Daemon only after you confirm you do not need a login session, and document that in your runbook. Either way, ProgramArguments must use absolute paths — launchd does not read your .zshrc.
4. onboard one-click launchd registration
OpenClaw onboard writes Node path, state directory, and gateway port into config, and recent versions support installing the launchd service directly. Recommended order:
# 1. Confirm runtime (must match PATH in plist)
node -v # expect v22.x
which openclaw # note absolute path, e.g. /opt/homebrew/bin/openclaw
# 2. Pre-flight
openclaw doctor
# 3. Interactive onboard (enable Gateway + launchd when prompted)
openclaw onboard
# 4. Acceptance
launchctl print "gui/$(id -u)/com.openclaw.gateway" 2>/dev/null || launchctl list | grep -i openclaw
lsof -nP -iTCP:18789 -sTCP:LISTEN
openclaw gateway probe
5. Production plist template (Git-ready)
If onboard did not generate a plist, or you need to version it explicitly in an infra repo, use the template below. Replace OPENCLAW_BIN with the output of which openclaw:
<?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.openclaw.gateway</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/openclaw</string>
<string>gateway</string>
<string>run</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/your-ci-user</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/your-ci-user</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/your-ci-user/Library/Logs/OpenClawGateway/gateway.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/your-ci-user/Library/Logs/OpenClawGateway/gateway.stderr.log</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>ThrottleInterval</key>
<integer>30</integer>
</dict>
</plist>
Load (Ventura+):
UID_NUM=$(id -u)
DOMAIN="gui/${UID_NUM}"
LABEL="com.openclaw.gateway"
PLIST="${HOME}/Library/LaunchAgents/${LABEL}.plist"
launchctl bootout "${DOMAIN}/${LABEL}" 2>/dev/null || true
launchctl bootstrap "${DOMAIN}" "${PLIST}"
launchctl kickstart -k "${DOMAIN}/${LABEL}"
Common pitfalls:
- On Intel Macs Homebrew is often at
/usr/local/bin; on Apple Silicon at/opt/homebrew/bin— hard-code in the plist, not a «universal PATH». mkdir -pthe log directory first — otherwise launchd may fail to start with nowhere to write stderr.- Never keep the same Label in both user and system domains;
bootoutthe old job before upgrading.
6. One-click bootstrap script
Collapse «install CLI → doctor → write plist → bootstrap → probe» into one command — ideal for a freshly rented cloud Mac or a GitHub Actions self-hosted runner. Full script in this article's resources:
resources/bootstrap-openclaw-gateway-launchd.sh
On the remote Mac:
chmod +x bootstrap-openclaw-gateway-launchd.sh
./bootstrap-openclaw-gateway-launchd.sh
Script flow summary:
- Detect
node/openclaw; if missing runnpm i -g openclaw@latest - Create
~/Library/Logs/OpenClawGateway - Prefer
openclaw onboard; if no plist exists, write a fallback LaunchAgent launchctl bootstrap+kickstart -k- Accept with
lsof+openclaw gateway probe
7. Health probes and outer self-healing
KeepAlive only checks whether the process exists — not «process zombie but port still LISTEN». Add a lightweight probe LaunchAgent (every 5 minutes):
#!/bin/bash
# ~/bin/openclaw-gateway-healthcheck.sh
set -euo pipefail
PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
LABEL="com.openclaw.gateway"
DOMAIN="gui/$(id -u)"
if ! openclaw gateway probe >/dev/null 2>&1; then
logger -t openclaw-ha "probe failed, kickstart gateway"
launchctl kickstart -k "${DOMAIN}/${LABEL}" || true
fi
Pair with a plist using StartCalendarInterval at ~/Library/LaunchAgents/com.openclaw.gateway-healthcheck.plist. You can also wire UptimeRobot or a Prometheus blackbox exporter — but the probe must follow the same path as webhooks (reverse proxy and TLS included), not just curl localhost.
Rotate logs with newsyslog or size-based truncation so a single file does not fill NVMe — when disk is full, launchd child processes exit with ENOSPC and it looks like random disconnects.
8. Git-versioned config and rolling upgrades
Put these under version control and review in PRs instead of editing over SSH:
| File | Purpose |
|---|---|
launchagents/com.openclaw.gateway.plist |
Primary gateway service |
scripts/bootstrap-openclaw-gateway-launchd.sh |
New node bootstrap |
scripts/upgrade-openclaw-gateway.sh |
Fixed upgrade sequence |
docs/runbook-gateway.md |
On-call handbook |
Minimum upgrade script order (aligned with the multi-channel gateway stability companion):
openclaw backup create
npm update -g openclaw@latest # or pin a version
openclaw doctor --fix
launchctl kickstart -k "gui/$(id -u)/com.openclaw.gateway"
openclaw gateway probe
# multi-plugin setups: run browser/cron doctor per plugin
9. Seven-step acceptance checklist
Before release or new machine go-live, SSH in and check off:
openclaw doctorshows no ERRORlaunchctl print gui/$(id -u)/com.openclaw.gatewaystate is runninglsof -nP -iTCP:18789 -sTCP:LISTENPID matches plistopenclaw gateway probesucceeds- curl the public entry from office network / mobile data (not only 127.0.0.1)
- After
sudo reboot, probe still succeeds within 2 minutes - Deliberately
killthe gateway PID — auto-recovery within 30 seconds without Throttle spam
10. Why Mac mini is still the best host for this automation
A gateway needs to stay always on, quiet, and path-consistent. Mac mini M4 on Apple Silicon idles around 4 W — 24/7 costs an order of magnitude less than a desktop tower. launchd, Homebrew, and OpenClaw's ~/.openclaw match your local dev machine, so you do not maintain two runbooks.
If you are moving OpenClaw from «experiment on a laptop» to a dedicated remote Mac, get single-node automation solid before adding nodes. Macstripe home page offers day-billed Mac mini trials in multiple regions — run the bootstrap script once and it beats leaving a machine powered on at the office.
FAQ
Does an OpenClaw gateway have to use launchd?
Not required — but on macOS launchd is the official daemon manager and fits unattended operation better than nohup. Docker suits parallel multi-version setups; launchd suits a low-latency gateway with less virtualization overhead.
LaunchAgent or LaunchDaemon — which should I pick?
Use LaunchAgent when you need user TCC or keychain access; use LaunchDaemon (root) when the service must listen without anyone logged in. Most teams start with Agent.
Does onboard conflict with a hand-written plist?
Label must be unique. bootout before upgrading to avoid two processes fighting over 18789. Commit the final plist to Git.
Probe fails but the port is listening?
Check auth, TLS, and bind address. Run doctor, then compare ~/.openclaw; details in the launchd troubleshooting handbook.
How do I deploy to multiple Macs?
Self-hosted runners + the same bootstrap script, secrets for tokens, rolling probe acceptance. See GitHub Actions multi-machine collaboration.
Conclusion
Stop configuring by hand does not mean «never open a terminal» — it means only run scripts in the terminal, never ad-hoc commands: onboard registers launchd, plist goes in Git, bootstrap provisions new machines, probe accepts, healthcheck catches edge cases. The gateway graduates from «foreground process in an SSH session» to repeatable infrastructure.
Next step: run bootstrap on one remote Mac, do a reboot drill, paste the seven-step checklist into your team wiki. When you need a dedicated node, pick a region on the Macstripe home page.