- Shell 100%
launchd plists for juniper (server, volume0/1, admin, worker, s3) and lila
(volume2/3), plus install/update scripts for each host.
- Daemons run as root: with UserName=sophie, Local Network Privacy blocks
the connection to the master ("no route to host").
- Each weed binary needs Full Disk Access to read its config on the
external disk; without it the volume connects without TLS and the
master drops it.
- RunAtLoad + KeepAlive + ThrottleInterval retries until the external disk
is mounted at boot; KeepAlive/PathState never fired after a reboot.
- update-juniper.sh (all | <service> | --remove) and update-lila.sh install,
verify and report per service, and enable AutomountDisksWithoutUserLogin.
- docs/MIGRATION.md records the move from Terminal and the findings;
docs/MANUAL_STEPS.md has the manual equivalents of the scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|---|---|---|
| docs | ||
| local.seaweedfs.admin.plist | ||
| local.seaweedfs.s3.plist | ||
| local.seaweedfs.server.plist | ||
| local.seaweedfs.volume0.plist | ||
| local.seaweedfs.volume1.plist | ||
| local.seaweedfs.volume2.plist | ||
| local.seaweedfs.volume3.plist | ||
| local.seaweedfs.worker.plist | ||
| README.md | ||
| update-juniper.sh | ||
| update-lila.sh | ||
SeaweedFS Cluster launchd Configurations
This repository contains macOS launchd service definitions (.plist) to manage a multi-node SeaweedFS cluster as persistent background services.
All .plist units are LaunchDaemons (/Library/LaunchDaemons), so services start at boot without anyone logging in. Logs are written to /Users/sophie/Library/Logs/ on the internal disk.
- Every daemon runs as root. With
UserName: sophie, macOS Local Network Privacy blocks the connection to the master. - Each
weedbinary needs Full Disk Access to read its config and data on the external disks. See Grant Full Disk Access. - Every daemon uses
RunAtLoad+KeepAlivewithThrottleInterval: 10. If its disk isn't mounted yet at boot, the launch fails and launchd retries every 10 seconds until it is.
docs/MIGRATION.md explains how these choices were reached, including the alternatives that didn't work. docs/MANUAL_STEPS.md has the commands the install scripts run, for doing it by hand.
Cluster Topology
| Host | IP / Address | Service Plist | Description | Ports / Mount |
|---|---|---|---|---|
juniper |
10.2.10.202 |
local.seaweedfs.server.plist |
Master & Filer | Master: 9333, Filer: 8888 (/Volumes/SeaweedFS0) |
juniper |
10.2.10.202 |
local.seaweedfs.volume0.plist |
Volume Node 0 | Port: 8080, Rack: SeaweedFS0 (/Volumes/SeaweedFS0) |
juniper |
10.2.10.202 |
local.seaweedfs.volume1.plist |
Volume Node 1 | Port: 8081, Rack: SeaweedFS1 (/Volumes/SeaweedFS1) |
juniper |
10.2.10.202 |
local.seaweedfs.s3.plist |
S3 Gateway | Port: 8333 (files.tuatara-puffin.ts.net) |
juniper |
10.2.10.202 |
local.seaweedfs.admin.plist |
Admin Server | Web UI, port 23646 (default) |
juniper |
10.2.10.202 |
local.seaweedfs.worker.plist |
Cluster Worker | Maintenance / Background jobs |
lila |
Remote (10.2.10.85) |
local.seaweedfs.volume2.plist |
Volume Node 2 | Port: 8082, Rack: SeaweedFS2 (/Volumes/SeaweedFS2) |
lila |
Remote (10.2.10.85) |
local.seaweedfs.volume3.plist |
Volume Node 3 | Port: 8083, Rack: SeaweedFS3 (/Volumes/SeaweedFS3) |
Components
All components run as root with a soft open file limit of 65536.
1. SeaweedFS Server (local.seaweedfs.server.plist) — Node: juniper
- Working Directory:
/Volumes/SeaweedFS0/seaweedfs - Binary:
/Volumes/SeaweedFS0/seaweedfs/weed - Command Equivalent:
./weed server -dir=../seaweedfs-meta -filer -volume=false -master.defaultReplication=010 -master.volumeSizeLimitMB=1000 - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-server.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-server.stderr.log
- Stdout:
2. SeaweedFS Volume 0 (local.seaweedfs.volume0.plist) — Node: juniper
- Working Directory:
/Volumes/SeaweedFS0/seaweedfs - Data Directory:
/Volumes/SeaweedFS0/weed - Command Equivalent:
./weed volume -dir=../weed -max=3600 -master="10.2.10.202:9333" -port=8080 -rack=SeaweedFS0 - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-volume0.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-volume0.stderr.log
- Stdout:
3. SeaweedFS Volume 1 (local.seaweedfs.volume1.plist) — Node: juniper
- Working Directory:
/Volumes/SeaweedFS1/seaweedfs - Data Directory:
/Volumes/SeaweedFS1/weed - Command Equivalent:
./weed volume -dir=../weed -max=3600 -master="10.2.10.202:9333" -port=8081 -rack=SeaweedFS1 - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-volume1.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-volume1.stderr.log
- Stdout:
4. SeaweedFS Volume 2 (local.seaweedfs.volume2.plist) — Node: lila
- Working Directory:
/Volumes/SeaweedFS2/seaweedfs - Data Directory:
/Volumes/SeaweedFS2/weed - Command Equivalent:
./weed volume -dir=../weed -max=3600 -master="10.2.10.202:9333" -port=8082 -rack=SeaweedFS2 - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-volume2.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-volume2.stderr.log
- Stdout:
5. SeaweedFS Volume 3 (local.seaweedfs.volume3.plist) — Node: lila
- Working Directory:
/Volumes/SeaweedFS3/seaweedfs - Data Directory:
/Volumes/SeaweedFS3/weed - Command Equivalent:
./weed volume -dir=../weed -max=3600 -master="10.2.10.202:9333" -port=8083 -rack=SeaweedFS3 - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-volume3.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-volume3.stderr.log
- Stdout:
6. SeaweedFS S3 Gateway (local.seaweedfs.s3.plist) — Node: juniper
- Working Directory:
/Volumes/SeaweedFS0/seaweedfs - Command Equivalent:
./weed s3 -filer=10.2.10.202:8888 -externalUrl=https://files.tuatara-puffin.ts.net - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-s3.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-s3.stderr.log
- Stdout:
7. SeaweedFS Admin (local.seaweedfs.admin.plist) — Node: juniper
- Working Directory:
/Volumes/SeaweedFS0/seaweedfs - Command Equivalent:
./weed admin - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-admin.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-admin.stderr.log
- Stdout:
8. SeaweedFS Worker (local.seaweedfs.worker.plist) — Node: juniper
- Working Directory:
/Volumes/SeaweedFS0/seaweedfs - Command Equivalent:
./weed worker - Logs:
- Stdout:
/Users/sophie/Library/Logs/weed-worker.stdout.log - Stderr:
/Users/sophie/Library/Logs/weed-worker.stderr.log
- Stdout:
Install / Update
Copy this directory to the host, then run its script. The same command does a first install and an update: it replaces an already-loaded daemon, so re-run it after changing a plist.
Both scripts:
- Refuse to run on the wrong host.
- Turn on
AutomountDisksWithoutUserLogin, so the external disks mount at boot without anyone logging in. - Refuse to start a service if another copy is already running outside launchd, e.g. from Terminal, since the two would clash on ports.
- Verify each service after loading it, and exit with instructions if something's wrong.
1. Grant Full Disk Access
Do this once for each weed binary, before its first daemon starts:
| Host | Binary | Used by |
|---|---|---|
juniper |
/Volumes/SeaweedFS0/seaweedfs/weed |
server, volume0, admin, worker, s3 |
juniper |
/Volumes/SeaweedFS1/seaweedfs/weed |
volume1 |
lila |
/Volumes/SeaweedFS2/seaweedfs/weed |
volume2 |
lila |
/Volumes/SeaweedFS3/seaweedfs/weed |
volume3 |
- Open System Settings → Privacy & Security → Full Disk Access. Use the list with switches; the Files & Folders view doesn't show whether it's on.
- If
weedis listed, switch it on. Otherwise click +, press ⌘⇧G and add the binary's path.
If this is missing, the daemon starts but logs Reading security.toml: open security.toml: operation not permitted. Without security.toml it connects without TLS, and the master drops it (error reading server preface: EOF). That failed launch adds weed to the list switched off. Switch it on, then re-run the script.
Because weed is unsigned, macOS ties this permission to the exact binary file. After upgrading or replacing weed, remove it from Full Disk Access and add it again.
2. juniper: update-juniper.sh
scp -r * juniper:stage/
# on juniper:
~/stage/update-juniper.sh all # every service, in order
~/stage/update-juniper.sh volume1 # or just one
all goes through the services in dependency order and stops at the first failure: server (the master must be up for the volumes to register), volume0, volume1, admin, worker (which connects to the admin server), then s3. Updating server briefly takes the master and filer down; the other services reconnect by themselves.
For each service, the script:
- Lints the plist and checks the
weedbinary is there, i.e. the disk is mounted. - Checks the service's port is free.
- Installs and loads the daemon.
- Waits up to 60 seconds for a health check:
server: the master responds, and the filer lists your existing top-level directories.- Volumes: registered with the master.
admin/s3: the daemon is listening on a TCP port.worker: still up on its first run after 20 seconds.
- Fails with instructions if the log shows
operation not permittedor the daemon keeps restarting.
If the server check fails, stop it straight away. An empty filer listing means the filer couldn't read its config, usually because Full Disk Access is missing.
~/stage/update-juniper.sh --remove server # unloads and removes a daemon
3. lila: update-lila.sh
scp -r * lila:stage/
# on lila:
~/stage/update-lila.sh
This installs and loads both volume daemons. After 10 seconds, it shows each daemon's state and last log lines, and checks that 8082 and 8083 are registered with the master. It needs the master on juniper to be running.
Service Management
-
Check status & PID:
sudo launchctl list | grep local.seaweedfs -
Inspect detailed status (exit codes, parameters):
sudo launchctl print system/local.seaweedfs.volume3 -
Restart a service:
sudo launchctl kickstart -k system/local.seaweedfs.volume3 # or legacy: sudo launchctl stop local.seaweedfs.volume3 && sudo launchctl start local.seaweedfs.volume3 -
Unload a service (it starts again at next boot unless the plist is removed):
sudo launchctl bootout system/local.seaweedfs.<service> -
Follow logs:
# Node juniper: tail -f /Users/sophie/Library/Logs/weed-server.stderr.log tail -f /Users/sophie/Library/Logs/weed-volume0.stderr.log tail -f /Users/sophie/Library/Logs/weed-volume1.stderr.log tail -f /Users/sophie/Library/Logs/weed-s3.stderr.log tail -f /Users/sophie/Library/Logs/weed-admin.stderr.log tail -f /Users/sophie/Library/Logs/weed-worker.stderr.log # Node lila: tail -f /Users/sophie/Library/Logs/weed-volume2.stderr.log tail -f /Users/sophie/Library/Logs/weed-volume3.stderr.log