Deployment guide · M4 Pro tested

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.

What we tested. On an M4 Pro Mac mini running macOS 26.5.1, the service loaded a model and completed a request after a full reboot while the Mac remained at the login window with zero logged-in users. KeepAlive restart and the stop-update-start workflow were also exercised.

Before you begin

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.