---
title: "macOS"
description: "The binary under launchd, and why a laptop is a poor host."
url: "https://saved.sh/docs/workers/install/macos"
---

macOS runs the same static binary as Linux. The two things that differ are Gatekeeper, which
will refuse a downloaded binary if you fetch it the wrong way, and launchd, which gives a
daemon a minimal `PATH` and so cannot see anything Homebrew installed.

## Download and verify [#download-and-verify]

```bash
VERSION=v0.1.0
ARCH=$(uname -m); case "$ARCH" in x86_64) ARCH=amd64 ;; esac

curl -fsSLO "https://github.com/savedhq/local-worker/releases/download/${VERSION}/local-worker-darwin-${ARCH}"
curl -fsSLO "https://github.com/savedhq/local-worker/releases/download/${VERSION}/SHA256SUMS"

grep "local-worker-darwin-${ARCH}\$" SHA256SUMS | shasum -a 256 -c -
```

`uname -m` already prints `arm64` on Apple silicon, which is the name the release uses. Only
Intel needs the rename.

```bash
sudo install -m 0755 "local-worker-darwin-${ARCH}" /usr/local/bin/local-worker
```

<Callout type="warn">
  **Download with `curl`, not a browser.** A file fetched by Safari or Chrome carries a
  quarantine attribute and Gatekeeper will refuse to execute it. If you hit it anyway, clear
  it with `xattr -d com.apple.quarantine ./local-worker`.
</Callout>

## Put the config somewhere [#put-the-config-somewhere]

```bash
sudo install -d -m 0700 /usr/local/etc/saved
sudo install -m 0600 config.yaml /usr/local/etc/saved/config.yaml
```

A LaunchDaemon runs as `root` unless you tell it otherwise, so root-owned `0600` is the
correct mode. If you would rather it ran as an existing account, add a `UserName` key to the
plist below and `chown` the config to match.

## Point at the tools explicitly [#point-at-the-tools-explicitly]

This is the one thing that catches everyone. &#x2A;*launchd starts a daemon with a minimal `PATH`**
of `/usr/bin:/bin:/usr/sbin:/sbin`. Homebrew installs to `/opt/homebrew/bin` on Apple silicon
and `/usr/local/bin` on Intel, so a worker that finds `pg_dump` perfectly well in your shell
will log `external tool not found` under launchd.

Give the absolute paths in `config.yaml` rather than hoping:

```yaml title="/usr/local/etc/saved/config.yaml"
tools:
  pg_dump: /opt/homebrew/bin/pg_dump
  mysqldump: /opt/homebrew/bin/mysqldump
  curl: /usr/bin/curl
```

`which pg_dump` in a normal shell tells you what to write.

## Run it once by hand [#run-it-once-by-hand]

```bash
cd /usr/local/etc/saved && sudo /usr/local/bin/local-worker
```

Confirm the `external tool resolved` lines name the paths you expect and that `backups=`
matches what this worker should serve, then stop it with `Ctrl-C`. See
[what healthy looks like](/docs/workers/troubleshooting#what-healthy-looks-like).

## The daemon [#the-daemon]

```xml title="/Library/LaunchDaemons/sh.saved.worker.plist"
<?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>sh.saved.worker</string>
  <key>ProgramArguments</key>  <array><string>/usr/local/bin/local-worker</string></array>
  <key>WorkingDirectory</key>  <string>/usr/local/etc/saved</string>
  <key>RunAtLoad</key>         <true/>
  <key>KeepAlive</key>         <true/>
  <key>StandardOutPath</key>   <string>/var/log/saved-worker.log</string>
  <key>StandardErrorPath</key> <string>/var/log/saved-worker.log</string>
</dict>
</plist>
```

`WorkingDirectory` is not optional. It is the only way the worker finds its config.

```bash
sudo chown root:wheel /Library/LaunchDaemons/sh.saved.worker.plist
sudo chmod 0644 /Library/LaunchDaemons/sh.saved.worker.plist

sudo launchctl bootstrap system /Library/LaunchDaemons/sh.saved.worker.plist
sudo launchctl print system/sh.saved.worker
tail -f /var/log/saved-worker.log
```

`bootstrap` and `bootout` replace the deprecated `load` and `unload`. To restart after
editing the config:

```bash
sudo launchctl kickstart -k system/sh.saved.worker
```

To remove it entirely:

```bash
sudo launchctl bootout system/sh.saved.worker
```

## LaunchAgent, and why probably not [#launchagent-and-why-probably-not]

A **LaunchAgent** in `~/Library/LaunchAgents` is the same plist bootstrapped into
`gui/$(id -u)` instead of `system`. It is the right choice for a worker you are testing
against a development database, and the wrong one for anything on a schedule.

|                 | LaunchDaemon             | LaunchAgent                  |
| --------------- | ------------------------ | ---------------------------- |
| Runs            | At boot, no login needed | Only while you are logged in |
| Survives logout | Yes                      | No                           |
| Runs as         | `root`, or `UserName`    | You                          |

<Callout type="warn">
  **A sleeping Mac runs nothing.** Neither kind of job fires while the machine is asleep, and a
  closed laptop lid is asleep. If this host must keep to a nightly schedule, it needs to be a
  machine that stays awake: check `pmset -g` and set `sleep 0` on a Mac that is meant to be a
  server. A laptop is a poor host for a nightly backup.
</Callout>

## Next [#next]

<Cards>
  <Card href="/docs/workers/configuration" title="Configuration" description="Every key the config file accepts." />

  <Card href="/docs/workers/troubleshooting" title="Troubleshooting" description="When the job loads and nothing runs." />
</Cards>
