i bought a Mac Mini like a lot of people did for "openclaw" (lol).
last year i also over-sold the value of local LLMs for a few days and spent a few grand for no reason. but it worked out: with subagents + heavier MCP clients, the Mac Mini became my always-on local build machine, and i mostly "vibe code" from my MacBook into it.
the problem: macOS file sharing via SMB is ok for big files, but it's painfully slow for dev trees full of tiny files (TypeScript, config, node_modules). i switched the "live filesystem" part to NFS and tuned it until it was both fast on the LAN and survived my MacBook leaving the LAN — which turns out to be the harder half of the problem, and the part most NFS-on-macOS blogs get wrong.
why NFS over SMB (for this workload)
SMB on macOS can be rough on small-file workloads because:
- Finder and friends do extra metadata work (including extended attributes)
- SMB has more per-operation overhead (and macOS's implementation doesn't always feel optimized for "60k tiny files")
- directory listings devolve into a lot of little round trips
NFS is simpler. on my LAN, tuned NFS took "listing big dirs" from "why is this taking forever" to "ok, usable".
the setup i tested
- server: Mac Mini (apple silicon), wired LAN,
192.168.1.200 - client: MacBook (macOS), same network, roams on and off the LAN
- workload: ~60,000 files / ~8GB, mostly TypeScript + config + node_modules
- rtt: ~4ms (Wi-Fi → switch → ethernet)
pick your client profile before you tune anything
two wildly different recipes hide under the same "NFS client" label:
- wired workstation — always on, wired ethernet, never moves, never closes its lid. can be aggressive because nothing ever disappears.
- mobile laptop — sleeps, changes networks, walks into rooms without Wi-Fi. every client-side knob has to assume the server will disappear.
most blogs recommend workstation settings and call it a day. on a laptop that's how you get Raycast to crash, Finder to beachball, and launchd hammering your radio every minute forever. server tuning is the same for both; client tuning is where they split.
server (Mac Mini): exports + nfsd tuning
/etc/exports
what matters:
-alldirs: mount subdirectories, not only the export root-mapall=501:20: maps all client access to a single local uid/gid (convenient for single-user dev)-network ... -mask ...: restricts access to your local subnet
note: 501:20 is common on macOS (first user + staff), not guaranteed. swap for your own uid/gid via id -u / id -g.
/etc/nfs.conf (server)
quick "why" table:
| parameter | default | value | why |
|---|---|---|---|
| require_resv_port | 1 | 0 | macOS clients often mount from non-privileged ports; avoids silent mount pain |
| nfsd_threads | 8 | 16 | more concurrency for lots of small file ops |
| async | 0 | 1 | faster writes; fine for dev where git is the source of truth |
| fsevents | 1 | 0 | reduces server-side overhead |
| wg_delay / wg_delay_v3 | 1000 / 0 | 0 / 0 | lower latency for small-file writes |
| reqcache_size | 64 | 512 | better duplicate-request handling under retransmits |
| request_queue_length | 128 | 512 | avoids queue bottlenecks under bursts |
| export_hash_size | 64 | 256 | faster export lookup under load |
| bonjour | 1 | 0 | i connect by IP anyway |
enable the daemon:
client — wired workstation profile
mount at /Volumes/yigitkonur, with aggressive tuning and hard mounts. this profile assumes the server is never absent.
/etc/auto_nfs
/etc/auto_master
append (otherwise auto_nfs is ignored):
apply:
/etc/nfs.conf (client)
do not add nfs.client.is_mobile = 0. even on a wired machine, auto is the correct default — forcing it off gives you nothing here and is catastrophic if you ever move the recipe to a laptop.
mount options (what actually mattered)
| option | why |
|---|---|
| vers=3 | NFSv3 is the most stable on macOS. NFSv4 (macOS supports 4.0, not 4.1) has been flaky across releases |
| hard,intr | hard mounts don't fail with random I/O errors; intr lets you ctrl+c stuck ops |
| noresvport | common "why won't NFS mount on macOS" fix |
| nfc | unicode normalization correctness on macOS |
| locallocks | avoids NLM overhead and stale lock weirdness |
| nonegnamecache | avoids phantom ENOENT when files churn |
| rsize / wsize | bigger buffers, fewer trips for big reads/writes |
| noatime | avoids a write RPC on reads |
| actimeo=10 | fewer metadata RPCs; still reasonable freshness for dev |
| rdirplus | big win for large dirs: fetch attrs with directory entries |
the biggest-win knobs (workstation)
| parameter | default | value | why |
|---|---|---|---|
| nfs.client.access_for_getattr | 0 | 1 | single biggest perf win: merges permission checks into getattr calls (less RPC spam) |
| nfs.client.nfsiod_thread_max | 16 | 32 | more concurrent I/O for lots of small files |
| nfs.client.allow_async | 0 | 1 | makes the async mount option actually do something |
client — laptop profile (the one i actually run)
four things change from the workstation recipe. each one matters.
1. mount path: ~/mnt/mini, not /Volumes/anything.
this is the single most impactful change on a laptop. /Volumes is scanned constantly by Finder, Spotlight, Raycast, Dock, LaunchServices, Time Machine, and every backup tool you install. a hung mount under /Volumes spreads the hang to all of them — that's the Raycast crash and the Finder beachball. a hung mount under ~/mnt/ only affects processes that explicitly walked into it.
2. mount options: keep hard,intr, add short timeo + retrans, add nobrowse, drop deadtimeout.
hard,intr keeps data integrity intact while letting you ctrl+c stuck interactive ops. short timeo=30,retrans=3 (~90s budget per RPC) is the real change: on a dead server, processes bounce off the mount in seconds instead of the 10 minutes deadtimeout=600 buys you. nobrowse keeps Finder from enumerating the mount at all — belt-and-braces in case you ever put it under /Volumes anyway.
3. /etc/nfs.conf (client) — same as the workstation block, except you explicitly leave nfs.client.is_mobile alone.
the macOS default (auto) enables "auto-unmount unresponsive network volume on a laptop" behavior. setting it to 0 disables that safety net — that's the root cause of beachballs on disconnect, and the reason most "NFS on Mac" blogs produce a machine that's fast on the LAN and unusable off it. leave it at the default. don't even put the line in the file.
4. no reconnect LaunchDaemon. ever.
a StartInterval daemon that pings the server every 30–60s is the thing your Console.app will fill with "attaching network" events forever — whether you're home or in a café. on a laptop it's never the right shape. two alternatives:
on-demand automount — accessing ~/mnt/mini triggers the mount, idle unmounts release it:
/etc/auto_mini:
/etc/auto_master (append):
apply:
or manual — a tiny mount-mini you run when you actually want the share:
you run it when you want the share. you don't run it when you're on coffee-shop Wi-Fi that has nothing to do with your LAN.
optional macOS overhead to disable
don't disable Gatekeeper's quarantine as a "perf tweak". some recipes recommend defaults write com.apple.LaunchServices LSQuarantine -bool NO. that flag is not an NFS knob — it disables the quarantine prompt for all downloaded files, everywhere. the perf win is noise; the security impact is not. skip it.
security: don't hand-wave this
NFSv3 + sec=sys authenticates by uid/gid. no encryption, no signing, no Kerberos. the honest threat model: any machine on your LAN that can spoof uid 501 (trivial on any Mac they control) can read and write every file in your exported home directory.
that's fine on a wired home network with only your own devices. it's not fine on a network with guest Wi-Fi, IoT devices on the same subnet, a housemate, or anything resembling a coworking space. treat the share like an unlocked git remote — useful because nobody else is on the wire.
making it reboot-safe
server side (Mac Mini) — always-on, wired, doesn't move
optional: a tiny watchdog so the daemon comes back if it crashes.
root crontab:
client side (MacBook) — mobile, roams
nothing. no LaunchDaemon, no cron, no polling. the automount direct map above covers "mount on access, unmount when idle" without a retry loop. if the LAN isn't there when you cd ~/mnt/mini, you get a fast mount error in seconds instead of a ten-minute hang that takes Raycast and Finder with it.
the big lesson: don't rsync through the NFS mount
this surprised me at first, but the math is unforgiving for small files:
for 60,000 files, you're paying minutes of pure protocol overhead before any real work. for bulk transfer, stream it instead:
for me: 60,000 files in 1 min 57 sec. rsync-over-nfs was not close.
helper:
benchmark snapshot
healthy LAN, same test suite across tuning passes:
| test | original | after tuning | after research | total gain |
|---|---|---|---|---|
| create 1000 files (1–10KB) | 101.7s (9/s) | 84.3s (11/s) | 61.9s (16/s) | +78% |
| read 1000 files | 25.1s (39/s) | 20.2s (49/s) | 10.3s (97/s) | +149% |
| stat 1000 files | 2.8s (363/s) | 1.9s (533/s) | 1.9s (535/s) | +47% |
| overwrite 500 files | 31.3s (15/s) | 18.8s (26/s) | 10.5s (47/s) | +213% |
| ls -la (1000 files) | 42.3s | 7.5s | 4.4s | 9.6x |
biggest contributors on a healthy LAN:
nfs.client.access_for_getattr = 1actimeo=10+rdirplus(metadata efficiency)- bigger
rsize/wsizefor the occasional big file
the number not in this table is the one that predicts whether your laptop stays usable when the LAN goes away: "ls on the mount with the server unreachable." with the naive workstation settings applied to a laptop (is_mobile=0, hard,intr,deadtimeout=600, mount under /Volumes) that's a ten-minute beachball. with the laptop profile above it's a few seconds of ENOENT and you move on.
NFSv3 vs NFSv4 on macOS: don't bother with v4
- macOS supports NFSv4.0, not 4.1
vers=4.1often falls back to v3 anywayvers=4has had regressions on some Sonoma / Sequoia builds- tuned v3 is already very good for dev workloads
NFS vs SMB vs alternatives
| protocol | small file perf | setup complexity | laptop-friendly | macOS support |
|---|---|---|---|---|
| NFS (tuned) | good | medium | yes, with the laptop profile | stable with v3 |
| SMB | meh for small files | easy | yes (built-in reconnect) | supported, often sluggish |
| SSHFS | moderate | easy | yes | project status varies |
| Mutagen | often strong | medium | yes | active development |
| Syncthing | async sync | easy | yes | good |
the perf test script i used
drop this in /tmp/nfs-perftest.sh (edit NFS_TARGET to point inside your mount):
appendix: if you post to Craft.do via API (auto-make markdown safe)
if you have a "github-flavored" version with triple-backtick code fences, you can convert it to Craft.do-safe markdown by turning fenced code blocks into indented code blocks before you POST. here's the jq filter:
use it on the markdown string you're about to send (single-pass jq; don't round-trip through shell variables).
tldr
- if SMB feels slow for small files on macOS, try NFSv3 — server tuning is the same for everyone, client tuning splits by shape
- server side:
require_resv_port=0,nfsd_threads=16,async=1, TCP only - workstation client:
/Volumes/*,hard,intr,actimeo=10,rdirplus, automount direct map - laptop client:
~/mnt/*, same options plustimeo=30,retrans=3,nobrowse, leavenfs.client.is_mobileat its default, no reconnect LaunchDaemon - the single biggest perf knob is
nfs.client.access_for_getattr = 1; the single most dangerous setting on a laptop isnfs.client.is_mobile = 0 - don't disable Gatekeeper's
LSQuarantineas a "perf tweak" - NFSv3 +
sec=sysis uid/gid auth only; keep the share on a trusted subnet - for bulk copies use
tar | ssh, not rsync through the mount