Skip to content

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:

ssh root@unraid "docker exec nextcloud ./occ files_external:list"

Step 2 — Delete external storage:

ssh root@unraid "docker exec nextcloud ./occ files_external:delete <ID>"

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.phpDefinitionParameter('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:

ssh root@unraid "grep -E '^(shareCachePool|shareUseCache)=' /boot/config/shares/<share>.cfg"

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

  • stacks/applications/nextcloud/compose.yaml — compose source
  • plans/260703-1242-unraid-zfs-pool-btrfs-to-zfs-migration/ — tank migration plan
  • docs/system-architecture.md — overall infra
  • Komodo deployment: periphery on Unraid :8120, Core at komodo.minhluc.info