Add Jellyfin + macOS: Persistent NFS Mount documentation
This commit is contained in:
259
macos/Jellyfin-NFS.md
Normal file
259
macos/Jellyfin-NFS.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# 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"
|
||||||
|
SERVER="192.168.50.105:/media"
|
||||||
|
HOST="${SERVER%%:*}"
|
||||||
|
|
||||||
|
# Ensure mountpoint exists
|
||||||
|
[ -d "$MOUNT" ] || mkdir -p "$MOUNT"
|
||||||
|
|
||||||
|
# If already mounted as NFS, exit quietly
|
||||||
|
if mount -t nfs | awk '{print $3}' | grep -qx "$MOUNT"; then
|
||||||
|
echo "$(date) already mounted: $MOUNT" >>"$LOG"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Preflight: only try to mount when NFS port is reachable
|
||||||
|
if ! /usr/bin/nc -G 2 -z "$HOST" 2049 >/dev/null 2>&1; then
|
||||||
|
echo "$(date) NAS not reachable on 2049, skipping mount" >>"$LOG"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date) mounting $SERVER -> $MOUNT" >>"$LOG"
|
||||||
|
/sbin/mount -t nfs -o resvport,hard,nfsvers=4.0 "$SERVER" "$MOUNT" >>"$LOG" 2>>"$ERR" || \
|
||||||
|
/sbin/mount -t nfs -o noresvport,hard,nfsvers=4.0 "$SERVER" "$MOUNT" >>"$LOG" 2>>"$ERR"
|
||||||
|
|
||||||
|
# Verify mount succeeded via mount(8)
|
||||||
|
if mount -t nfs | awk '{print $3}' | grep -qx "$MOUNT"; then
|
||||||
|
echo "$(date) mount OK: $MOUNT" >>"$LOG"
|
||||||
|
# Optional: restart Jellyfin if installed via Homebrew
|
||||||
|
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
|
||||||
|
else
|
||||||
|
echo "$(date) mount FAILED" >>"$ERR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SH
|
||||||
|
|
||||||
|
sudo chmod 755 /usr/local/sbin/mount_media_nfs.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_nfs.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_nfs.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 "
|
||||||
|
|
||||||
|
# NFS mount parameters
|
||||||
|
nfsstat -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.
|
||||||
|
- NFS 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.
|
||||||
Reference in New Issue
Block a user