Automating OpenClaw gateway deployment with launchd on a remote Mac

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
Core insight: The gateway is a stateful long-lived service, not a one-off script. 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 ~/.openclaw backed 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 pathslaunchd 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
Mantra: Interactive SSH works → onboard locks it in → reboot to accept. Skip step two and jump straight to KeepAlive — you only get a faster crash loop.

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 -p the 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; bootout the 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:

  1. Detect node / openclaw; if missing run npm i -g openclaw@latest
  2. Create ~/Library/Logs/OpenClawGateway
  3. Prefer openclaw onboard; if no plist exists, write a fallback LaunchAgent
  4. launchctl bootstrap + kickstart -k
  5. Accept with lsof + openclaw gateway probe
Team pattern: Script lives in the infra repo; each Mac only gets injected secrets (API token, region). Bare metal to green probe in under 5 minutes — that is «stop configuring by hand».

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 doctor shows no ERROR
  • launchctl print gui/$(id -u)/com.openclaw.gateway state is running
  • lsof -nP -iTCP:18789 -sTCP:LISTEN PID matches plist
  • openclaw gateway probe succeeds
  • 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 kill the 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.