11 KiB
11 KiB
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/mediafrom NAS192.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.105exporting/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,
resvporthelps. The script falls back tonoresvportif 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/chmodsay “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'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local.mountmedia</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/sbin/mount_media_smb.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
<key>KeepAlive</key>
<dict>
<key>NetworkState</key>
<true/>
</dict>
<key>StandardOutPath</key>
<string>/var/log/mount_media.out</string>
<key>StandardErrorPath</key>
<string>/var/log/mount_media.err</string>
</dict>
</plist>
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/mediauntil 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.resvportthen fallbacknoresvport: 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.outand.errmake 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
- Validate plist:
- Mount health:
nfsstat -mshould 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).
showmountmay fail against NFSv4-only servers; it’s not needed here.- Adjust
SERVER,MOUNT, andStartIntervalto suit your environment.