---
title: ZFS-NFS-Exports
description: Exporting NFS shares from ZFS datasets
published: true
date: 2026-02-23T21:58:20.626Z
tags: 
editor: markdown
dateCreated: 2026-02-01T20:45:40.210Z
---

# NFS Configuration

## Overview

ZNAS exports storage via NFSv4. All exports are ZFS datasets mounted directly to `/export/*` — no bind mounts. NFS is configured to wait for ZFS at boot via a systemd override.

ZNAS also mounts its own NFS exports back to itself at `/data/nfs/znas`. This is intentional: Docker Swarm containers scheduled to ZNAS need to access NAS storage at the same paths as containers running on other swarm members. The loopback mount provides a consistent NFS-backed path regardless of which node a container lands on.

All other clients are Linux systems using autofs.

---

## Server Configuration

### ZFS Mountpoints

ZFS datasets mount directly to `/export/*`. No bind mounts are used.

```
vault                    → /export
vault/Common             → /export/Common
vault/Data               → /export/Data
vault/Data/media_books   → /export/Data/media/books
vault/Data/media_comics  → /export/Data/media/comics
vault/Docker             → /export/Docker
vault/Green              → /export/Green
vault/Green/Pocket       → /export/Green/Pocket
vault/Photos             → /export/Photos
```

Verify at any time:

```bash
mount | grep export
```

### /etc/exports

```
# NFSv4 - pseudo filesystem root
/export *(ro,fsid=0,no_root_squash,no_subtree_check,crossmnt)

# Shares beneath the NFSv4 root
/export/Common *(fsid=4,rw,no_subtree_check,insecure)
/export/Data *(fsid=5,rw,no_subtree_check,insecure,crossmnt)
/export/Data/media/books *(fsid=51,rw,no_subtree_check,insecure,nohide)
/export/Data/media/comics *(fsid=52,rw,no_subtree_check,insecure,nohide)
/export/Docker *(fsid=29,rw,no_root_squash,sync,no_subtree_check,insecure)
/export/Green *(fsid=30,rw,no_root_squash,no_subtree_check,insecure)
/export/photos *(fsid=31,rw,no_root_squash,no_subtree_check,insecure)
```

**Key options:**

- `fsid=0` on `/export` — required for NFSv4 pseudo-root. Clients enumerate all exports from here.
- `crossmnt` — allows NFS to cross ZFS dataset boundaries when traversing the tree.
- `nohide` — required on `media/books` and `media/comics` because they are separate ZFS datasets mounted beneath the `vault/Data` export path. Without it clients see empty directories.
- `no_root_squash` — Docker and Green exports allow root writes. Required for container volume mounts.
- `insecure` — permits connections from unprivileged ports (>1024). Required for some Linux NFS clients and all macOS clients.
- `sync` on Docker — forces synchronous writes for container volume safety.

### systemd Boot Order Override

NFS is configured to wait for ZFS to fully mount before starting.

`/etc/systemd/system/nfs-server.service.d/override.conf`:

```ini
[Unit]
After=zfs-import.target zfs-mount.service local-fs.target
Requires=zfs-import.target zfs-mount.service
```

Apply after any changes:

```bash
sudo systemctl daemon-reload
sudo systemctl restart nfs-server
```

### Autofs Disabled on Server

Autofs is disabled on ZNAS itself. It must only run on NFS clients. Running autofs on the server creates recursive mount loops.

```bash
sudo systemctl stop autofs
sudo systemctl disable autofs
```

---

## Loopback Mount (Docker Swarm)

ZNAS mounts its own NFS exports back to itself at `/data/nfs/znas`. This ensures containers scheduled to ZNAS by Docker Swarm access storage at the same NFS-backed paths as containers running on any other swarm member — consistent regardless of which node a service lands on.

Swarm container volume mounts reference paths under `/data/nfs/znas/` rather than `/export/` directly.

### The Timing Problem

Getting this mount to survive reboots reliably was non-trivial. The loopback has a chicken-and-egg dependency chain:

1. ZFS must import and mount pools before NFS server can export anything
2. NFS server must be fully started before the loopback mount can succeed
3. The loopback mount must be established before Docker Swarm containers start

A plain `_netdev` fstab entry is not sufficient — `_netdev` only guarantees the network is up, not that the NFS server is ready. The mount would race against NFS startup and fail silently or hang.

### Solution — fstab with x-systemd.after

The loopback is established via `/etc/fstab` using the `x-systemd.after` option to explicitly declare the dependency on `nfs-server.service`:

```
localhost:/  /data/nfs/znas  nfs4  defaults,_netdev,x-systemd.after=nfs-server.service  0  0
```

