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.

Migrating TrueNAS SCALE from 2-Disk Mirror to 3-Disk RAIDZ1
Photo by Domaintechnik / Unsplash

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:

  1. Back up or replicate the data.
  2. Destroy/recreate the pool.
  3. 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:

  1. Tailscale
  2. Cloudflared
  3. Nginx Proxy Manager
  4. Garage
  5. 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.

Subscribe to Eugene's Musings

Don't miss out on the latest posts. Sign up now to be the first to know when I publish a new post.🚀
[email protected]
Subscribe