Run at boot on a headless Mac
Register rapid-mlx serve as a system LaunchDaemon so an
inference appliance starts before any user logs in and automatically
recovers after the server exits.
Before you begin
- Use a dedicated, non-administrator local account for the service.
- Install Rapid-MLX and download the intended model as that account.
- Keep physical or out-of-band recovery access during setup.
- You need administrator access to install the system plist.
FileVault matters. A LaunchDaemon starts without GUI login only after the data volume is unlocked. Following a cold boot, a FileVault-protected Mac may wait at preboot authentication, where neither SSH nor Rapid-MLX can start. Choose encryption and unattended recovery policy deliberately.
1. Prepare the service account
$ curl -fsSL https://rapidmlx.com/install.sh | bash $ mkdir -p "$HOME/Library/Logs/Rapid-MLX" $ chmod 750 "$HOME/Library/Logs/Rapid-MLX" $ rapid-mlx serve qwen3.5-4b-4bit --host 127.0.0.1 --port 8000
Let the first model download finish, verify the foreground server, then
stop it. The daemon must use the same account so HOME, the
virtual environment, and ~/.cache/huggingface/hub all resolve
to the prepared files.
2. Configure launchd
Switch to a separate administrator session. Download the reviewed template, replace the account name, and inspect every argument. Keep the service account in every absolute path; do not use the administrator's home directory.
$ curl -fsSLo /tmp/com.rapidmlx.server.plist \ https://raw.githubusercontent.com/raullenchai/Rapid-MLX/main/examples/launchd/com.rapidmlx.server.plist $ sed -i '' 's/serveuser/YOUR_SERVICE_ACCOUNT/g' \ /tmp/com.rapidmlx.server.plist $ plutil -lint /tmp/com.rapidmlx.server.plist $ plutil -p /tmp/com.rapidmlx.server.plist
Set the model and serving flags for the machine. Keep all paths absolute:
launchd does not expand ~ or $HOME in plist
values. Keep the explicit UserName and HOME keys;
without them the daemon can fail to find its model cache.
$ sudo install -o root -g wheel -m 644 \ /tmp/com.rapidmlx.server.plist \ /Library/LaunchDaemons/com.rapidmlx.server.plist $ sudo launchctl bootstrap system \ /Library/LaunchDaemons/com.rapidmlx.server.plist $ sudo launchctl print system/com.rapidmlx.server
3. Keep the endpoint private
The template binds to 127.0.0.1. Reach it from another
computer through an SSH tunnel:
$ ssh -L 8000:127.0.0.1:8000 YOUR_SERVICE_ACCOUNT@your-mac
Do not put an API key in ProgramArguments or the plist's
EnvironmentVariables. Arguments are visible to local
processes, and launchd can display configured environment values through
launchctl print even when the plist is mode 0600. For LAN or
internet access, keep Rapid-MLX on loopback and put a separately managed
TLS-terminating, authenticating reverse proxy in front of it.
4. Verify recovery
$ curl -fsSLo headless_service_smoke.sh \ https://raw.githubusercontent.com/raullenchai/Rapid-MLX/main/scripts/headless_service_smoke.sh $ chmod +x headless_service_smoke.sh $ export RAPID_MLX_SERVICE_USER=YOUR_SERVICE_ACCOUNT $ export RAPID_MLX_SMOKE_MODEL=default $ ./headless_service_smoke.sh
The smoke test verifies the system-domain job, process owner, liveness, readiness, model inventory, and a real one-token completion. Then verify KeepAlive and run it again. The script waits up to 120 seconds for the configured model to become ready:
$ old_pid=$(launchctl print system/com.rapidmlx.server | \ awk -F'= ' '/^[[:space:]]*pid =/{print $2; exit}') $ sudo launchctl kill SIGTERM system/com.rapidmlx.server $ for _ in {1..30}; do new_pid=$(launchctl print system/com.rapidmlx.server 2>/dev/null | \ awk -F'= ' '/^[[:space:]]*pid =/{print $2; exit}') [ -n "$new_pid" ] && [ "$new_pid" != "$old_pid" ] && break sleep 1 done $ [ -n "${new_pid:-}" ] && [ "$new_pid" != "$old_pid" ] || exit 1 $ ./headless_service_smoke.sh
Finally, turn automatic login off, reboot with recovery access available, leave the Mac at the login window, and run the smoke test remotely.
5. Power policy
$ pmset -g custom $ sudo pmset -a sleep 0 autorestart 1
This requests restart when AC power returns; it does not bypass FileVault. If outage recovery is a hard requirement, perform a controlled power-loss acceptance test at the deployment site.
6. Update safely
$ sudo -u YOUR_SERVICE_ACCOUNT -H \ /Users/YOUR_SERVICE_ACCOUNT/.local/bin/rapid-mlx --version $ sudo -u YOUR_SERVICE_ACCOUNT -H /bin/bash -c \ '/Users/YOUR_SERVICE_ACCOUNT/.rapid-mlx/bin/python -m pip freeze \ > "$HOME/rapid-mlx-before-upgrade.txt"' $ sudo cp -p /Library/LaunchDaemons/com.rapidmlx.server.plist \ /Library/LaunchDaemons/com.rapidmlx.server.plist.pre-upgrade $ sudo launchctl bootout system/com.rapidmlx.server $ curl -fsSL https://rapidmlx.com/install.sh | \ sudo -u YOUR_SERVICE_ACCOUNT -H /bin/bash # Re-assert every optional extra used by this appliance. $ sudo -u YOUR_SERVICE_ACCOUNT -H \ /Users/YOUR_SERVICE_ACCOUNT/.rapid-mlx/bin/python \ -m pip install --upgrade 'rapid-mlx[vision]' $ sudo -u YOUR_SERVICE_ACCOUNT -H \ /Users/YOUR_SERVICE_ACCOUNT/.local/bin/rapid-mlx doctor || \ echo 'Doctor reported issues; keep the daemon stopped and continue validation.' $ sudo launchctl bootstrap system \ /Library/LaunchDaemons/com.rapidmlx.server.plist $ ./headless_service_smoke.sh
Always boot the daemon out before maintenance so KeepAlive cannot race the installer. A compatible environment is upgraded in place, but a runtime rebuild does not preserve optional extras. Record and re-assert them. Investigate every Doctor issue, but do not use Doctor alone to accept or reject the update: some 0.13.3 runtime layouts can report false import failures. Treat the API smoke as the actionable gate because it exercises the actual launchd environment and configured model.
Rollback a failed update
$ sudo launchctl bootout system/com.rapidmlx.server 2>/dev/null || true $ sudo -u YOUR_SERVICE_ACCOUNT -H \ /Users/YOUR_SERVICE_ACCOUNT/.rapid-mlx/bin/python -m pip install \ --force-reinstall \ -r /Users/YOUR_SERVICE_ACCOUNT/rapid-mlx-before-upgrade.txt $ sudo cp -p \ /Library/LaunchDaemons/com.rapidmlx.server.plist.pre-upgrade \ /Library/LaunchDaemons/com.rapidmlx.server.plist $ sudo launchctl bootstrap system \ /Library/LaunchDaemons/com.rapidmlx.server.plist $ ./headless_service_smoke.sh
Stop or remove the service
$ sudo launchctl bootout system/com.rapidmlx.server $ sudo rm /Library/LaunchDaemons/com.rapidmlx.server.plist
Removing the plist leaves the service account, model cache, Python
environment, and logs untouched. Rapid-MLX does not yet provide a
service install command or rotate daemon logs automatically.