`x-systemd.after=nfs-server.service` causes systemd-fstab-generator to automatically create a mount unit (`data-nfs-znas.mount`) with `After=nfs-server.service` in its `[Unit]` block. This guarantees the full dependency chain:

```
zfs-import.target
    → zfs-mount.service
        → nfs-server.service      (via nfs-server override.conf)
            → data-nfs-znas.mount (via x-systemd.after in fstab)
                → remote-fs.target
                    → Docker Swarm containers
```

The generated unit (created automatically at runtime by systemd-fstab-generator — not a file on disk):

```ini
# /run/systemd/generator/data-nfs-znas.mount
[Unit]
Documentation=man:fstab(5) man:systemd-fstab-generator(8)
SourcePath=/etc/fstab
After=nfs-server.service
Before=remote-fs.target

[Mount]
What=localhost:/
Where=/data/nfs/znas
Type=nfs4
Options=defaults,_netdev,x-systemd.after=nfs-server.service
```

**Do not create a hand-written systemd mount unit for this.** systemd-fstab-generator handles it automatically from the fstab entry. A manual unit would conflict.

### Verify Loopback is Active

```bash
mount | grep data/nfs/znas
# Should show: localhost:/ on /data/nfs/znas type nfs4 (...)

systemctl status data-nfs-znas.mount
# Should show: active (mounted)
```

---

## Client Configuration

All non-Swarm clients are Linux systems using autofs.

### Autofs Configuration

`/etc/auto.master` (relevant entry):

```
/data/nfs   /etc/auto.nfs
```

`/etc/auto.nfs`:

```
znas   -fstype=nfs4     192.168.5.10:/
```

This mounts the full NFSv4 tree from ZNAS at `/data/nfs/znas` on demand — the same path used by the loopback mount on ZNAS itself. All swarm nodes (including ZNAS) access NAS storage via `/data/nfs/znas/`.

**Note:** Autofs must be enabled on clients and disabled on the NFS server. Running autofs on the server creates recursive mount loops.

### Adding a New Client

```bash
# Install autofs if not present
sudo apt install autofs

# Add to /etc/auto.master if not already present
echo "/data/nfs   /etc/auto.nfs" | sudo tee -a /etc/auto.master

# Create or update /etc/auto.nfs
echo "znas   -fstype=nfs4     192.168.5.10:/" | sudo tee -a /etc/auto.nfs

# Reload autofs
sudo systemctl reload autofs

# Trigger mount by accessing the path
ls /data/nfs/znas/
```

### Manual Mount (testing only)

```bash
# Verify exports are visible from client
showmount -e 192.168.5.10

# Test manual mount
sudo mkdir -p /mnt/znas
sudo mount -t nfs4 192.168.5.10:/ /mnt/znas

# Verify tree is accessible
ls /mnt/znas/Data/media/books/

# Unmount after testing
sudo umount /mnt/znas
```

---

## Adding New Datasets

When creating a new ZFS dataset that needs to be NFS-accessible:

```bash
# Create with the correct mountpoint from the start
sudo zfs create -o mountpoint=/export/Data/new_folder vault/Data/new_folder
```

The dataset will be automatically visible via NFS due to `crossmnt` and `nohide` on the parent — no changes to `/etc/exports` needed unless the new dataset requires different access controls.

If different permissions are required, add an explicit entry to `/etc/exports` and reload:

```bash
sudo exportfs -ra
```

---

## Current Export List

Verified via `showmount -e 127.0.0.1`:

```
/export/photos            *
/export/Green             *
/export/Docker            *
/export/Data/media/comics *
/export/Data/media/books  *
/export/Data              *
/export/Common            *
/export                   *
```

---

## Known Gotchas

**Loopback mount races NFS at boot** — This was the hardest problem to solve. A plain `_netdev` fstab entry only guarantees the network interface is up, not that the NFS server is ready to accept connections. The loopback mount would attempt before NFS finished starting and fail silently or hang. The fix is `x-systemd.after=nfs-server.service` in the fstab options, which causes systemd-fstab-generator to emit an `After=nfs-server.service` dependency in the generated mount unit. The full required boot chain is: `zfs-import.target` → `zfs-mount.service` → `nfs-server.service` → `data-nfs-znas.mount`. Each link must be explicit.

**Do not hand-write a systemd mount unit for the loopback** — systemd-fstab-generator creates `data-nfs-znas.mount` automatically from the fstab entry at runtime (in `/run/systemd/generator/`, not `/etc/systemd/system/`). Creating a manual unit in `/etc/systemd/system/` will conflict with the generated one.

**Autofs must be disabled on the server** — Running autofs on ZNAS itself creates a recursive mount loop. Autofs belongs on clients only. If autofs is accidentally re-enabled on ZNAS it will fight with the fstab loopback mount.

