I run an always-on homelab, a Mac mini, a Dell server on the LAN, a gaming PC, a UPS on every box. Files move between all three constantly, so the hot tier of the Dell used to be a Nextcloud mount over NFS. It worked, it was just slow, a 1 GiB copy from the Mac topped out around 33 MB/s write and 66 read.
I wanted faster, and I wanted it gone from Nextcloud's DB, so I put JuiceFS on the Dell instead. A Docker compose stack with a redis container for metadata, a one-shot format, and a privileged FUSE mount container exporting /srv/services/dellfs/mnt to the host. No S3 layer, my first attempt reached for Minio as the storage backend and that was the wrong turn, minio/minio is gone from every public registry, the hub repo is deleted and quay.io 401s. JuiceFS has a native --storage file mode, and a server with a real disk behind it wants that mode. Don't reach for a bucket when you have a filesystem.
The numbers after the swap, same Mac, same 1 GiB dd:
| path | write | read |
|---|---|---|
| Mac -> DellFS SMB, LAN :445 | 116 MB/s | 97 MB/s cold |
| Mac -> DellFS SMB, Tailscale | 36 MB/s | 61 MB/s |
| Dell native FUSE | 225 MB/s | 618 MB/s |
Three times the NFS ceiling over the wire I already used. The Tailscale leg is transport-limited, which is the correct outcome, away from the house the network is the bottleneck and the filesystem stops mattering.
The failures, in order
- The JuiceFS Hub tags are not what they look like.
juicedata/juicefs:*on Docker Hub are Docker plugin manifests, they fail to unpack as images. The image you actually want isjuicedata/mount, and itsCmdis[bash], so a composeexec-style command silently replaces the shell. I had to write it as:
command: [bash, -c, "exec juicefs mount ..."]
Samba served the share read-only by default.
read only = yesis the share default, and the symptom isNT_STATUS_ACCESS_DENIEDplus "rejected by share access mask" in the log. Two minutes oftestparmpinned it, my config had only[global]in it, because theservercontainers/sambaSAMBA_VOLUME_CONFIG_*value has to literally contain the[sharename]; ...string or smbd gets a headerless blob.macOS refused every authentication shape I tried first.
mount_smbfsaccepts exactly one form,//WORKGROUP;user:pass@host/share. Theuser%passform, the no-domain form, and the password-file form all fail auth, quoting permutations stop mattering once you read the man page, the parser is strict.A root-mounted SMB share shows up as
root:wheel 0700on the Mac, even when I mounted it as root viasufrom a daemon. A root-owned 0700 directory in/Volumesis invisible to the GUI session, so Finder can't see a thing. The fix is a split, a root launchd daemon that recreates and chowns the mount point every 15 seconds, plus a user-agent that runsmount_smbfsas the real session user every 30 seconds. The daemon loops instead of running once,diskarbitrationddeletes/Volumes/DellFSoutright after an unmount, so the mount point itself disappears and a one-shot daemon has nothing left to remount.Deleting a file over SMB threw "Operation not supported." JuiceFS plus
streams_xattrplus SMBunlinkis a bad combination. Deletion works from Finder and from the server side of the mount, the mount itself throws. I left that gap documented and moved on.The retirement nearly cost me the stack. Unmounting the old NFS tier left two references alive, the compose file still bind-mounted
/Volumes/DellHot, and the health script still tried to mount it. The bind mount made the OrbStack VM kernel spamkrpc ENOENTfor five days at about 100% CPU, and the health script'sErrno 13killed every health cycle. About 36 hours of a wedged homelab. I found the leftovers by grepping the retired path across the repo, the error never pointed there, and retiring an external mount means cleaning the compose bind, the health-script mount logic, and theocc files_externalentry in the same change.
What I'd do differently
Benchmark before you pick the backend. The native-file-storage form of JuiceFS was 3x faster than NFS over the same wire, and I was reaching for an S3-shaped stack because that's the shape most writeups lead with.
Test the mount as the session user first. Every macOS automounter bug I hit that week was a who owns the mount bug in disguise, and the fastest way to find that class of bug is to run the mount exactly the way the consumer runs it.
The stack runs headless now, redis for metadata, file storage on /data, FUSE mount propagated to the host, one host smbd share on :445 feeding the Mac and the Windows box, two launchd units plus one Windows scheduled task keeping the drive letters alive through reboots. Rollback is intact, the old NFS export and its contents are still on disk.
Hardware: Mac mini M4, Dell server on a LAN, OrbStack VM for the containers, everything on Tailscale. Benchmarks are dd of 1 GiB, cold cache, LAN, October 2026.
Top comments (1)
"Don't reach for a bucket when you have a filesystem" is the right instinct, and it's the mistake most object-storage-adjacent tooling nudges you into by default.
The Tailscale number is the honest one to publish. 36 MB/s over a tunnel next to 225 native looks bad and is exactly correct , measuring transport and filesystem separately is what makes the rest of the table believable.
Worth flagging that MinIO's registry disappearance is now turning up in several unrelated write-ups this month. Whatever the reason, the lesson is that a public image isn't a dependency you control, and a compose file pinned to minio/minio has a failure mode with nothing to do with your config.
The Cmd: [bash] trap deserves emphasis , a command that silently replaces rather than fails is the worst kind, because the container comes up healthy doing the wrong thing.