Skip to content

Server Setup: NAS


  • This is just how I did it. There are plenty of other ways to do it using different configurations (ZFS, Proxmox, Unraid, TrueNAS, OMV, etc). All options have pros and cons.

  • My NAS is a plain Debian 13 "trixie" server (Docker CE on top, like the rest of the servers) with MergerFS + SnapRAID. It also runs the media stack, so the same box serves files and apps.

    MergerFS: Pool drives and read them as one drive. FUSE system. Various configurations, read the official docs. The docs are very literal.

    SnapRAID: Nightly parity snapshots of the data drives, so a dead drive can be rebuilt. No striping, easy to expand, straightforward to manage.

    Backups are separate. Parity protects against a disk dying, not against deleting a file, ransomware or losing the whole box. Configs, databases and the small irreplaceable folders go to borgmatic; the media library relies on parity only, by choice.

The layout

Disk(s) Filesystem Mounted at Role
NVMe (boot) ext4 / OS, SnapRAID content file, borg repos the other servers back up into
SATA SSD ext4 /mnt/appdata Container configs and databases (fast, outside the pool and parity)
2 × 12 TB HDD XFS /mnt/disk1, /mnt/disk2 Data disks, pooled by MergerFS at /mnt/storage
1 × 12 TB HDD XFS /mnt/parity1 SnapRAID parity (never in the pool)
3 TB HDD ext4 /mnt/frigate Camera recordings: constant writes, so outside the pool and parity

Why the extra disks sit outside the pool: anything that writes all day (databases, NVR recordings) would make every SnapRAID sync large and would have to be stopped during it. Keeping them on their own disks keeps parity for the data that changes slowly (media, documents).

Mount points

Branches Docs

Naming Convention

The examples on these pages use /mnt/disk1, /mnt/disk2, /mnt/parity1, which is the easiest to read. On my own box I mount each drive at its type and serial instead, so a mount point tells you which physical disk to pull:

HDDs: /mnt/hdd SATA SSDs: /mnt/ssd NVME/M2: /mnt/nvme REMOTE: /mnt/remote

Example
$ ls -lh /mnt/hdd/
total 8K
d--------- 2 root root 4.0K Apr 14 15:58 10T-01234567
d--------- 2 root root 4.0K Apr 12 20:51 20T-12345678

# You can find the serial number of a drive using lsblk
$ lsblk -d -o NAME,PATH,SIZE,SERIAL
NAME    PATH           SIZE SERIAL
sda     /dev/sda       9.1T 01234567
sdb     /dev/sdb      18.2T 12345678
nvme0n1 /dev/nvme0n1 953.9G ABCDEFGH

Serial naming and the parity disk

If the parity disk lives under /mnt/hdd/ too, you can't pool with a glob like /mnt/hdd/*, because that would put the parity disk into the pool. List the data branches explicitly in fstab (see below).

1. Drive Preparation

Before pooling, drives must be partitioned and formatted.

  • Note: Identify your disks carefully using lsblk -o NAME,SIZE,SERIAL or sudo fdisk -l.
  • Strategy: XFS for the big data and parity disks, ext4 for the SSD and smaller disks. Both are in the Debian installer and well supported by SnapRAID.
# Tools used below (parted isn't always installed on a minimal Debian)
sudo apt update && sudo apt install -y parted xfsprogs attr

Formatting SSD as ext4

Back up first

wipefs, parted and mkfs erase everything on the target disk. Back up any data you want to keep and double-check the device name with lsblk before running these commands.

# 1. Create a GPT partition table (Destructive!)
sudo wipefs -a /dev/sdb
sudo parted -s /dev/sdb mklabel gpt

# 2. Create the partition (100% of disk)
sudo parted -s -a opt /dev/sdb mkpart primary ext4 0% 100%

# 3. Format to EXT4
# -m 0: Reserve 0% for root (max space for data)
# -L: Label the disk to make it easy to identify
sudo mkfs.ext4 -m 0 -L appdata /dev/sdb1

Formatting 3x HDDs as XFS