**NFSv4 pseudo-root is required** — The `/export` entry with `fsid=0` is mandatory for NFSv4 clients. Without it clients cannot enumerate the export tree. Do not remove it even though it looks redundant.

**`nohide` on sub-datasets** — `vault/Data/media_books` and `vault/Data/media_comics` are separate ZFS datasets mounted beneath the `vault/Data` export path. NFS does not cross filesystem boundaries by default. Without `nohide` clients see empty directories at those paths even though the data is present.

**Do not use bind mounts for ZFS datasets** — Configure ZFS mountpoints directly to `/export/*`. Bind mounts in fstab for ZFS datasets cause ordering problems and are unnecessary.

**Always set mountpoints when creating new datasets** — If a dataset is created without an explicit mountpoint it will inherit the parent's path and may not be visible or exportable correctly. Set `mountpoint=` at creation time.

---

## Troubleshooting

### Datasets not visible via NFS

```bash
# Verify dataset is mounted
zfs list | grep dataset_name

# Check NFS can read it
sudo -u nobody ls -la /export/path/to/dataset/

# Reload exports
sudo exportfs -ra
sudo systemctl restart nfs-server
```

### Client shows empty directories

```bash
# Clear NFS cache and remount
sudo umount -f /mnt/znas
sudo mount -t nfs4 192.168.5.10:/ /mnt/znas

# Test without caching to isolate the problem
sudo mount -t nfs4 -o noac,lookupcache=none 192.168.5.10:/ /mnt/znas
```

### After reboot, exports are empty

```bash
# Confirm ZFS mounted before NFS started
systemctl status zfs-mount.service
systemctl status nfs-server.service

# Confirm override is in place
systemctl cat nfs-server.service | grep -A5 "\[Unit\]"
```

### Loopback mount not working for Swarm containers

```bash
# Check mount unit status
systemctl status data-nfs-znas.mount

# Verify full dependency chain is satisfied
systemctl status zfs-mount.service
systemctl status nfs-server.service
systemctl status data-nfs-znas.mount

# Verify loopback is mounted
mount | grep data/nfs/znas

# If missing, mount manually to test
sudo mount -t nfs4 127.0.0.1:/ /data/nfs/znas

# Check container can see the path
docker run --rm -v /data/nfs/znas/Data:/data alpine ls /data
```

If the unit fails at boot, confirm the fstab entry includes `x-systemd.after=nfs-server.service` — without this the mount races against NFS startup and loses. A plain `_netdev` entry is not sufficient.

---

## Configuration Files Reference

### /etc/exports

```
/export *(ro,fsid=0,no_root_squash,no_subtree_check,crossmnt)
/export/Common *(fsid=4,rw,no_subtree_check,insecure)
/export/Data *(fsid=5,rw,no_subtree_check,insecure,crossmnt)
/export/Data/media/books *(fsid=51,rw,no_subtree_check,insecure,nohide)
/export/Data/media/comics *(fsid=52,rw,no_subtree_check,insecure,nohide)
/export/Docker *(fsid=29,rw,no_root_squash,sync,no_subtree_check,insecure)
/export/Green *(fsid=30,rw,no_root_squash,no_subtree_check,insecure)
/export/photos *(fsid=31,rw,no_root_squash,no_subtree_check,insecure)
```

### /etc/systemd/system/nfs-server.service.d/override.conf

```ini
[Unit]
After=zfs-import.target zfs-mount.service local-fs.target
Requires=zfs-import.target zfs-mount.service
```

### /etc/fstab (ZNAS system mounts only)

ZFS datasets are not listed here — ZFS handles its own mounting. Only system partitions appear:

```
# / - btrfs on nvme0n1p2
/dev/disk/by-uuid/40c60952-0340-4a78-81f9-5b2193da26c6 / btrfs defaults 0 1
# /boot - ext4 on nvme0n1p3
/dev/disk/by-uuid/4abb4efa-0b2b-4e4a-bcaf-78227db4628f /boot ext4 defaults 0 1
# swap
/dev/disk/by-uuid/d07437a0-3d0e-417a-a88e-438c603c2237 none swap sw 0 0
# /srv - btrfs on nvme0n1p5
/dev/disk/by-uuid/c66e81ff-436e-4d6f-980b-6f4875ea7c8e /srv btrfs defaults 0 1
```

---

## Command Reference

- Show active exports: `sudo exportfs -v`
- Reload exports: `sudo exportfs -ra`
- Show available exports (from any host): `showmount -e 192.168.5.10`
- Restart NFS: `sudo systemctl restart nfs-server`
- Check NFS status: `systemctl status nfs-server`
- Verify ZFS mounts: `mount | grep export`
- Verify loopback: `mount | grep data/nfs`
