How to Build a Minimal ZFS NAS without Synology, QNAP, TrueNAS

ZFS System by Oracle

If you need a basic NAS and don't care about GUI features, it is suprisingly simple to set up a ZFS dataset and share it over the network using Samba.

Scope:

Scope & Requirements
Raid Level RAIDZ1 (1 Drive Redundancy)
Operating System Debian 12 Bookworm
Encryption None
ZFS Implementation OpenZFS, zfs-2.1.1
CPU 4 Cores, Xeon Server CPU can be had for cheap
RAM ECC RDIMM RAM 16 GB
Storage 4x4TB NVMe SSD
Backups Not covered, use ZFS Backup Scheduler
Skills Basic familiarity with Linux
Skill Level Beginner/Easy

I am using this article to document it for future myself, feel free to adopt it for your needs. Problem with TrueNAS is that it is a full-featured, supposedly enterprise-grade, software suite. While it may be simple to set it up (I've never tried), I just don't need any of the bells and whistles it offers. It's the mismatch between what I need and what it offers; not something inherently wrong with TrueNAS. There is also something to be said about a system you know everything about and not having to rely on yet another thing.

ZFS's best feature that's never explained or written anywhere

ZFS filesystem is self contained. If your OS is nuked suddently, simply take all disks to another machine or install a new OS, install zfs, run zfs import and get back your data. This freedom is underrated and not well understood. It is also not explained anywhere.

It's worth emphasizing: All configuration/details about ZFS is stored on the disks themselves. If you've setup a RAIDZ2 (Raid 6) with 6 disks, they are self contained. Move them to a new machine with zfs tools installed, and simply run zfs import. Boom, they'll show up as RAIDZ2. This is an amazing feature that no matter what happens to the host OS, machine, etc; as long as the disks are not damaged, your data is fine.

Step 1. Locate and Organize Disks

List all disks on a linux machine using lsblk -d -o TRAN,NAME,TYPE,MODEL,SERIAL,SIZE command.

[root@sys ~]# lsblk -d -o TRAN,NAME,TYPE,MODEL,SERIAL,SIZE
TRAN   NAME     TYPE MODEL                        SERIAL           SIZE
       sda      disk Virtual disk                                   40G
nvme   nvme5n1  disk Samsung SSD 990 PRO 4TB      XXXXXXXXXXXXXXX  3.6T
nvme   nvme2n1  disk Samsung SSD 990 PRO 4TB      XXXXXXXXXXXXXXX  3.6T
nvme   nvme6n1  disk Samsung SSD 990 PRO 4TB      XXXXXXXXXXXXXXX  3.6T
nvme   nvme3n1  disk Samsung SSD 990 PRO 4TB      XXXXXXXXXXXXXXX  3.6T

These are brand new NVMe drives from Samsung so they should be completely unallocated.

The disks are also mapped to an ID, running ls -lh /dev/disk/by-id:

[root@sys ~]# ls -lh /dev/disk/by-id
total 0
lrwxrwxrwx. 1 root root 10 May 10 09:34 dm-name-rhel-root -> ../../dm-0
lrwxrwxrwx. 1 root root 10 May 10 09:34 dm-name-rhel-swap -> ../../dm-1
lrwxrwxrwx. 1 root root 10 May 10 09:34 lvm-pv-uuid-DGBpev-Na0C-tY20-YY6E-tpL3-epNA-8Ts3Y0 -> ../../sda3
lrwxrwxrwx. 1 root root 13 May 10 09:34 nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX -> ../../nvme2n1
lrwxrwxrwx. 1 root root 13 May 10 09:34 nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX -> ../../nvme5n1
lrwxrwxrwx. 1 root root 13 May 10 09:34 nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX -> ../../nvme3n1
lrwxrwxrwx. 1 root root 13 May 10 09:34 nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX -> ../../nvme6n1

Notice that they are symlinked to their disk names in /dev/.

We can create a /etc/zfs/vdev_id.conf that maps an alias to these IDs:

[root@sys ~]# vim /etc/zfs/vdev_id.conf

# Add these lines in the vdev_id.conf file
alias nvme0 /dev/disk/by-id/nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX
alias nvme1 /dev/disk/by-id/nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX
alias nvme2 /dev/disk/by-id/nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX
alias nvme3 /dev/disk/by-id/nvme-Samsung_SSD_990_PRO_4TB_XXXXXXXXXXXXXXX

Run udevadm trigger to set the alias (or you can reboot the machine). We can verify that the aliases have been mapped by running ls -lh /dev/disk/by-vdev:

lrwxrwxrwx.  1 root root  13 May 10 10:28 nvme0 -> ../../nvme2n1
lrwxrwxrwx.  1 root root  13 May 10 10:28 nvme1 -> ../../nvme5n1
lrwxrwxrwx.  1 root root  13 May 10 10:28 nvme2 -> ../../nvme3n1
lrwxrwxrwx.  1 root root  13 May 10 10:28 nvme3 -> ../../nvme6n1

Alias mapping is completely optional, you can if you'd like use the full ID /dev/disk/by-id/nvme-eui.002538414143c248 of the disk when creating the zpool as we will do in the next section. Using an alias makes it nice. However, please don't use /dev/nvme1, /dev/nvme2, ... as the order is not guaranteed, especially if you mount a new drive to the system. Creating a vdev_id.conf ensures that the serial number of the drive is tied to the alias.

Remember when we discussed there is no configuration needed on the host OS? /etc/zfs/vdev_id/conf is not necessary and only used when creating a zpool for convenience. If your OS gets nuked, and you lose vdev_id.conf, it won't matter at all.

Step 2. Create ZPOOL

For this tutorial, I am creating a RAIDZ1 (RAID 5) zpool. That means 1 drive redundancy in-case of failure. It's up to you if you'd like additional redundancy, RAIDZ2 (RAID 6) would certainly be more risilient.

First, we need to install zfs on the linux machine. Please refer to the OpenZFS documentation on how to install it. It's usually as straight forward as, in my case, dnf install zfs on RHEL 9.

I recommend setting the ashift=12 option when creating the zpool as this is your last chance to do so. Most disks report 512kB sector size to OS due to backwards compatibility reasons, but large disks such as Samsung 990 Pro has a sector size of 4KB or even 8KB. ashift=12 represents a sector size of 4KB which will substantially improve performance.

[root@sys ~]# ls /dev/disk/by-vdev
nvme0 nvme1 nvme2 nvme3
[root@sys ~]# zpool create -o ashift=12 s16z1 raidz1 nvme0 nvme1 nvme2 nvme3
[root@sys ~]# zpool status s16z1
  pool: s16z1
 state: ONLINE
config:

	NAME        STATE     READ WRITE CKSUM
	s16z1       ONLINE       0     0     0
	  raidz1-0  ONLINE       0     0     0
	    nvme0   ONLINE       0     0     0
	    nvme1   ONLINE       0     0     0
	    nvme2   ONLINE       0     0     0
	    nvme3   ONLINE       0     0     0

errors: No known data errors
[root@sys ~]#

Perfect! I chose s16z1 as the name as it describes the size and type of raid. Most tutorials will use tank as the name, as it relates to pool. Corny, I reject this.

We're not done yet. zpool is a disk abstraction, and zfs is the file system. When we ran zpool create, it created a zfs file system with it.

List all properties of the zfs file system by running: zfs get all s16z1. To check whether our zpool is properly configured with ashift=12, we can run:

[root@sys ~]# zdb | grep ashift

ashift: 12

Before we share the system, configure compression (optionally) as well as the default mount point.

[root@sys ~]# zfs set mountpoint=/mnt/s16z1 s16z1
[root@sys ~]# zfs set compression=lz4 s16z1

Next, let's create a couple of zfs datasets under s16z1 root dataset. We will share them using Samba in the next section.

[root@sys ~]# zfs create s16z1/docs
[root@sys ~]# zfs create s16z1/backups

docs for documents, backups for time machine backup. You can create as many datasets as you'd like. Try to keep them at the top level. If you're wondering what is the difference between just a regular filesystem folder and a dataset—a zfs dataset is a way more than just a folder. You can manage zillion properties of a dataset, encrypt it, send and replicate a dataset, take snapshots, etc.—essentially, the entire ZFS feature set. Therefore, it is a good idea to create individual datasets for large categories of your files. docs or backups is a good abstraction level for a dataset. If you want to send just docs to a another remote server as a backup, you can do that without sending the whole s16z1 root dataset.

We will discuss sharing s16z1/docs as a general purpose share as well as ceating a proper Time Machine storage (for Apple systems) by sharing s16z1/backups with special properties.

Step 3. Share disk on the network

We'll use Samba for this, and the type of file sharing system is orthogonal to ZFS. ZFS doesn't care as long as it is mounted on the host system.

Install Samba:

[root@sys ~]# apt install samba

Create a UNIX user specifically for samba, we'll call it john:

[root@sys ~]# useradd -m john

and create a UNIX password for john:

[root@sys ~]# passwd john

Next, associate the UNIX user john to Samba by creating a Samba password for john . This password will then be used by hosts connecting to the share as SYS\john.

[root@sys ~]# smbpasswd -a john

john is also added to the Samba user group. You can verify john as a SMB user by running:

[root@sys ~]# pdbedit -L -v john

Unix username:        john
NT username:
Account Flags:        [U          ]
User SID:             S-1-5-21-880039843-1994218806-4034623300-1001
Primary Group SID:    S-1-5-21-880039843-1994218806-4034623300-513
Full Name:            john
Home Directory:       \\SYS\john
HomeDir Drive:
Logon Script:
Profile Path:         \\SYS\john\profile
Domain:               SYS
Account desc:
Workstations:
Munged dial:
Logon time:           0
Logoff time:          Wed, 06 Feb 2036 08:06:39 MST
Kickoff time:         Wed, 06 Feb 2036 08:06:39 MST
Password last set:    Fri, 23 Aug 2024 23:46:08 MST
Password can change:  Fri, 23 Aug 2024 23:46:08 MST
Password must change: never
Last bad password   : 0
Bad password count  : 0
Logon hours         : FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

To delete john SMB user, pdbedit -x -u john

Now that the UNIX user and Samba user is setup, configuring Samba service is extremely straight forward.

Edit /etc/samba/smb.conf (delete everything and replace with the following):

[docs]
   path = /mnt/s16z1/docs
   browseable = yes
   read only = no
   guest ok = no
   valid users = john
   create mask = 0755
[backups]
   path = /mnt/s16z1/backups
   read only = no
   guest ok = no
   inherit acls = yes
   spotlight = yes
   fruit:aapl = yes
   fruit:time machine = yes
   vfs objects = catia fruit streams_xattr
   valid users = john

Test it out on macOS by mounting it with cmd+K in Finder app and using the following url format: smb://10.0.0.6/docs and smb://10.0.0.6/backups where 10.0.0.6 is the IP of the server sharing the SMB share.

To test it on Debian systems or similar, install apt install smbclient and run:

[root@sys ~]#  smbclient -U john //10.0.0.6/docs -c 'ls'

Mount the smb://10.0.0.6/backups and it will show up as a Time Machine share on macOS. Once mounted, start Time Machine backups by adding the share to macOS > Settings > General > Time Machine.

That's all for now. I plan to write another article expanding on the encryption and very powerful zfs dataset replication features.

Cover image credit to Oracle ZFS systems. God, it is beautiful: https://docs.oracle.com/cd/E78901_01/html/E78910/gqtmb.html

← Back to Home


Self promotion. Check out our new typeface, Berkeley Mono:

Berkeley Mono →