Migrating TrueNAS SCALE from 2-Disk Mirror to 3-Disk RAIDZ1
A complete guide to migrating TrueNAS SCALE from a 2-disk mirror to 3-disk RAIDZ1, including all the post-migration fixes for ix-apps, SMB shares, and application datasets.
I recently migrated a TrueNAS SCALE system from a 2 × 6 TB mirror to a 3 × 6 TB RAIDZ1 pool.
The goal was simple:
- Keep single-disk redundancy
- Gain more usable storage
- Preserve existing datasets, SMB shares, Apps, and backup tasks
- Avoid reinstalling TrueNAS or rebuilding services from scratch
The migration worked, but there were several TrueNAS-specific issues that had to be fixed afterward, especially around Apps, ix-apps, ZFS mountpoints, SMB ACL types, and read-only replicated datasets.
This guide documents the full process.
Final Result
Before:
- 2 × 6 TB mirror
- ~5.46 TiB usable
- 1-disk failure tolerance
- ~788 GB used
After:
- 3 × 6 TB RAIDZ1
- ~10.9 TiB usable
- 1-disk failure tolerance
- same data restored
The new pool is:
prox-pool
Final working state:
prox-pool
├── apps
│ ├── garage
│ ├── nextcloud
│ ├── nginx-proxy-manager
│ └── tailscale
├── ix-apps
├── pbs
├── prox-share
├── scripts
├── snapshot-view
└── storage-share
Why Move from Mirror to RAIDZ1?
The original mirror was simple and reliable:
6 TB ─┐
├── MIRROR
6 TB ─┘
But with only ~788 GB used, there was a lot of unused potential.
The new layout:
6 TB ─┐
6 TB ─┼── RAIDZ1
6 TB ─┘
RAIDZ1 gives approximately:
(number of disks - 1) × disk size
So with three 6 TB disks:
3 × 6 TB RAIDZ1
≈ 12 TB raw usable
≈ 10–11 TiB usable in practice
Comparison:
| Layout | Drives | Usable Capacity | Failure Tolerance | Efficiency |
|---|---|---|---|---|
| Mirror | 2 × 6 TB | ~5.46 TiB | 1 disk | ~50% |
| RAIDZ1 | 3 × 6 TB | ~10.9 TiB | 1 disk | ~67% |
This was a good tradeoff for this system because the workload did not require the random I/O advantages of mirrors.
Important Warning
You cannot directly convert a 2-disk mirror vdev into a 3-disk RAIDZ1 vdev.
To change the topology, you need to:
- Back up or replicate the data.
- Destroy/recreate the pool.
- Restore the data.
Do not destroy the old pool until your backup or replication has been verified.
Assumptions
This guide assumes:
- TrueNAS SCALE 25.04.x
- Existing pool has already been backed up or replicated
- New pool is named
prox-pool - The migration includes TrueNAS Apps
- SMB shares are used
- Existing datasets include Apps data and normal file shares
Adjust commands if your pool name or dataset names are different.
Phase 1: Pre-Migration Checks
Before changing anything, inspect the current system.
zpool status
zfs list -r
Also check configured SMB shares:
midclt call sharing.smb.query
Check Cloud Sync tasks:
midclt call cloudsync.query
Check replication tasks:
midclt call replication.query
Check snapshot tasks:
midclt call pool.snapshottask.query
Phase 2: Back Up the TrueNAS Configuration
Before making destructive storage changes, download a TrueNAS configuration backup.
In the UI:
System Settings → General → Manage Configuration → Download File
If TrueNAS offers to include the secret seed, include it. That helps preserve encrypted credentials when restoring the configuration.
Store this file somewhere outside the TrueNAS pool being modified.
Phase 3: Backup or Replication Strategy
There are several possible ways to move the data:
- ZFS replication to a temporary pool
- TrueNAS Cloud Sync to S3-compatible storage such as Cloudflare R2
- rclone
- External disk backup
- Another NAS
For normal files, Cloud Sync or rclone can work well.
For full dataset preservation, ZFS replication is better because it can preserve:
- Datasets
- Dataset hierarchy
- ZFS properties
- Snapshots
- ACL-related properties
- Application dataset layout
In this migration, ZFS replication was used for the main restore.
The replication eventually reported:
732.8 GiB of 732.8 GiB
28 of 28 datasets
SUCCESS
That was strong confirmation that the data copied correctly.
Phase 4: Create the New RAIDZ1 Pool
After confirming the backup/replication, destroy the old mirror pool and create the new RAIDZ1 pool.
In the TrueNAS UI:
Storage → Create Pool
Select the three 6 TB disks.
Choose:
RAIDZ1
The new pool layout:
prox-pool
└── RAIDZ1
├── 6 TB
├── 6 TB
└── 6 TB
After creation, confirm:
zpool status prox-pool
Expected:
state: ONLINE
errors: No known data errors
Phase 5: Restore the Data
Restore your datasets from the backup or replication source.
After the restore, verify with:
zfs list -r prox-pool
In this migration, important datasets included:
prox-pool/apps
prox-pool/apps/garage
prox-pool/apps/garage/config
prox-pool/apps/garage/data
prox-pool/apps/garage/metadata
prox-pool/apps/nextcloud
prox-pool/apps/nextcloud/app-data
prox-pool/apps/nextcloud/data
prox-pool/apps/nextcloud/postgres
prox-pool/apps/nginx-proxy-manager
prox-pool/apps/nginx-proxy-manager/certs
prox-pool/apps/nginx-proxy-manager/data
prox-pool/apps/tailscale
prox-pool/apps/tailscale/state
prox-pool/ix-apps
prox-pool/ix-apps/app_configs
prox-pool/ix-apps/app_mounts
prox-pool/ix-apps/docker
prox-pool/ix-apps/truenas_catalog
prox-pool/pbs
prox-pool/prox-share
prox-pool/storage-share
At this point, the pool existed and the data was restored, but several post-migration fixes were still needed.
Post-Migration Repair Steps
Problem 1: Replicated Pool Was Read-Only
After the restore, the pool had inherited readonly=on.
Check:
zfs get readonly prox-pool
zfs get readonly prox-pool/ix-apps
Example problematic output:
NAME PROPERTY VALUE SOURCE
prox-pool readonly on received
NAME PROPERTY VALUE SOURCE
prox-pool/ix-apps readonly on inherited from prox-pool
Fix:
zfs set readonly=off prox-pool
zfs set readonly=off prox-pool/ix-apps
Verify:
zfs get readonly prox-pool
zfs get readonly prox-pool/ix-apps
Expected:
readonly off
Problem 2: ix-apps Was Mounted at the Wrong Path
TrueNAS SCALE expects the Apps system dataset to be mounted at:
/mnt/.ix-apps
The Apps UI showed:
Application(s) have failed to start:
[EFAULT] 'prox-pool/ix-apps' dataset is not mounted on '/mnt/.ix-apps'
Initially, setting the mountpoint like this caused a problem:
zfs set mountpoint=/mnt/.ix-apps prox-pool/ix-apps
It resulted in:
/mnt/mnt/.ix-apps
This happens because TrueNAS imports pools with an altroot of /mnt.
So the correct ZFS mountpoint is not /mnt/.ix-apps.
It is:
/.ix-apps
TrueNAS then presents it at:
/mnt/.ix-apps
Correct ix-apps Fix
Run:
zfs unmount -f prox-pool/ix-apps 2>/dev/null || true
umount -lf /mnt/.ix-apps 2>/dev/null || true
umount -lf /mnt/mnt/.ix-apps 2>/dev/null || true
zfs set mountpoint=/.ix-apps prox-pool/ix-apps
for ds in $(zfs list -r -H -o name prox-pool/ix-apps | tail -n +2); do
zfs inherit mountpoint "$ds"
done
zfs set canmount=noauto prox-pool/ix-apps
zfs mount prox-pool/ix-apps
Verify:
zfs list -r -o name,mountpoint,mounted prox-pool/ix-apps
ls -ld /mnt/.ix-apps
ls /mnt/.ix-apps/
midclt call app.query 2>&1 | head -5
mount | grep .ix-apps
Expected output should show:
prox-pool/ix-apps /mnt/.ix-apps yes
And:
app_configs
app_mounts
backups
docker
metadata.yaml
truenas_catalog
user_config.yaml
A successful midclt call app.query should no longer throw:
FileNotFoundError: /mnt/.ix-apps/app_configs
Instead, it should return either an empty list or the installed Apps.
In this migration, after fixing the mountpoint, Apps were visible again.
Problem 3: ix-apps Was Mounted Read-Only
Even after fixing the mountpoint, the dataset was mounted as read-only:
prox-pool/ix-apps on /mnt/.ix-apps type zfs (ro,...)
Check:
zfs get readonly prox-pool/ix-apps
zfs get readonly prox-pool
Fix:
zfs set readonly=off prox-pool
zfs set readonly=off prox-pool/ix-apps
Then force a clean remount:
zfs unmount -f prox-pool/ix-apps
umount -lf /mnt/.ix-apps 2>/dev/null || true
zfs mount prox-pool/ix-apps
Verify:
mount | grep .ix-apps
zfs list -o name,mountpoint,mounted,readonly prox-pool/ix-apps
ls /mnt/.ix-apps/
midclt call app.query 2>&1 | head -5
Expected:
prox-pool/ix-apps on /mnt/.ix-apps type zfs (rw,...)
And:
RDONLY off
Problem 4: Apps Were Visible but Stopped
After fixing ix-apps, app.query returned the installed apps:
cloudflared
nextcloud
tailscale
garage
nginx-proxy-manager
But they were stopped.
This was expected after the migration.
Check Apps:
midclt call app.query
Or for a shorter view:
midclt call app.query | jq -r '.[] | "\(.name) \(.state)"'
Example:
cloudflared STOPPED
nextcloud STOPPED
tailscale STOPPED
garage STOPPED
nginx-proxy-manager STOPPED
Do not start them all at once yet. First, make sure the application data datasets are mounted.
Problem 5: App Data Datasets Existed but Were Not Mounted
When trying to start apps, Garage failed:
Failed 'up' action for 'garage' app
The lifecycle log showed:
Error response from daemon:
invalid mount config for type "bind":
bind source path does not exist:
/mnt/prox-pool/apps/garage/config
Tailscale had a similar error:
bind source path does not exist:
/mnt/prox-pool/apps/tailscale/state
The ZFS datasets existed, but they were not mounted.
Check:
zfs list -r -o name,mountpoint,mounted,canmount prox-pool/apps
mount | grep prox-pool/apps || echo "Nothing mounted under prox-pool/apps"
ls /mnt/prox-pool/
Problematic output looked like:
prox-pool/apps mounted no
prox-pool/apps/garage mounted no
prox-pool/apps/garage/config mounted no
prox-pool/apps/garage/data mounted no
prox-pool/apps/garage/metadata mounted no
prox-pool/apps/tailscale mounted no
prox-pool/apps/tailscale/state mounted no
Fix:
zfs mount -a
Or specifically:
zfs mount prox-pool/apps
zfs mount -r prox-pool/apps
Verify:
zfs list -r -o name,mountpoint,mounted prox-pool/apps | head -20
ls /mnt/prox-pool/apps/
ls /mnt/prox-pool/apps/garage/
ls /mnt/prox-pool/apps/tailscale/
Expected:
prox-pool/apps yes
prox-pool/apps/garage yes
prox-pool/apps/garage/config yes
prox-pool/apps/garage/data yes
prox-pool/apps/garage/metadata yes
prox-pool/apps/tailscale yes
prox-pool/apps/tailscale/state yes
And:
/mnt/prox-pool/apps/garage/
├── config
├── data
└── metadata
/mnt/prox-pool/apps/tailscale/
└── state
After this, the apps were able to start.
Problem 6: SMB Shares Had ACL Type Mismatch
The SMB shares still existed in TrueNAS, but enabling them initially produced an ACL mismatch error:
[EINVAL] sharingsmb_update.path_local:
ACL type mismatch with child mountpoint at /mnt/prox-pool/pbs:
prox-pool - POSIX, prox-pool/pbs - NFSV4
Check ACL types:
zfs get acltype prox-pool
zfs get acltype prox-pool/pbs
zfs get acltype prox-pool/storage-share
zfs get acltype prox-pool/prox-share
The mismatch was:
prox-pool posix
prox-pool/pbs nfsv4
prox-pool/storage-share nfsv4
prox-pool/prox-share nfsv4
For SMB shares on TrueNAS SCALE, nfsv4 is generally the right ACL type.
Fix the parent:
zfs set acltype=nfsv4 prox-pool
Verify:
zfs get acltype prox-pool
zfs get acltype prox-pool/pbs
zfs get acltype prox-pool/storage-share
zfs get acltype prox-pool/prox-share
Expected:
prox-pool nfsv4
prox-pool/pbs nfsv4
prox-pool/storage-share nfsv4
prox-pool/prox-share nfsv4
Problem 7: SMB Shares Were Disabled
The SMB share definitions survived the pool replacement, but they were disabled.
Check:
midclt call sharing.smb.query | jq -r '.[] | "\(.id) \(.name) enabled=\(.enabled) path=\(.path)"'
Example:
1 storage-share enabled=false path=/mnt/prox-pool/storage-share
2 prox-share enabled=false path=/mnt/prox-pool/prox-share
3 pbs enabled=false path=/mnt/prox-pool/pbs
Enable them:
midclt call sharing.smb.update 1 '{"enabled": true}'
midclt call sharing.smb.update 2 '{"enabled": true}'
midclt call sharing.smb.update 3 '{"enabled": true}'
Verify:
midclt call sharing.smb.query | jq -r '.[] | "\(.id) \(.name) enabled=\(.enabled)"'
Expected:
1 storage-share enabled=true
2 prox-share enabled=true
3 pbs enabled=true
At this point, test SMB access from a client machine.
Starting Apps Safely
Once these were fixed:
- prox-pool was read-write
- prox-pool/ix-apps was mounted correctly at /mnt/.ix-apps
- App datasets under /mnt/prox-pool/apps were mounted
- Apps were visible in app.query
Apps could be started.
Recommended order:
- Tailscale
- Cloudflared
- Nginx Proxy Manager
- Garage
- Nextcloud
Nextcloud was started last because it is the most complex and has multiple containers and database state.
If an app fails, check:
tail -50 /var/log/app_lifecycle.log
For a specific failure, also check the app state:
midclt call app.query '[["name", "=", "garage"]]'
In this migration, after mounting the missing app datasets, all apps started successfully.
Snapshot Tasks
Snapshot tasks survived the migration but remained disabled.
Check:
midclt call pool.snapshottask.query
Example snapshot tasks:
prox-pool/storage-share hourly
prox-pool/storage-share daily
prox-pool/storage-share weekly
Re-enable them:
midclt call pool.snapshottask.update 1 '{"enabled": true}'
midclt call pool.snapshottask.update 2 '{"enabled": true}'
midclt call pool.snapshottask.update 3 '{"enabled": true}'
Verify:
midclt call pool.snapshottask.query | jq -r '.[] | "\(.id) \(.dataset) enabled=\(.enabled)"'
Cloud Sync Tasks
Cloud Sync tasks also survived but were intentionally left disabled until the system was fully validated.
Check them:
midclt call cloudsync.query
Important things to verify before enabling Cloud Sync again:
- The source paths still exist.
- The destination buckets are correct.
- Credentials are valid.
- Any pre/post scripts still exist.
- Transfer mode is correct.
- You are not about to accidentally delete remote data with a SYNC task.
In this migration, some tasks were configured like:
/mnt/prox-pool/storage-share
/mnt/prox-pool/prox-share
/mnt/prox-pool/snapshot-view
One task used scripts:
/mnt/prox-pool/scripts/pre-cloud-sync.sh
/mnt/prox-pool/scripts/post-cloud-sync.sh
Those should be checked before re-enabling the task.
Also, if any S3/R2 credentials were exposed during troubleshooting, rotate them before re-enabling Cloud Sync.
Useful Verification Commands
Pool Health
zpool status prox-pool
Expected:
state: ONLINE
errors: No known data errors
Dataset Overview
zfs list -r prox-pool
Mount Status
zfs list -r -o name,mountpoint,mounted prox-pool
Apps Mounts
mount | grep .ix-apps
mount | grep prox-pool/apps
Apps Query
midclt call app.query | jq -r '.[] | "\(.name) \(.state)"'
SMB Shares
midclt call sharing.smb.query | jq -r '.[] | "\(.id) \(.name) enabled=\(.enabled) path=\(.path)"'
ACL Types
zfs get acltype prox-pool
zfs get acltype prox-pool/pbs
zfs get acltype prox-pool/storage-share
zfs get acltype prox-pool/prox-share
Read-Only Properties
zfs get readonly prox-pool
zfs get readonly prox-pool/ix-apps
App Lifecycle Log
tail -50 /var/log/app_lifecycle.log
Cleanup Tasks
Do not rush cleanup.
Keep the migration backup, old replication source, or Cloud Sync backup until:
- Pool is healthy
- SMB clients can read/write
- Apps start successfully
- Nextcloud data is verified
- Garage data is verified
- Proxmox/PBS access works
- Cloud Sync has been checked
- Snapshots are running again
- The system has survived at least one reboot
Only then remove temporary migration datasets or backups.
Reboot Test
After everything looks good, schedule a controlled reboot.
After reboot, verify:
zpool status prox-pool
zfs list -r -o name,mountpoint,mounted prox-pool/ix-apps
zfs list -r -o name,mountpoint,mounted prox-pool/apps
mount | grep .ix-apps
mount | grep prox-pool/apps
midclt call app.query | jq -r '.[] | "\(.name) \(.state)"'
midclt call sharing.smb.query | jq -r '.[] | "\(.name) enabled=\(.enabled)"'
Expected:
- Pool is online
- /mnt/.ix-apps is mounted
- App datasets are mounted
- Apps are visible
- Apps can start
- SMB shares remain enabled
Key Lessons Learned
1. RAIDZ1 Is a Good Fit When Capacity Matters
For this system, RAIDZ1 nearly doubled usable space while keeping one-disk redundancy.
The tradeoff is that mirrors still have advantages:
- Better random I/O
- Simpler resilvering
- More flexible layouts
- Easier vdev replacement scenarios
But for this use case, RAIDZ1 was the better capacity tradeoff.
2. Do Not Try to Convert a Mirror into RAIDZ1
This requires a destroy/recreate/restore workflow.
Plan for:
backup → destroy old pool → create new pool → restore
3. TrueNAS Uses /mnt as an Altroot
This matters.
For normal-looking paths under /mnt, ZFS properties may need to omit /mnt.
For ix-apps, the correct mountpoint property was:
zfs set mountpoint=/.ix-apps prox-pool/ix-apps
not:
zfs set mountpoint=/mnt/.ix-apps prox-pool/ix-apps
The incorrect version produced:
/mnt/mnt/.ix-apps
4. Replication Can Restore Datasets as Read-Only
If the pool or datasets are read-only after restore:
zfs set readonly=off prox-pool
5. zfs list Is Not Enough
A dataset can exist but not be mounted.
Always check:
zfs list -r -o name,mountpoint,mounted dataset
6. App Bind Mount Errors Usually Mean Missing Mounted Datasets
If Docker/Apps says:
bind source path does not exist
check that the dataset is mounted at that path.
7. SMB Shares Prefer Consistent ACL Types
If the parent is posix and children are nfsv4, SMB may complain.
For SMB datasets, align to nfsv4:
zfs set acltype=nfsv4 prox-pool
8. Start Apps One at a Time
After a storage migration, starting everything at once makes troubleshooting harder.
Start simple apps first and database-heavy apps last.
Final Outcome
The migration completed successfully.
Final state:
3 × 6 TB RAIDZ1
~10.9 TiB usable
1-disk redundancy
all datasets restored
SMB shares enabled
Apps restored and started
The main fixes required after replication were:
zfs set readonly=off prox-pool
zfs set mountpoint=/.ix-apps prox-pool/ix-apps
zfs mount -a
zfs set acltype=nfsv4 prox-pool
After that, SMB and Apps worked normally again.
The pool replacement achieved the goal: much more usable space with the same single-disk redundancy, without rebuilding the TrueNAS system from scratch.