Nextcloud ↔ Unraid Tank Pool — Mount Playbook¶
Created: 2026-07-04
Scope: Mount /mnt/tank/<subfolder> (ZFS pool on Unraid) as Local external storages in Nextcloud container
Approach: Option A — per-folder bind mount (vs Option B/f considered, see "Architecture" below)
Architecture Decision¶
3 options were evaluated:
| Option | Container mount | Add/remove folder | s3-storage |
|---|---|---|---|
| (A) Per-folder bind mount ⭐ | 1 bind per folder | Edit compose + redeploy | Auto-excluded (not in mount list) |
(B) Mount whole /mnt/tank |
1 bind | 100% auto | Visible — needs chmod to hide |
| (f) Mount whole + occ per-storage | 1 bind + N occ | 1 occ per add | Auto-excluded (UI level) |
Chosen: Option A — explicit control, simple mental model, naturally excludes RustFS-internal s3-storage.
Trade-off accepted: Add/remove tank folder requires compose edit + container recreate (~10s downtime). Acceptable because changes are rare.
Current Configuration¶
Compose (repo source-of-truth)¶
File: stacks/applications/nextcloud/compose.yaml
volumes:
- /mnt/user/appdata/nextcloud:/var/www/html
# Option A: per-folder tank mounts (skip s3-storage — RustFS internal)
# Only mount shares whose useCache=only + cachePool=tank. Downloads is array-only.
- /mnt/user/audiobooks:/tank/audiobooks
- /mnt/user/podcasts:/tank/podcasts
- /mnt/user/music:/tank/music
- /mnt/user/isos:/tank/isos
External Storages (Nextcloud DB)¶
Run docker exec nextcloud ./occ files_external:list to verify.
| ID | Mount Point | datadir |
|---|---|---|
| 2 | /Audiobooks | /tank/audiobooks |
| 3 | /Podcasts | /tank/podcasts |
| 4 | /Music | /tank/music |
| 5 | /Isos | /tank/isos |
Permission Model¶
- ZFS datasets created with
chmod 0777 nobody:users(Phase 4 of tank migration) - Container bind mount: read-write by default
- Nextcloud runs as
www-data(uid 33, gid 33) - RW works because 0777 = world-writable
- RustFS protected by being unmounted in Nextcloud container (Option A auto-exclusion)
Verify RW:
ssh root@unraid "docker exec -u www-data nextcloud sh -c 'touch /tank/audiobooks/.test && rm /tank/audiobooks/.test && echo RW OK'"
Operations¶
Add a new tank folder to Nextcloud¶
Example: Mount new tank/photos (assuming ZFS dataset exists on tank).
Step 1 — Edit compose:
# stacks/applications/nextcloud/compose.yaml
volumes:
- /mnt/user/appdata/nextcloud:/var/www/html
# ... existing mounts ...
- /mnt/user/photos:/tank/photos # ← new line
Step 2 — Recreate container (Komodo will do this on git push; or manual):
# Manual (if Komodo unavailable):
ssh root@unraid "docker stop nextcloud && docker rm nextcloud"
# Re-run docker run with all original params + new -v line
# OR push to git → Komodo auto-redeploys
Step 3 — Add external storage:
ssh root@unraid "docker exec nextcloud ./occ files_external:create \
'Photos' local null::null -c datadir=/tank/photos"
Step 4 — Verify:
ssh root@unraid "docker exec nextcloud ./occ files_external:list"
ssh root@unraid "docker exec nextcloud ./occ files:scan --all"
Remove a tank folder from Nextcloud¶
Step 1 — Find mount ID:
Step 2 — Delete external storage:
Step 3 — Edit compose (remove the -v line) + recreate container.
Critical Gotchas¶
1. occ parameter name is datadir (NOT datalocalpath, path, datadirectory)¶
Source: /var/www/html/apps/files_external/lib/Lib/Backend/Local.php → DefinitionParameter('datadir', 'Location').
Wrong key returns: Unknown configuration for backends "<key>".
2. No docker-compose binary on Unraid¶
Use docker run directly to recreate, OR push compose change to git → Komodo auto-redeploys.
3. files:scan required after adding¶
Without occ files:scan --all, Nextcloud UI may show empty folder until user browses into it (triggers lazy scan).
4. Don't mount s3-storage¶
RustFS internal structure (.rustfs.sys/, multipart/, pool.bin/) is sensitive — if Nextcloud user modifies these files, RustFS corrupts. Always exclude from bind mounts.
5. Container recreate = ~10s downtime¶
Plan for brief Nextcloud unavailability during compose changes.
6. Verify share is actually tank-backed before mounting¶
The bind source /mnt/user/<share> follows FUSE merger routing per share config. If a share has useCache=no, the merger routes to ARRAY, not tank — even if a tank/<share> dataset exists (orphaned).
Pre-check before adding a mount:
Only mount if shareUseCache="only" AND shareCachePool="tank".
Lesson learned (2026-07-04): Initially mounted downloads to /tank/downloads in Nextcloud. But downloads share was useCache=no (array-only). The bind mount saw ARRAY data via FUSE merger, while the actual tank/downloads dataset was orphaned (1.17G wasted). Fixed by removing mount + destroying orphan dataset.
Troubleshooting¶
Folder shows empty in Nextcloud¶
# Check filesystem visibility
ssh root@unraid "docker exec nextcloud ls /tank/<folder>"
# Trigger scan
ssh root@unraid "docker exec nextcloud ./occ files:scan --all"
# Check storage verify
ssh root@unraid "docker exec nextcloud ./occ files_external:verify <ID>"
Permission denied on write¶
# Check dataset perms
ssh root@unraid "ls -la /mnt/tank/<folder>"
# Should be nobody:users 0777. If not:
ssh root@unraid "chmod 0777 /mnt/tank/<folder> && chown nobody:users /mnt/tank/<folder>"
Storage not in occ list¶
# Check DB directly
ssh root@unraid "docker exec nextcloud ./occ files_external:list --output json_pretty"
# Or check oc_external_mounts table
Migration History¶
- 2026-07-03: Tank pool migrated from btrfs RAID5 → ZFS RAIDZ1 (
plans/260703-1242-unraid-zfs-pool-btrfs-to-zfs-migration/) - 2026-07-04: Nextcloud tank mounts added (Option A, 5 folders, IDs 2-6)
- s3-storage deliberately excluded — RustFS internal use only
Related Files¶
stacks/applications/nextcloud/compose.yaml— compose sourceplans/260703-1242-unraid-zfs-pool-btrfs-to-zfs-migration/— tank migration plandocs/system-architecture.md— overall infra- Komodo deployment: periphery on Unraid :8120, Core at komodo.minhluc.info