# Jellyfin + macOS: Persistent NFS Mount (Fix for Libraries Randomly “Clearing”) This README documents the working fix I applied when Jellyfin (on a Mac mini) periodically “lost” or cleared my Movies/TV libraries that live on a NAS mounted over NFS. It includes the exact commands, files, and rationale so I can reproduce it later. --- ## Problem Summary - Symptom: Every day or two, Jellyfin showed empty Movies/TV libraries. - Media location: NFS share at `/Volumes/media` from NAS `192.168.50.105:/media`. - Root cause: macOS was using autofs (`/- /etc/auto_nfs`). autofs can unmount after inactivity or brief network blips. When the mount disappears during a Jellyfin scan/file-watch, Jellyfin sees files as missing and removes them from its DB. ## Solution Summary - Stop using autofs for this path. - Create a persistent mount at boot using a LaunchDaemon and a small network‑aware mount script. - The script: - Is idempotent: does nothing if already mounted. - Checks NAS reachability first. - Logs to `/var/log/mount_media.(out|err)`. - Optionally restarts Jellyfin (Homebrew service) if the mount comes back. --- ## Prerequisites / Assumptions - macOS with admin (sudo) access. - NFS server: `192.168.50.105` exporting `/media` (adjust as needed). - Mount point: `/Volumes/media` (adjust as needed). - Jellyfin installed (optional Homebrew service restart in script). > Tip: If your NAS requires privileged source ports for NFSv4, `resvport` helps. The script falls back to `noresvport` if needed. --- ## Steps (copy/paste commands) ### 1) Disable autofs for this path and unmount any automounted share ``` # Backup and comment out the direct map for NFS sudo cp /etc/auto_master /etc/auto_master.bak.$(date +%F_%H%M%S) sudo sed -i.bak 's|^/- /etc/auto_nfs|#/- /etc/auto_nfs|' /etc/auto_master # Reload automountd (will unmount /Volumes/media if it was automounted) sudo automount -vc # Ensure the mountpoint is not currently mounted (ignore errors if already unmounted) sudo umount /Volumes/media 2>/dev/null || sudo umount -f /Volumes/media 2>/dev/null || true ``` > Note: If `chown`/`chmod` say “Operation not permitted,” the path is still mounted (or your NAS has root-squash). Unmount first. --- ### 2) Create the network‑aware mount script ``` sudo mkdir -p /usr/local/sbin sudo tee /usr/local/sbin/mount_media_nfs.sh > /dev/null <<'SH' #!/bin/sh set -eu LOG="/var/log/mount_media.out" ERR="/var/log/mount_media.err" MOUNT="/Volumes/media" # SMB server settings — use domain name (FQDN) HOST="nas.example.local" # <- change to your domain SHARE="media" # <- change share name if needed # Optional auth: # - If SMB_USER is set, script will try authenticated mount. # - Supply SMB_PASS (environment) OR set SMB_KEYCHAIN_ITEM to fetch password from Keychain. SMB_USER="${SMB_USER:-}" SMB_PASS="${SMB_PASS:-}" SMB_KEYCHAIN_ITEM="${SMB_KEYCHAIN_ITEM:-}" # Ensure mountpoint exists [ -d "$MOUNT" ] || mkdir -p "$MOUNT" # If already mounted on the mountpoint, exit quietly if mount | awk '{print $3}' | grep -qx "$MOUNT"; then echo "$(date) already mounted: $MOUNT" >>"$LOG" exit 0 fi # Preflight: only try to mount when SMB port is reachable (try 445 then 139) if ! ( /usr/bin/nc -G 2 -z "$HOST" 445 >/dev/null 2>&1 || /usr/bin/nc -G 2 -z "$HOST" 139 >/dev/null 2>&1 ); then echo "$(date) NAS not reachable on SMB ports (445/139), skipping mount" >>"$LOG" exit 0 fi # Helpful server listing for debugging (doesn't include credentials) echo "$(date) smbutil listing for debugging" >>"$LOG" 2>>"$ERR" smbutil view "//$HOST" >>"$LOG" 2>>"$ERR" || true smbutil view "//guest@$HOST" >>"$LOG" 2>>"$ERR" || true # Helper: function to verify mount and restart Jellyfin if needed verify_and_exit() { if mount | awk '{print $3}' | grep -qx "$MOUNT"; then echo "$(date) mount OK: $MOUNT" >>"$LOG" if command -v brew >/dev/null 2>&1 && brew services list | grep -q '^jellyfin\b'; then echo "$(date) restarting Jellyfin (brew services)" >>"$LOG" brew services restart jellyfin >>"$LOG" 2>>"$ERR" || true fi exit 0 fi } # Try authenticated mount if SMB_USER provided if [ -n "$SMB_USER" ]; then # Retrieve password from Keychain if requested and SMB_PASS not set if [ -z "$SMB_PASS" ] && [ -n "$SMB_KEYCHAIN_ITEM" ]; then # Try to read password from Keychain (service name = SMB_KEYCHAIN_ITEM, account = SMB_USER) # The -w flag prints only the password SMB_PASS="$(security find-generic-password -s "$SMB_KEYCHAIN_ITEM" -a "$SMB_USER" -w 2>/dev/null || true)" fi if [ -n "$SMB_PASS" ]; then # Use password via stdin to avoid exposing it in process list echo "$(date) attempting authenticated mount as user '$SMB_USER' -> $MOUNT" >>"$LOG" # Do NOT include the password in the URL or logs. MOUNT_URL="//${SMB_USER}@${HOST}/${SHARE}" # Send password followed by newline to mount_smbfs which will read it from stdin if printf '%s\n' "$SMB_PASS" | /sbin/mount_smbfs "$MOUNT_URL" "$MOUNT" >>"$LOG" 2>>"$ERR"; then verify_and_exit else echo "$(date) authenticated mount attempt FAILED" >>"$ERR" # Fall through to guest attempts fi else # No password available for authenticated mount echo "$(date) SMB_USER set but no SMB_PASS or Keychain entry found -> will try guest" >>"$LOG" fi fi # If we reach here, try guest access (null/anonymous session) echo "$(date) trying guest/null session (mount_smbfs -N) -> $MOUNT" >>"$LOG" if /sbin/mount_smbfs -N "//$HOST/$SHARE" "$MOUNT" >>"$LOG" 2>>"$ERR"; then verify_and_exit fi echo "$(date) trying explicit guest user (guest@$HOST) -> $MOUNT" >>"$LOG" if /sbin/mount_smbfs "//guest@$HOST/$SHARE" "$MOUNT" >>"$LOG" 2>>"$ERR"; then verify_and_exit fi # If we reached here, all attempts failed echo "$(date) ALL SMB mount attempts FAILED" >>"$ERR" echo "------ smbutil status ------" >>"$ERR" smbutil statshares -a >>"$ERR" 2>&1 || true echo "------ mount table ------" >>"$ERR" mount >>"$ERR" 2>&1 || true exit 1 SH sudo chmod 755 /usr/local/sbin/mount_media_smb.sh ``` --- ### 3) Create the LaunchDaemon (mount at boot, re-check periodically, network‑aware) ``` sudo tee /Library/LaunchDaemons/com.local.mountmedia.plist > /dev/null <<'PLIST' Label com.local.mountmedia ProgramArguments /usr/local/sbin/mount_media_smb.sh RunAtLoad StartInterval 300 KeepAlive NetworkState StandardOutPath /var/log/mount_media.out StandardErrorPath /var/log/mount_media.err PLIST sudo chown root:wheel /Library/LaunchDaemons/com.local.mountmedia.plist sudo chmod 644 /Library/LaunchDaemons/com.local.mountmedia.plist sudo plutil -lint /Library/LaunchDaemons/com.local.mountmedia.plist sudo launchctl bootout system /Library/LaunchDaemons/com.local.mountmedia.plist 2>/dev/null || true sudo launchctl bootstrap system /Library/LaunchDaemons/com.local.mountmedia.plist sudo launchctl enable system/com.local.mountmedia sudo launchctl kickstart -k system/com.local.mountmedia ``` --- ### 4) Run once and verify ``` # Run once now (idempotent; logs "already mounted" if present) sudo /usr/local/sbin/mount_media_smb.sh # LaunchDaemon status sudo launchctl print system/com.local.mountmedia | egrep 'state|last exit|PID' || true # Mount status (should NOT say "automounted") mount | grep " on /Volumes/media " # SMB mount parameters smbstat -m | sed -n '/\/Volumes\/media/,+12p' # Script logs tail -n 100 /var/log/mount_media.out /var/log/mount_media.err 2>/dev/null || true ``` --- ### 5) Jellyfin settings - Temporarily disable “Enable real-time monitoring” for libraries under `/Volumes/media` until you confirm the mount stays stable. - Then run “Scan Library Files” to repopulate anything previously removed. --- ### 6) Reboot test (recommended) ``` sudo shutdown -r now ``` After reboot: ``` mount | grep " on /Volumes/media " || echo "Not mounted yet" sudo launchctl print system/com.local.mountmedia | egrep 'state|last exit' || true tail -n 100 /var/log/mount_media.out /var/log/mount_media.err 2>/dev/null || true ``` --- ## Rationale for Key Choices - Persistent mount (LaunchDaemon) instead of autofs: - autofs can unmount after inactivity; Jellyfin then removes items it thinks are gone. - LaunchDaemon ensures the mount is present before scans and remains mounted. - smb options: - `hard`: Blocks I/O until server responds, avoiding spurious “file missing” errors. - `nfsvers=4.0`: Matches typical NAS defaults and the client’s chosen version. - `resvport` then fallback `noresvport`: Some servers require privileged ports; the script tries both. - Network preflight: - Check TCP/2049 reachability to avoid “Network is unreachable” failures (exit code 51) at boot or during link flaps. - Logging: - `/var/log/mount_media.out` and `.err` make it easy to correlate with Jellyfin logs. --- ## Troubleshooting - “Operation not permitted” when `chown`/`chmod`: - The path is mounted over NFS, and root-squash likely prevents ownership changes from the client. Unmount first or change ownership on the NAS. - LaunchDaemon errors: - Validate plist: `sudo plutil -lint /Library/LaunchDaemons/com.local.mountmedia.plist` - Service state: `sudo launchctl print system/com.local.mountmedia` - Mount health: - `nfsstat -m` should show vers=4.0, hard, resvport/noresvport. - Network/power: - Prevent system sleep that drops the NIC; enable “Wake for network access.” --- ## Optional: If you must keep autofs Increase the autofs timeout so it doesn’t unmount on brief inactivity (less ideal than the LaunchDaemon approach): ``` sudo cp /etc/auto_master /etc/auto_master.bak.$(date +%F_%H%M%S) sudo sed -i.bak -E 's|^/-[[:space:]]+/etc/auto_nfs$|/- -timeout=604800 /etc/auto_nfs|' /etc/auto_master sudo automount -vc ``` --- ## Reverting To revert to autofs: ``` # Stop and remove LaunchDaemon sudo launchctl bootout system /Library/LaunchDaemons/com.local.mountmedia.plist sudo rm -f /Library/LaunchDaemons/com.local.mountmedia.plist # Restore /etc/auto_master (uncomment direct map) and reload sudo sed -i.bak 's|^#/- /etc/auto_nfs|/- /etc/auto_nfs|' /etc/auto_master sudo automount -vc ``` --- ## Notes - Change permissions/ownership on the NFS export from the NAS, not the macOS client (root-squash). - `showmount` may fail against NFSv4-only servers; it’s not needed here. - Adjust `SERVER`, `MOUNT`, and `StartInterval` to suit your environment.