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 juicefs binary (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.

ServiceWhat it doesPort
redisJuiceFS metadata authority (AOF persistence)30179
minioS3-compatible object store for JuiceFS chunks30151 (S3), 30152 (console)
juicefs-initOne-shot pre-flight checks + first-time volume format
juicefsLive FUSE mount + WebDAV (browse / smoke test)30180
juicemount-managerControl-plane web UI (migrations, trash, backups, maintenance, settings)30190

Watch it come up

stylized: the real check is docker compose ps
servicestate
redis healthy
minio healthy
juicefs-init exited (0), volume formatted
juicefs healthy, mounted, WebDAV on :30180
juicemount-manager healthy, web UI on :30190

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):

DatasetWhat it holdsSized for
bucketMinIO chunk storage, every byte of your mediaLarge (HDD pool fine)
redisRedis AOF/RDB, JuiceFS metadataSmall (a few GB) on SSD
cacheJuiceFS local chunk cacheFast SSD/NVMe, sized to your active project working set
manager-stateJSON file of job history, settings, destinations, schedulesUnder 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

terminal
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.

docker compose yaml, from server/INSTALL-TrueNAS.md
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.

terminal, on the NAS
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

30179 Redis: file metadata required
30151 MinIO S3: file data required
30152 MinIO console: web UI diagnostic
30180 WebDAV: browse the volume without the app diagnostic
30190 JuiceMount Manager: migrations, trash, backups, maintenance, settings admin
The mount itself needs only 30179 and 30151. The rest are how you look the stack in the eye. Over a WAN, your VPN needs to route at least the two required ports.

Check it from a browser

From the Mac, with <nas-ip> as your NAS’s LAN IP:

  • MinIO console: http://<nas-ip>:30152 should show a login page. Sign in with juicemount and your MINIO_ROOT_PASSWORD if you want to look around.
  • Manager web UI: http://<nas-ip>:30190 should load (if you included the service).
  • WebDAV: in Finder, press ⌘K and connect to http://<nas-ip>:30180. You should see the empty volume (a data and a shared folder 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.

CodeMeaning
2MinIO unreachable from inside the container network
3Redis PING failed
4Credentials empty, whitespace, or placeholder pattern: edit the YAML
5JM_BUCKET_URL missing the http:// scheme
6juicefs 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

terminal · on the Mac
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…

stylized: the real window walks you through fixing whichever row fails

Point the app at your NAS

The Setup Assistant (and later Preferences → Connection) wants two fields:

preferences → connection
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:

terminal
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 for offline

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.

offline-files mode

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.

write spool · opt-in

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.

instant search

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.

cache and disk

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.

migrate + maintain

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:

terminal, 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.

Your Mac and your NAS, each running the VPN, reach each other from any network. The tunnel carries the two required ports from step 3: 30179 (Redis) and 30151 (MinIO). JuiceMount does the storage and configuration; the tunnel does the routing.

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.

  1. 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.
  2. 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.
  3. 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:
preferences → connection, over the tunnel
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:

terminal
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.

terminal, from the cloned repo
./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.

what it removes
  • 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
what it deliberately leaves
  • /Applications/JuiceMount.app: drag to Trash yourself
  • The juicefs binary: 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.