Documentation
Set up JuiceMount
From an empty NAS to a volume in Finder. You will confirm your NAS can run the server stack, bring it up with one compose file, verify your Mac can reach it, build and launch the menu-bar app, and mount. The NAS side takes about 10 minutes; the Mac side is a build from source and a Setup Assistant.
Self-hosted · macOS 14+ on the Mac · TrueNAS, Synology, QNAP, Unraid, or any Docker box on the NAS side · Apache-2.0
step 1 of 8
Check your NAS
JuiceMount has two halves: a server stack that runs on your NAS in Docker, and a menu-bar app on the Mac. Before installing anything, confirm both sides qualify.
- On the Mac
-
- macOS 14 (Sonoma) or later. Developed and tested on Apple Silicon; Intel Macs are untested; building from source should work, but no one has verified it.
- macFUSE, required by the JuiceFS client. The first-run Setup Assistant checks for it and walks you through installing it if it’s missing.
- The
juicefsbinary (brew install juicefs), auto-detected from/opt/homebrew/bin,/usr/local/bin,/usr/bin, then$PATH. - To build the app: Go 1.26+ and Xcode command-line tools (Swift 5.9+). Building from source is currently the only way to get the app. There’s no prebuilt, notarized DMG yet.
- An admin password prompt, once per session, the first time it mounts; step 5 explains why.
- On your NAS
-
- Docker and Docker Compose (or a NAS UI that runs compose apps, see the cards below).
- Disk for two things: the MinIO object bucket (your actual media) and a small Redis dataset (metadata, AOF-persisted).
- A LAN you trust: the stack’s Redis and MinIO ports are LAN-exposed by default; firewall them if untrusted clients share the network.
- Network: anything from hotel Wi-Fi (with pinned files and the write spool) up to 10 GbE. For WAN use the author runs Tailscale; any VPN that routes the Mac to the Redis and MinIO ports works.
Pick your install path
The same five-service compose file runs everywhere Docker does. What differs is how your NAS wants to receive it. One path is production-tested; the rest are honest pointers.
TrueNAS SCALE 24.10+ production-tested
The path this project’s QA actually exercises, on TrueNAS SCALE 24.10+ (Electric Eel, Fangtooth, Goldeye). The old “Add Catalog” feature was removed in 24.10, so you can’t sideload third-party catalogs anymore. The replacement is Apps → Discover → ⋮ menu → Install via YAML, which takes a Docker Compose YAML and runs it as a native TrueNAS app.
Have ready before step 2: three datasets (bucket, redis, cache) plus a small one for manager state, created under Datasets → Add Dataset. Step 2 has the sizing table.
Any Linux box with Docker + Compose first-class
Ubuntu, a laptop with Docker Desktop, an old tower: the compose file works as-is. You clone the repo, edit the bind-mount paths and the MinIO password, and run docker compose up -d. Step 2 has the exact commands.
Have ready: directories on your disks for the bucket, Redis data, and the JuiceFS cache.
Synology DSM 7+ vendor-generic
Synology DSM 7+ runs the stack through Container Manager. The repo lists this as a supported Docker host but does not document Synology’s UI: install Container Manager from Package Center, create a project from the same docker-compose.yml, and edit the bind-mount paths to folders on your volumes; consult Synology’s documentation for the exact menu path.
The compose file itself is identical; only the way you feed it to the NAS differs. Nothing here is Synology-tested by this project.
QNAP vendor-generic
Not covered by this project’s docs or QA. QNAP’s Container Station can run Docker Compose applications: create one from the same docker-compose.yml and edit the bind-mount paths to shares on your pools; consult QNAP’s documentation for the exact steps.
If you run it on QNAP, a report (success or failure) is a genuinely useful contribution.
Unraid vendor-generic
Not covered by this project’s docs or QA. Unraid runs compose stacks through a compose plugin from Community Applications: add the same docker-compose.yml as a stack and point the bind-mounts at your array paths; consult Unraid’s documentation for the exact steps.
As with QNAP, a field report is a real contribution.
Success for this step: you know which path you’re taking and where on your disks the bucket, Redis data, and cache will live.
step 2 of 8
Install the stack
One compose file boots the whole backend: Redis for metadata, MinIO for file data, a one-shot init that formats the JuiceFS volume, a live JuiceFS mount with WebDAV, and the JuiceMount Manager web UI.
| Service | What it does | Port |
|---|---|---|
redis | JuiceFS metadata authority (AOF persistence) | 30179 |
minio | S3-compatible object store for JuiceFS chunks | 30151 (S3), 30152 (console) |
juicefs-init | One-shot pre-flight checks + first-time volume format | — |
juicefs | Live FUSE mount + WebDAV (browse / smoke test) | 30180 |
juicemount-manager | Control-plane web UI (migrations, trash, backups, maintenance, settings) | 30190 |
Watch it come up
Settled. juicefs-init runs once, formats the volume, and exits 0; everything else stays up.
The order matters and compose enforces it: juicefs-init waits for Redis and MinIO to report healthy, and juicefs and the Manager wait for init to exit 0. On every boot after the first, init detects the already-formatted volume and skips the format.
TrueNAS SCALE: create datasets first
Create these in Datasets → Add Dataset and note their full paths (for example /mnt/tank/juicemount/bucket):
| Dataset | What it holds | Sized for |
|---|---|---|
bucket | MinIO chunk storage, every byte of your media | Large (HDD pool fine) |
redis | Redis AOF/RDB, JuiceFS metadata | Small (a few GB) on SSD |
cache | JuiceFS local chunk cache | Fast SSD/NVMe, sized to your active project working set |
manager-state | JSON file of job history, settings, destinations, schedules | Under 1 MB; any pool fine |
Optional, for migrating existing data: if a dataset already holds media you want to copy into the volume, note its path too. The Manager mounts it read-only inside the container so the copy can’t damage the source. You can add several.
Generate credentials
openssl rand -base64 24 # for MINIO_ROOT_PASSWORD
openssl rand -hex 32 # for JM_ADMIN_KEY (used to gate /manager UI)
Also note your NAS’s LAN IP; your Mac will need to reach that address on the published ports.
TrueNAS SCALE: paste the YAML
Apps → Discover → ⋮ menu → Install via YAML, paste the compose below, and edit every CHANGEME_* placeholder before submitting. The MinIO password appears twice, in the minio service and in juicefs-init, and the two must match.
services:
# ─── Redis: JuiceFS metadata authority ─────────────────────────
redis:
image: redis:7.4-alpine
restart: unless-stopped
ports:
- "30179:6379" # Mac client connects here
command:
- "redis-server"
- "--appendonly"
- "yes"
- "--appendfsync"
- "everysec"
- "--maxmemory-policy"
- "noeviction"
- "--save"
- "900"
- "1"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- CHANGEME_REDIS_PATH:/data # !!! EDIT:your Redis dataset path
# ─── MinIO: S3 object store for chunks ─────────────────────────
minio:
image: minio/minio:RELEASE.2025-01-20T14-49-07Z
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: juicemount
MINIO_ROOT_PASSWORD: CHANGEME_MINIO_PASSWORD # !!! EDIT:strong, no whitespace
MINIO_API_REQUESTS_DEADLINE: 10m
ports:
- "30151:9000" # Mac client connects here
- "30152:9001" # MinIO console: web UI
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
- CHANGEME_BUCKET_PATH:/data # !!! EDIT:your MinIO bucket dataset path
# ─── juicefs-init: one-shot with pre-flight checks ─────────────
juicefs-init:
image: juicedata/mount:ce-v1.3.1
restart: "no"
depends_on:
minio:
condition: service_healthy
redis:
condition: service_healthy
environment:
MINIO_ROOT_USER: juicemount
MINIO_ROOT_PASSWORD: CHANGEME_MINIO_PASSWORD # !!! EDIT:same as above
JM_BUCKET_URL: "http://minio:9000/zpool"
JM_META_URL: "redis://redis:6379/1"
JM_VOL_NAME: "zpool"
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
echo "[precheck-0] sleeping 2s for minio admin api warmup..."
sleep 2
echo "[precheck-1] minio reachable at $$JM_BUCKET_URL"
if ! curl -fsS -m 5 http://minio:9000/minio/health/live >/dev/null; then
echo "[precheck-1] FAIL: minio not reachable. Check MinIO container logs." >&2; exit 2
fi
echo "[precheck-2] redis ping"
if ! redis-cli -h redis ping 2>/dev/null | grep -q PONG; then
echo "[precheck-2] FAIL: redis not reachable. Check Redis container logs." >&2; exit 3
fi
echo "[precheck-3] credentials non-empty + no placeholder + no whitespace"
if [ -z "$$MINIO_ROOT_USER" ] || [ -z "$$MINIO_ROOT_PASSWORD" ]; then
echo "[precheck-3] FAIL: MINIO_ROOT_USER/PASSWORD must be non-empty" >&2; exit 4
fi
case "$$MINIO_ROOT_PASSWORD" in CHANGEME*|CHANGE*|REPLACE*|replaceme*)
echo "[precheck-3] FAIL: MinIO password looks like a placeholder. Edit the YAML." >&2; exit 4 ;;
esac
case "$$MINIO_ROOT_USER" in *' '*) echo "[precheck-3] FAIL: MINIO_ROOT_USER contains whitespace" >&2; exit 4 ;; esac
case "$$MINIO_ROOT_PASSWORD" in *' '*) echo "[precheck-3] FAIL: MINIO_ROOT_PASSWORD contains whitespace" >&2; exit 4 ;; esac
echo "[precheck-4] bucket URL well-formed"
case "$$JM_BUCKET_URL" in http://*|https://*) ;; *)
echo "[precheck-4] FAIL: bucket URL must start with http:// or https://" >&2; exit 5 ;;
esac
echo "[precheck-5] juicefs already formatted?"
if juicefs status "$$JM_META_URL" >/dev/null 2>&1; then
echo "[precheck-5] OK: volume already formatted; skipping format."
exit 0
fi
echo "[format] running juicefs format with bucket=$$JM_BUCKET_URL"
# SLICE 3 default: --trash-days 7. New installs format with
# JuiceFS's built-in trash retention enabled so the Manager
# UI's Trash tab has a useful default window. The "Upgrading
# from --trash-days 0" section below covers existing installs.
juicefs format --storage minio --bucket "$$JM_BUCKET_URL" \
--access-key "$$MINIO_ROOT_USER" --secret-key "$$MINIO_ROOT_PASSWORD" \
--trash-days 7 "$$JM_META_URL" "$$JM_VOL_NAME" || { echo "[format] FAIL: juicefs format errored" >&2; exit 6; }
echo "[format] complete"
# ─── juicefs: live FUSE mount + WebDAV (for browse / smoke test) ─
juicefs:
image: juicedata/mount:ce-v1.3.1
restart: unless-stopped
depends_on:
juicefs-init:
condition: service_completed_successfully
cap_add:
- SYS_ADMIN
devices:
- /dev/fuse
security_opt:
- apparmor:unconfined
ports:
- "30180:80" # WebDAV: Finder Cmd+K
healthcheck:
test: ["CMD", "sh", "-c", "mountpoint -q /jfs && curl -sf http://localhost:80/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
volumes:
- CHANGEME_CACHE_PATH:/jfs-cache # !!! EDIT:your fast-SSD cache dataset
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
echo "Mounting JuiceFS at /jfs..."
juicefs mount \
--cache-dir /jfs-cache \
--cache-size 100000 \
--buffer-size 4096 \
--prefetch 3 \
--backup-meta 3600 \
redis://redis:6379/1 \
/jfs &
sleep 5
until mountpoint -q /jfs; do sleep 1; done
echo "Mount ready. Starting WebDAV on :80..."
mkdir -p /jfs/data /jfs/shared
chmod 777 /jfs/data /jfs/shared
juicefs webdav --cache-dir /jfs-cache --cache-size 100000 \
redis://redis:6379/1 0.0.0.0:80 &
wait
# ─── juicemount-manager: control-plane web UI for the JuiceFS volume ─
# (Renamed from juicemount-migrator in SLICE 0 of the manager
# roadmap. The legacy juicemount-migrator image tag is still
# published for one release as a compat alias, but new installs
# should use juicemount-manager.)
#
# Browse host paths under /sources, pick a destination under /jfs,
# watch live progress with real % bar (driven by juicefs's Prometheus
# metrics). Bind-mount as many existing datasets read-only as you
# want. Omit this service entirely if you have no existing data.
juicemount-manager:
image: ghcr.io/lelanddutcher/juicemount-manager:production-hardening
pull_policy: always
restart: unless-stopped
depends_on:
juicefs-init:
condition: service_completed_successfully
environment:
JM_META: "redis://redis:6379/1"
JM_VOL_NAME: "zpool"
JM_SOURCE_ROOTS: "/sources"
JM_ADMIN_KEY: CHANGEME_ADMIN_KEY # !!! EDIT:32+ random chars; empty = LAN-only
JM_STATE_FILE: "/var/lib/manager/state.json" # state persists across restart
ports:
- "30190:8080" # web UI
volumes:
# One bind-mount per existing dataset you want to migrate from.
# Each becomes a browsable source root in the UI.
- CHANGEME_SOURCE_PATH:/sources/<your-source-name>:ro
# Small writable mount for the JSON state file. Without this,
# state vanishes on container restart (no Resume button
# available for canceled jobs after a redeploy).
- CHANGEME_STATE_PATH:/var/lib/manager
The shipped YAML names the volume zpool; that name becomes your mount point on the Mac, /Volumes/zpool. Want Finder to say something nicer? Set JM_VOL_NAME: "JuiceMount" before the first docker compose up and the volume mounts at /Volumes/JuiceMount. The name is yours, but pick it once: it's baked in at format time. If you have no existing data to migrate, you can omit the juicemount-manager service entirely.
Any Docker host: clone and compose
On Ubuntu, Synology, QNAP, Unraid, or a laptop, use the repo’s compose file directly. Its default bind-mount paths are TrueNAS-flavored (/mnt/zSSD/...); change them to wherever your data should live.
git clone https://github.com/lelanddutcher/juicemount
cd juicemount/server
# Edit docker-compose.yml: set the bind-mount paths for your disks
# and a strong MINIO_ROOT_PASSWORD (openssl rand -base64 24).
docker compose up -d
docker compose ps # wait for all services healthy
docker compose logs juicefs-init # confirms first-time volume format
What success looks like
docker compose ps shows every long-running service healthy, and the juicefs-init log (docker compose logs juicefs-init) ends with a successful format line on first boot (or reports the volume is already formatted on any boot after that) with no FAIL in it. The exact wording differs between the TrueNAS YAML above and the repo’s docker-compose.yml, but in both, format lines are prefixed [format] and every pre-flight line is prefixed [precheck-N] for easy grepping. On TrueNAS the same log appears under Apps → Logs.
step 3 of 8
Verify it’s reachable
Before touching the Mac app, prove two things: the stack is actually up, and your Mac can reach the NAS on the published ports.
The ports your Mac must reach
Check it from a browser
From the Mac, with <nas-ip> as your NAS’s LAN IP:
- MinIO console:
http://<nas-ip>:30152should show a login page. Sign in withjuicemountand yourMINIO_ROOT_PASSWORDif you want to look around. - Manager web UI:
http://<nas-ip>:30190should load (if you included the service). - WebDAV: in Finder, press ⌘K and connect to
http://<nas-ip>:30180. You should see the empty volume (adataand asharedfolder on a fresh install). This is the smoke test that the JuiceFS volume itself is alive.
If the init container exits non-zero
Each exit code names the failure. Read the matching [precheck-N] log line for detail.
| Code | Meaning |
|---|---|
| 2 | MinIO unreachable from inside the container network |
| 3 | Redis PING failed |
| 4 | Credentials empty, whitespace, or placeholder pattern: edit the YAML |
| 5 | JM_BUCKET_URL missing the http:// scheme |
| 6 | juicefs format itself errored; read the log line above it for the JuiceFS-side reason |
Security notes before you move on
The Redis port (30179) is exposed on the LAN. Anyone who can reach the NAS on that port can read and write the entire JuiceFS metadata, equivalent to total volume loss. Confine it with host firewall rules if untrusted clients share your network. The MinIO console (30152) is HTTP-only, so don’t expose it to public IPs. MINIO_ROOT_PASSWORD is the master key to every byte in the volume. And the Manager’s JM_ADMIN_KEY gates write access to its HTTP API: empty means LAN-only, no auth, which is fine for a home NAS; set a 32+ character key for anything internet-reachable.
step 4 of 8
Install the Mac app
Two Homebrew installs, a clone, and a build script. Building from source is currently the only way to get the app.
Build and install
brew install juicefs
brew install --cask macfuse # approve the system extension if macOS asks
git clone https://github.com/lelanddutcher/juicemount
cd juicemount
./scripts/build-app.sh # Go c-archive + Swift app + codesign
./scripts/install.sh # → /Applications (add --launchd for login start)
open /Applications/JuiceMount.app
A locally built app is not quarantined, so Gatekeeper won’t object. If you instead obtained a pre-built JuiceMount.app from someone else (unsigned or ad-hoc-signed), macOS will block it: remove the quarantine flag with xattr -d com.apple.quarantine /Applications/JuiceMount.app, or launch once and approve it under System Settings → Privacy & Security → Open Anyway. The right-click-Open trick no longer bypasses Gatekeeper on macOS 15 and later.
First launch: the Setup Assistant
On first launch the Setup Assistant opens automatically and pre-flights the three things a mount depends on. You can reopen it any time from the menu-bar icon → Setup Assistant…
Point the app at your NAS
The Setup Assistant (and later Preferences → Connection) wants two fields:
Redis URL: redis://<nas-ip>:30179/1
S3 Endpoint Override: http://<nas-ip>:30151/zpool
The S3 endpoint override is needed when the volume was formatted with a docker-internal hostname, which the shipped YAML does (http://minio:9000/zpool), so set it. MinIO credentials live in the JuiceFS volume’s format metadata in Redis; they don’t need to be re-entered on the Mac. Then click Save.
step 5 of 8
First mount
Hit Start in the popover. Enter your admin password at the mount prompt. /Volumes/zpool appears in Finder.
What the admin prompt is about
macOS restricts mount_nfs and umount to root, so the app escalates through the standard macOS authorization dialog the first time it mounts, once per session; macOS caches the authorization. If you restart the app often or run it headless, the repo’s docs/dev-setup.md sets up a scoped passwordless-sudo rule (exactly /sbin/mount_nfs, /sbin/umount, /bin/mkdir, no shell, no wildcards) and the app probes for it and uses it automatically.
What success looks like
The volume shows up under Locations in Finder at /Volumes/zpool (the mount point derives from the volume name in Preferences → Connection). Point your NLE’s media browser at it and edit. Every Mac that mounts the volume sees the same absolute paths, so a teammate’s .prproj, .drp, or .fcpx opens without relinking, with the honest caveat that heavy simultaneous multi-editor use hasn’t been soak-tested yet.
The menu-bar states
The menu-bar icon is the status system; the tint is the signal. These are the marks the app ships:
-
Green: healthyAll systems healthy: Redis, MinIO, FUSE, and the NFS mount all reachable. At 50% opacity it means idle: the server isn’t started.
-
Amber: degradedRunning, but a backend (Redis / MinIO / FUSE / NFS) is unhealthy or recovering. The popover names which one and why.
-
Blue: offline-files modePinned files served locally at SSD speed; un-pinned reads fail fast instead of hanging.
-
Red: faultUnreachable, start failed, or disconnected. The failure message surfaces in the popover.
A small blue up-arrow badge appears bottom-right of the mark while spool uploads are draining.
If the volume doesn’t appear: open the popover. If the NFS row says “Volume not mounted”, click Mount Now, a privileged re-mount that may show the admin prompt once. Also check that something else doesn’t already own the path:
mount | grep <volume-name>
step 6 of 8
Make it yours
Mounted is the baseline. These are the habits that make it an editing drive: pin what you’re cutting, flip offline mode when you leave, and decide whether writes should spool.
Pin folders for offline
Popover → Pin Folder for Offline…, or right-click a folder in Finder → Services → JuiceMount: Pin for Offline. A prefetcher pulls every byte to local SSD and shows per-folder progress in the popover.
macOS hides Services items by default. Enable once in System Settings → Keyboard → Keyboard Shortcuts → Services → Files and Folders, check “JuiceMount: Pin for Offline”. May need a killall Finder to refresh.
Flip offline mode when you leave
One toggle in the popover (cellular, plane, NAS down): pinned files keep reading at SSD speed; un-pinned reads refuse in 4–67 ms instead of hanging Finder on a ~30 s NFS retry. Back online, Sync Now runs verify-and-repair on the pin set, re-fetching anything the cache evicted.
Decide whether writes spool
Preferences → Cache & Storage → Enable write spool. On: writes ack the moment they’re durable on local SSD, then trickle-upload in the background, SHA-256-verified at every hop; the popover shows pending uploads until they drain. Off (the default): writes go through synchronously, like any network drive. Note offline-files mode gates reads; it doesn’t make un-spooled writes safe.
Search the whole library
⌘⇧F from any app opens search across the indexed volume: results in tens of milliseconds at 100 K+ entries. Spacebar for Quick Look, Enter to reveal in Finder, or drag results straight into a Premiere, Resolve, or FCPX timeline. Toggleable in Preferences if another app owns the hotkey.
Set the cache, reclaim space
Set the cache size in Preferences → Cache & Storage. JuiceMount grows the cache only as far as needed to keep pinned content fully cached and never squeezes the boot disk below a hard 10 GiB free floor. The popover’s disk row has a Reclaim button that thins Time Machine local snapshots (APFS purgeable space), so “disk full” usually isn’t.
Move your library in, keep the stack current
Already have terabytes on the NAS? Open the Manager at http://<nas-ip>:30190 → Migrations tab: browse a read-only source, pick a destination, watch live progress. Junk files (.DS_Store, ._*, Thumbs.db, .sync.ffs_db) are excluded and permissions are not preserved by default, so files land readable on the Mac; jobs queue and run sequentially. Deletes on new installs go to JuiceFS trash for 7 days, browsable in the Manager’s Trash tab.
To update the stack later, on the NAS:
cd server
git pull
docker compose pull # pull newer service images
docker compose up -d # restart with no data loss
Data lives in the host paths from your volumes: lines, so container churn never touches it. And remember what this isn’t: a backup. It’s primary storage with a cache; run real backups of the MinIO bucket and Redis.
step 7 of 8
Working remotely
The steps above did the storage side: the volume, the cache, pins, the spool. What JuiceMount deliberately doesn’t do is routing: when you leave the LAN, your Mac needs a route back to the NAS, and that’s a VPN’s job. The author runs Tailscale; any VPN that gives your Mac a route to the NAS’s Redis and MinIO ports works the same way, whether WireGuard, OpenVPN, or your router’s VPN.
Three steps with Tailscale
Tailscale is a third-party mesh VPN, free for personal use, not affiliated with this project. It’s what the author runs because it removes the routing work: install it on both machines and they can reach each other from anywhere.
- Install Tailscale on both ends: on the NAS (or whatever box runs the Docker stack) and on the Mac. Signed in to the same account, the two machines can reach each other from any network.
- Note the NAS’s Tailscale address. Tailscale gives each machine an address that stays stable wherever your Mac roams; copy the NAS’s from the Tailscale app or admin console.
- Point the app at that address. In Preferences → Connection (or the Setup Assistant), connect using the NAS’s Tailscale address instead of its LAN IP, the same two fields from step 4:
Redis URL: redis://<tailscale-address>:30179/1
S3 Endpoint Override: http://<tailscale-address>:30151/zpool
Whatever VPN you pick, the test is the same as step 3: if the Mac can reach 30179 and 30151 on the NAS, the mount works. The diagnostic ports (30152, 30180, 30190) come along too, if your VPN routes them.
What remote actually feels like
- Pinned files play at SSD speedPin the project before you leave (step 6): every byte is already on local SSD, so playback doesn’t touch the link at all.
- First-touch reads stream at link speedUn-pinned files come over the tunnel as you touch them: scrub a 100 GB file and only the blocks you touch cross the wire, at whatever the connection gives, hotel Wi-Fi included.
- Writes land locally and upload behind youWith the write spool on (opt-in, step 6), a write acks the moment it’s durable on local SSD, then trickle-uploads at whatever the network allows; the popover shows pending uploads until they drain.
- Offline mode refuses fastIf the link is gone entirely, flip offline-files mode: pinned files keep reading, and un-pinned reads fail in 4–67 ms instead of hanging Finder on a ~30 s NFS retry.
Success for this step: the volume mounts with the NAS’s Tailscale address in Preferences → Connection, from a network that isn’t your LAN.
step 8 of 8
If something breaks
Open the popover first. The health rows (Redis, MinIO, FUSE, NFS mount) are the first thing to check; most fixes start there.
Symptoms and fixes
The volume doesn’t appear in Finder
If the popover’s NFS row says “Volume not mounted”, click Mount Now, a privileged re-mount that may show the admin prompt once. If the prompt itself is the obstacle (headless Mac, automated restarts), set up the scoped sudoers rule from docs/dev-setup.md. Also check that something else doesn’t already own the path with mount | grep <volume-name>.
Finder says “not responding”, or the icon turns amber
Amber means degraded: running, but a backend is unhealthy or recovering, and the popover names which one and why. Give it a moment: the health monitor force-remounts a wedged FUSE daemon once the backend is reachable again (about 15 s after the network returned, in the controlled long-outage repro). If the kernel mount itself is wedged (server died, every Finder access hangs), Force Eject in the popover is the last resort: a privileged kernel-level unmount behind a confirmation dialog, after which in-flight operations fail with I/O errors rather than hanging.
Uploads look stuck
With the spool enabled, the popover’s Pending uploads section shows pending, in-flight, stalled, and failed counts with per-entry age and last error; Retry failed and Recover stalled act on them directly. A full spool surfaces to Finder as “disk full” rather than a mystery error.
Search returns no results
Click Sync Now in the popover; the search index rebuilds at the end of every sync. Verify the metadata cache has entries: the popover should show an entry count above zero.
The menu-bar icon doesn’t appear
The app has no Dock icon by design; look at the right side of the menu bar. If Activity Monitor shows a JuiceMount process but no icon, refresh the menu bar with killall SystemUIServer.
“Damaged or unsigned” warning on launch
Locally built apps are not quarantined, so this shouldn’t appear after ./scripts/build-app.sh. It appears when the app was downloaded; browsers and AirDrop add the quarantine attribute. Fix with xattr -d com.apple.quarantine /Applications/JuiceMount.app, or approve under System Settings → Privacy & Security → Open Anyway.
Server-side: init exit codes, empty migrations, unreadable files
juicefs-init exit codes are in step 3: code 2 is MinIO unreachable, 3 is Redis, 4 is bad credentials, 5 is a malformed bucket URL, 6 is the format itself. Two more from the field: a Manager copy that reports “0 files / 0 B” means a stale image, so pull ghcr.io/lelanddutcher/juicemount-manager:production-hardening and redeploy. And if the Mac can’t open migrated files, the source had restrictive permissions; either un-tick Preserve permissions before migrating, or chmod -R u+rwX,g+rwX,o+rX on the destination. docker compose logs <service> has the full startup output of any container.
You want to re-run first-time setup
Menu-bar icon → Setup Assistant… reopens onboarding any time: it pre-flights juicefs, macFUSE, and backend reachability, and re-points the app at your NAS, the same fields as Preferences → Connection.
Logs and diagnostics
Structured JSON logs live at ~/Library/Logs/JuiceMount/juicemount.log (16 MB × 5 rotation); the JuiceFS daemon’s own log is auto-tailed into it with warnings promoted. For live debugging:
tail -f ~/Library/Logs/JuiceMount/juicemount.log | jq .
For a bug report, use Export Diagnostics… (in the popover and in Preferences → Maintenance): it bundles logs, the mount table, and backend health into a local zip; nothing is sent anywhere.
when you leave
Uninstalling
One script removes JuiceMount’s per-user state from the Mac. It is careful by design: it stops the app, unmounts, shows exactly what it will remove with sizes, and asks once before touching anything.
./scripts/uninstall.sh # interactive
./scripts/uninstall.sh --dry-run # show the plan, change nothing
./scripts/uninstall.sh --yes # skip the main confirmation
./scripts/uninstall.sh --yes --delete-pending-uploads # fully unattended
The spool check matters. If the write spool still holds files, those are writes that were acknowledged to Finder but never finished uploading to your NAS, so deleting them is permanent data loss. The script requires its own explicit confirmation for that step (or the --delete-pending-uploads flag). To drain them instead: relaunch JuiceMount, enable the spool, and wait for pending uploads to reach zero.
- The LaunchAgent (
~/Library/LaunchAgents/com.juicemount.agent.plist) - The sudoers rule (
/etc/sudoers.d/juicemount-mount, the one step that asks for sudo) - App state at
~/.juicemount: the local metadata cache (rebuilt from Redis on a future reinstall) and the pin list, which lives here too and is not recovered - App support at
~/Library/Application Support/JuiceMount, including the spool - Logs at
~/Library/Logs/JuiceMount - The JuiceFS chunk cache at
~/.juicefs/cache, usually the big one; it can be hundreds of GB, and the size is shown before you confirm - The app’s UserDefaults
/Applications/JuiceMount.app: drag to Trash yourself- The
juicefsbinary:brew uninstall juicefs - macFUSE: remove via System Settings or its installer
- Everything on your NAS: Redis and MinIO are untouched; your media stays where it is
Your bytes were never locked in to begin with: file data sits in JuiceFS’s open, documented chunk format in your bucket, and the stock juicefs client can mount the volume with no JuiceMount involved.