# Drive 1 (sdb)
# wipe fs
sudo wipefs -a /dev/sdb
# create GPT partition table
sudo parted -s /dev/sdb mklabel gpt
# create partition
sudo parted -s /dev/sdb mkpart primary xfs 0% 100%
# list partition
sudo fdisk -l /dev/sdb
# format partition
sudo mkfs.xfs -f -L "disk1" /dev/sdb1

# Drive 2 (sdc)
sudo wipefs -a /dev/sdc
sudo parted -s /dev/sdc mklabel gpt
sudo parted -s /dev/sdc mkpart primary xfs 0% 100%
sudo fdisk -l /dev/sdc
sudo mkfs.xfs -f -L "disk2" /dev/sdc1

# Drive 3 (sdd)
sudo wipefs -a /dev/sdd
sudo parted -s /dev/sdd mklabel gpt
sudo parted -s /dev/sdd mkpart primary xfs 0% 100%
sudo fdisk -l /dev/sdd
sudo mkfs.xfs -f -L "parity1" /dev/sdd1

  • Get UUIDs

    ls -l /dev/disk/by-uuid/
    

  • Output will be:

    lrwxrwxrwx 1 root root 10 Dec  7 05:34 <DISK1_UUID> -> ../../sdb1
    lrwxrwxrwx 1 root root 15 Dec  7 04:33 <BOOT_UUID> -> ../../nvme0n1p2
    lrwxrwxrwx 1 root root 10 Dec  7 05:34 <DISK2_UUID> -> ../../sdc1
    lrwxrwxrwx 1 root root 10 Dec  7 05:34 <APPDATA_UUID> -> ../../sda1
    lrwxrwxrwx 1 root root 10 Dec  7 05:34 <PARITY1_UUID> -> ../../sdd1
    

Always mount by UUID=, never /dev/sdX: the letters can change between boots (see Disk letters change between boots).

Mounting

  • Create Mount points:

    sudo mkdir -p /mnt/disk1 /mnt/disk2 /mnt/parity1 /mnt/appdata
    
    # Lock the empty mount points down BEFORE mounting, so nothing can write to the
    # root disk by accident if a drive is missing (details on the MergerFS page)
    sudo chmod 0000 /mnt/disk1 /mnt/disk2 /mnt/parity1
    

  • Edit fstab

  • Scroll to bottom of fstab and add:
    # ---------------------------------------------------------
    # DATA + PARITY DRIVES (XFS) - 12TB WD Reds
    # ---------------------------------------------------------
    UUID=<DISK1_UUID>   /mnt/disk1   xfs defaults,noatime 0 0
    UUID=<DISK2_UUID>   /mnt/disk2   xfs defaults,noatime 0 0
    UUID=<PARITY1_UUID> /mnt/parity1 xfs defaults,noatime 0 0
    
    # ---------------------------------------------------------
    # FAST STORAGE (EXT4) - SSD
    # ---------------------------------------------------------
    UUID=<APPDATA_UUID> /mnt/appdata ext4 defaults,noatime 0 0
    
  • NOTE: MUST RUN AFTER EDITING fstab
    # verify syntax
    sudo findmnt --verify
    
    # reload fstab
    sudo systemctl daemon-reload
    

Verify

  • Test this BEFORE reboot - no output is good

    sudo mount -a
    

  • Check correct sizing:

    df -h
    

You should see:

  • disk1, disk2, parity1: ~11T

  • appdata: the SSD's size

  • Check ownership:

    ls -ld /mnt/disk1 /mnt/disk2 /mnt/parity1 /mnt/appdata
    

  • If owner listed as root (and doesnt need to be) run:

    sudo chown <PUID>:<PGID> /mnt/disk1 /mnt/disk2 /mnt/appdata
    

2. Install MergerFS

Read the official docs. There is a lot to go over with mergerFS. Debian 13 ships a recent mergerfs (2.40), so the distro package is fine; upstream also publishes .deb files on GitHub if you need a newer release.

sudo apt update && sudo apt install mergerfs -y
mergerfs --version

Configure Pool in Fstab Add this line to the bottom of /etc/fstab (this is the line my NAS runs today):

