Skip to content

feat: replace SMB with NFS, mount NFS inside Podman VM#82

Merged
smartwatermelon merged 19 commits intomainfrom
claude/feat-nfs-mount-migration-20260316
Mar 17, 2026
Merged

feat: replace SMB with NFS, mount NFS inside Podman VM#82
smartwatermelon merged 19 commits intomainfrom
claude/feat-nfs-mount-migration-20260316

Conversation

@smartwatermelon
Copy link
Copy Markdown
Owner

Summary

  • Replace SMB mount with NFS for the DSMedia share on the Synology NAS. NFS eliminates macOS SMB's oplock/deferred-deletion mechanism that caused .smbdelete* ghost files when VirtioFS held file descriptors open.
  • Mount NFS directly inside the Podman VM rather than passing through VirtioFS. Apple's Virtualization framework caches file descriptors indefinitely at the hypervisor level (no configuration to disable this). Direct NFS inside the VM completely bypasses VirtioFS for the data path, eliminating .nfs.* silly-rename files.
  • Replace podman compose with podman run because podman-compose validates that volume source paths exist on the macOS host, but the VM-internal NFS mount (/var/mnt/DSMedia) only exists inside the VM.

What changed

File Change
config/config.conf.template Add NAS_VOLUME for NFS export path
app-setup/templates/mount-nas-media.sh SMB → NFS mount (mount_nfs with noowners,actimeo=2) + sudoers for root mount
app-setup/plex-setup.sh Remove credential embedding, add NAS_VOLUME substitution, deploy sudoers rule
app-setup/containers/transmission/compose.yml Volume changed to __NFS_MOUNT_POINT__:/data (reference only)
app-setup/podman-transmission-setup.sh Add VM NFS systemd mount unit, replace compose with podman run, validate NAS config
app-setup/templates/transmission-post-done.sh Reverted torrent-remove RPC (no longer needed without VirtioFS)

Architecture

Before: NAS ←SMB→ macOS host ←VirtioFS→ Podman VM → Container
After:  NAS ←NFS→ Podman VM → Container (data path)
        NAS ←NFS→ macOS host (Plex, Finder, FileBot)

Synology NFS export requirements

  • Enable NFS service, NFSv4
  • Export /volume2/DSMedia to 10.0.12.0/22
  • Enable "Allow connections from non-privileged ports" (required for VM NAT traffic)

Test plan

  • NFS mount works on macOS host (both users)
  • NFS mount works inside Podman VM
  • Container sees NFS data directly (type nfs4, not virtiofs)
  • File create/delete from host while container running — no .nfs.* remnants
  • Transmission downloads complete successfully
  • Post-done trigger fires and FileBot processes media
  • Plex libraries accessible (same mount paths)
  • Processed torrent directories cleanable from Finder

🤖 Generated with Claude Code

Claude Code Bot and others added 16 commits March 16, 2026 19:39
Default is volume2 (verified via showmount -e). Used by mount-nas-media.sh
to construct the NFS export path: hostname:/volumeN/sharename

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NFS eliminates VirtioFS/SMB oplock conflict that caused .smbdelete
ghost files when deleting torrents while Podman VM is running.

- mount_smbfs → mount -t nfs with resvport,rw,soft,bg,intr,rsize/wsize=65536
- Removed credential embedding (NFS uses host-based auth)
- Added NAS_VOLUME template variable for Synology export path
- Updated test_mount to check NFS mount table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Renamed setup_persistent_smb_mount → setup_persistent_nfs_mount
- Removed credential retrieval/embedding (NFS uses host-based auth)
- Added NAS_VOLUME template substitution for export path
- Updated troubleshooting output with NFS-specific commands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unlike mount_smbfs which has a user-level helper, NFS mount on macOS
requires root privileges. The mount script now uses sudo for mount
and umount commands, paired with a sudoers rule deployed during setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use /sbin/mount_nfs and /sbin/umount (full paths required for sudoers match)
- Validate NAS_SHARE_NAME against safe character pattern before sudoers write
- Document wildcard as intentional (allows variable mount options, both users admin)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VirtioFS (Podman container) writes files through a separate NFS client
connection than the host scripts that read them. Without actimeo=2, the
NFS attribute cache (default 30s) causes find to return empty results
for newly written files, breaking transmission-done's media detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When Transmission completes a download, VirtioFS holds file descriptors
on all files it served to the container. If the host-side post-done
script tries to rename/move files while those FDs are open, NFS creates
.nfs.* silly-rename files that block directory operations.

