No description
Find a file
Sophie Quinn-Graham 1e6ecea079 Run SeaweedFS cluster as macOS LaunchDaemons
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>
2026-09-27 14:27:00 +10:00
docs Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.admin.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.s3.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.server.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.volume0.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.volume1.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.volume2.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.volume3.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
local.seaweedfs.worker.plist Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
README.md Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
update-juniper.sh Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00
update-lila.sh Run SeaweedFS cluster as macOS LaunchDaemons 2026-09-27 14:27:00 +10:00

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 weed binary needs Full Disk Access to read its config and data on the external disks. See Grant Full Disk Access.
  • Every daemon uses RunAtLoad + KeepAlive with ThrottleInterval: 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

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

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

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

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

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

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

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

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
  1. Open System Settings → Privacy & Security → Full Disk Access. Use the list with switches; the Files & Folders view doesn't show whether it's on.
  2. If weed is 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:

  1. Lints the plist and checks the weed binary is there, i.e. the disk is mounted.
  2. Checks the service's port is free.
  3. Installs and loads the daemon.
  4. 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.
  5. Fails with instructions if the log shows operation not permitted or 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