# MergerFS data pool: the data disks only, never the parity disk
/mnt/disk1:/mnt/disk2 /mnt/storage fuse.mergerfs cache.files=per-process,cache.files.process-names=rtorrent|qbittorrent-nox|TorrentCreation,category.create=mspmfs,func.getattr=newest,dropcacheonclose=false,minfreespace=100G,moveonenospc=true,fsname=mergerfs 0 0
  • cache.files=per-process: Page caching off for everything except the named processes. Torrent clients use mmap, which needs page caching; with cache.files=off they error out. Everyone else (Plex, the *arrs, Samba) gets uncached I/O.
  • category.create=mspmfs: Most Shared Path, Most Free Space. Of the branches that already share the most of the file's parent path, writes to the one with the most free space. Keeps a show's season on one disk.
  • func.getattr=newest: Returns file attributes from the file with the newest mtime. Critical for apps like Plex/Kodi to detect changes.
  • minfreespace=100G: A branch with less than 100 GB free gets no new files. When every branch is below it, new files fail, so the free-space alert watches this (below).
  • moveonenospc=true: If a write runs out of space on one branch, mergerfs moves the file to another branch and carries on.

Full explanation on the MergerFS page.

C. Activate

sudo mkdir -p /mnt/storage
sudo systemctl daemon-reload
sudo mount -a
df -h /mnt/storage   # size = sum of the data disks

3. SnapRAID

SnapRAID calculates parity (like RAID 5) but does it on a schedule (Snapshot), not in real-time. Ideally suited for media servers where files rarely change.

A. Install SnapRAID Debian 13 has SnapRAID 12.4 in its main repo, which is what runs here. Upstream is newer (see the SnapRAID page if you want to build it from source).

sudo apt install snapraid -y
snapraid --version

B. Configure /etc/snapraid.conf Backup the default and create your own: sudo nano /etc/snapraid.conf

# 1. Parity Location (The dedicated large drive)
parity /mnt/parity1/snapraid.parity

# 2. Content Files (Index of all your files)
# One copy per physical disk: the boot NVMe plus every data disk
content /var/snapraid/snapraid.content
content /mnt/disk1/snapraid.content
content /mnt/disk2/snapraid.content

# 3. Data Disks (the pool branches, not the pool itself)
data d1 /mnt/disk1
data d2 /mnt/disk2

# 4. Excludes (Temp files/Trash)
exclude *.unrecoverable
exclude /tmp/
exclude /lost+found/
sudo mkdir -p /var/snapraid

C. Initialize Run the first sync to calculate parity. This will take hours depending on data size.

sudo snapraid sync

4. Automation & Maintenance

SnapRAID is manual by default. You must automate it to stay protected, and you must notice when the automation stops working. My first setup had a plain cron job whose sync was blocked by the "too many deleted files" safety check for a month, and its only notification (a Discord webhook) failed silently because of a DNS problem.

What runs now:

  • snapraid-aio-script from root's crontab, nightly: diff, sync if the change counts are sane, then scrub 5% of the array. It pauses the containers for the run.
  • A small wrapper around it that stops Plex cleanly instead of pausing it, and reports every run to an Uptime Kuma push monitor (dead-man switch) and ntfy.

The details, including why Plex is stopped rather than paused, are on the SnapRAID page.

Disk health and free space are watched separately:

  • smartd runs daily short and monthly long self-tests, and sends problems to ntfy (see Disk Management).
  • An hourly free-space check alerts on the pool's free GiB, not its percentage: mergerfs refuses new files below minfreespace, and a big pool can be 95% full with plenty of room. The individual branches and the parity disk are skipped, since they run near-full by design.

Troubleshooting

  • "Disk is full" during sync: Your parity drive must be equal to or larger than your single largest data drive.
  • Data Recovery: If disk1 fails, replace it, mount to /mnt/disk1, and run snapraid -d d1 -l fix.log fix. Full procedure on the SnapRAID page.
  • A drive failing its SMART long test: Storage lessons learned.

Sources & further reading