Fix: the container-side post-done script now removes the completed
torrent from Transmission via RPC (keeping files on disk) before writing
the trigger file. This makes Transmission close its file handles,
allowing VirtioFS to release the FDs, so the host-side script can
freely rename and move files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transmission's RPC returns HTTP 409 for session ID retrieval. With
curl -f (fail on HTTP error) and set -euo pipefail, the script dies
before writing the trigger file. Changed to -s (silent only) with
|| true guards on all RPC pipelines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
macOS TCC blocks LaunchAgent-spawned processes from reading NFS-mounted
files unless the process has Network Volume access. The noowners flag
(same as SMB had) makes macOS ignore UID mapping and treat all files
as owned by the current user, bypassing TCC restrictions. Without this,
transmission-done gets "Operation not permitted" on ls/find.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The torrent-remove was a workaround for VirtioFS FD caching. With NFS
mounted directly inside the VM (bypassing VirtioFS), it's no longer
needed — and it prevented seeding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bypasses VirtioFS for NFS-backed data, eliminating Apple Virtualization
framework FD caching that caused .nfs.* silly-rename files on the host.

- Add Section 2b: creates systemd .mount unit inside VM via SSH
- Change compose volume from VirtioFS passthrough to VM-internal NFS
- Add NFS mount check to podman-machine-start.sh before compose up
- Validate NAS config variables (non-empty + safe characters)
- Quote all variables in SSH command strings
- Exit on NFS mount failure (don't continue with broken mount)

The VM now mounts the NAS share directly. The host-side NFS mount
remains for Plex, Finder, and FileBot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
podman-compose validates that volume source paths exist on the macOS
host. The NFS data volume (/var/mnt/DSMedia) only exists inside the VM,
causing compose to fail. podman run bypasses this and correctly
bind-mounts from the VM filesystem.

- Remove podman-compose install (no longer needed)
- Replace compose up with podman run in startup wrapper and initial start
- Quote all variable expansions in heredoc wrapper script
- Document variable dependencies in wrapper heredoc
- Keep compose.yml as reference documentation only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Convert **Step N:** bold text to ### headings (MD036) and
disambiguate duplicate heading names (MD024).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread app-setup/podman-transmission-setup.sh
These variables were only set inside the compose template conditional,
but are also used by the podman run command and startup wrapper. If the
compose template was missing, they'd be empty.

Post-push loop iteration 2: address Sentry finding at line 575.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread app-setup/podman-transmission-setup.sh
Consistent with NAS_HOSTNAME and NAS_SHARE_NAME validation —
NAS_VOLUME is also used in SSH commands and systemd unit files.

Post-push loop iteration 3: address Sentry finding at line 339.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread app-setup/plex-setup.sh
Comment thread app-setup/podman-transmission-setup.sh
- chown root:wheel on sudoers file before mv (sudo ignores non-root-owned files)
- Add watch directory to container mkdir block (podman run doesn't auto-create)

Post-push loop iteration 3: address Sentry findings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@smartwatermelon smartwatermelon merged commit 5a46cc7 into main Mar 17, 2026
20 checks passed
@smartwatermelon smartwatermelon deleted the claude/feat-nfs-mount-migration-20260316 branch March 17, 2026 06:49
smartwatermelon added a commit that referenced this pull request Mar 17, 2026
## Summary

Updates all active documentation to reflect the SMB→NFS migration and
podman compose→podman run changes from PR #82.

- **7 files updated**, 22 edits across README, plex-setup docs, operator
guide, keychain docs, environment variables, container proposal, and
prerequisites
- Historical docs (plans/, code-review) left as-is — they document
decisions at a point in time
- Key theme: NFS uses host-based auth (no credential embedding), mount
scripts use `sudo mount_nfs` with `noowners`

## Test plan

- [x] No stale SMB references in active docs (verified via grep)
- [x] `NAS_VOLUME` documented in environment-variables.md
- [x] Container proposal §5.1 and §5.2 reflect current VirtioFS bypass
architecture
- [x] Troubleshooting commands updated (mount_nfs, showmount -e)
- [x] markdownlint passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Code Bot <claude-code@smartwatermelon.github>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant