Migration from Unraid
This server previously ran Unraid. It now runs Debian 13 with Docker Compose, mergerfs, SnapRAID, and systemd maintenance. This page records what changed and how the cutover was performed; it is not a rebuild guide. The resulting storage layout is described in storage-layout.md and host-facts.md.
Why move at all
The change moved service definitions and operational configuration from UI templates and plugins into Git, Docker Compose, and systemd. Debian also makes the operator responsible for fstab, mergerfs, SnapRAID, backups, updates, and recovery. The main storage trade-off is parity timing: Unraid updates parity in real time, while SnapRAID protects new writes at the next scheduled sync. This was accepted for a mostly write-once media library .
What moved
| Piece | On Unraid | On Debian |
|---|---|---|
| Data disks | Unraid array + user shares | 5 independent XFS disks (2×28 TB, 3×12 TB) pooled by mergerfs at /mnt/user |
| Parity | Real-time array parity | 28 TB SnapRAID parity disk, outside the pool, spins down |
| Cache | Unraid btrfs cache pool | Same NVMe, reused in place: mounted as-is, never reformatted; appdata rode across on the device itself |
| Appdata | /mnt/cache/appdata |
/mnt/cache/appdata, same numeric ownership |
| Containers | Community-Apps templates | compose.yaml in git |
| Ownership | nobody:users = UID 99 / GID 100 |
Containers run PUID=99 / PGID=100, matched rather than chowned |
| GPU transcode | Unraid Intel plugin | Intel Arc A380 on bare-metal Debian (iHD VA-API driver) |
| Maintenance | Plugins / mover | systemd timers (SnapRAID, btrfs scrub, restic, watchdogs) |
Two things deliberately did not move. There is no mover: SABnzbd stages
incomplete/ on the NVMe so unpack I/O stays off the spinning disks, then
post-processing writes the finished file straight to the array. And the Docker
data-root stayed on the OS SSD at the default /var/lib/docker rather than
following appdata onto the cache. The current engine uses the containerd image
store and reports overlayfs; see Host facts.
How the cutover was run
The migration used explicit stop points. Disks were addressed by
/dev/disk/by-id/, never /dev/sdX; existing data disks were mounted
read-only first; data disks were never formatted; and
formatting the old Unraid parity disk as the commit point where easy rollback
ends.
- Inventory. Record the disk-to-slot map, every container's image, ports, paths, volumes and devices, numeric UID/GID ownership, and the network/ingress dependencies. Secrets as placeholders, real values kept private. This is the work that makes the migration reversible.
- Rehearse on Proxmox. The storage and Compose plan was exercised on a VM first, so the cutover night had as few new variables as possible.
- Debian prep. Minimal Debian 13, key-only SSH, Docker + Compose, the
storage packages, and a translated Compose project that passed
docker compose configwith a placeholder.env, all before the window. - Clean Unraid shutdown. Pause downloads and imports, stop containers and the array cleanly, and keep the Unraid USB as a rollback boot device.
- Read-only validation on Debian. Boot Debian, confirm every disk appears
by its stable ID, mount the data and cache disks read-only, verify the
expected files are there, and check
journalctlfor storage errors. Stop and roll back here if anything looks wrong; nothing destructive has happened yet. - Persistent mounts. Add by-id
/etc/fstabentries (nofail, with explicitx-systemd.requires-mounts-for=so the mergerfs pool cannot mount before its branch disks and present an empty pool), then bring up the pool read-write. - Commit point. With the pool showing the expected media and the mapping
re-checked, format the old parity disk, write
snapraid.conf, and start the firstsnapraid sync. After this, rollback to Unraid is no longer clean. - Services. Mount appdata in place, confirm numeric ownership, fill in the
private
.env, and bring containers up in dependency order: download clients, indexers, media managers, media server, request apps.
What was validated
The checks that actually mattered, in order:
- Storage ordering.
findmnt -R /mntshows every branch mounted, and the mergerfs pool never masks a missing disk with an empty directory on/. - Path fidelity. Download clients see their old paths so torrents resume without a forced recheck, and media managers keep their existing root folders.
- Appdata is real, not fresh. Every container reads migrated state, not a new empty config directory. A fresh config dir is the failure mode that looks like data loss.
- Restore before trust. The appdata backup was restore-tested, not just created. A backup that has never been restored is not yet a backup.
- Hardware transcode. A real transcode confirmed on the Arc A380 (
vainfo,intel_gpu_top), after plain direct play worked first. - Parity protection. The first full SnapRAID sync completed, and I made sure I understood its protection-gap window before trusting it.
Lessons recorded
- Reusing the NVMe cache kept appdata in place and avoided a restore and path migration during the cutover.
- Matching the existing numeric UID/GID (
99:100) preserved permissions and hardlinks without changing the media tree. - The Proxmox rehearsal identified most configuration issues before cutover.
SATA link resets that appeared only on the physical host were later resolved
with a
max_performanceALPM rule; see storage-layout.md. - SnapRAID leaves new data unprotected until the next sync. Monitoring and the sync schedule make that window explicit.
- Host configuration and Compose deployment are now converged through Ansible; see the behavior contract.