How to Set Up an NFS Server
Set up a production-ready NFSv4 server on Linux: configure exports, mount shares on clients, lock down permissions, and tune read/write performance.
Before you start
- ▸Root or sudo access on both server and client machines
- ▸Server and clients on the same LAN or routable network with known IP addresses
- ▸A data directory already created on the server with content to share
NFS (Network File System) lets Linux machines share directories over a network as if they were local filesystems. It is still the workhorse protocol for shared storage in home labs, university clusters, and production environments alike. This guide walks through installing and configuring an NFSv4 server, exporting shares with sensible permissions, mounting them on clients, and tuning a few knobs that matter most for throughput.
Install NFS Server and Client Packages
Debian / Ubuntu
sudo apt update
sudo apt install nfs-kernel-server nfs-common
Fedora / RHEL / Rocky Linux
sudo dnf install nfs-utils
Arch Linux
sudo pacman -S nfs-utils
On client-only machines, you only need nfs-common (Debian/Ubuntu) or nfs-utils (Fedora/Arch). The kernel NFS server module is loaded automatically when the daemon starts; no manual modprobe is needed on modern kernels.
Create and Prepare the Export Directory
Pick a dedicated root for your exports. Using a single NFS pseudo-root (/srv/nfs) and bind-mounting real directories into it is the cleanest NFSv4 pattern — it confines what clients can traverse.
sudo mkdir -p /srv/nfs/shared
sudo mkdir -p /data/shared # your actual data lives here
sudo mount --bind /data/shared /srv/nfs/shared
Make the bind mount persistent by adding it to /etc/fstab:
/data/shared /srv/nfs/shared none bind 0 0
Set Ownership and Permissions
Decide on an ownership model before touching /etc/exports. Two common approaches:
- Shared group: set a common GID (e.g.,
nfssharewith GID 2000) on both server and all clients. - nobody/nogroup mapping: use
all_squashto map all remote users to a single anonymous UID — simpler but less granular.
# Shared-group example
sudo groupadd -g 2000 nfsshare
sudo chown root:nfsshare /data/shared
sudo chmod 2775 /data/shared # setgid bit so new files inherit the group
Configure /etc/exports
/etc/exports is the single file that controls what is shared, to whom, and under what conditions. Each line follows the pattern:
# /path/to/export client(options) another_client(options)
A realistic example exporting to a trusted LAN subnet:
# /etc/exports
# NFSv4 pseudo-root — must be exported with fsid=0
/srv/nfs 192.168.1.0/24(ro,sync,no_subtree_check,crossmnt,fsid=0)
# Shared read-write directory
/srv/nfs/shared 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
Key options explained:
- sync — write data to disk before acknowledging the client. Slower than
asyncbut prevents data corruption on crash. Useasynconly for scratch data where loss is acceptable. - no_subtree_check — disables subtree checking, which causes more problems than it solves on modern kernels. Always set this.
- fsid=0 — marks this path as the NFSv4 pseudo-root. Required for NFSv4; only one export gets this.
- crossmnt — allows clients to cross mount points within the pseudo-root tree without separate export entries.
- no_root_squash — remote root keeps UID 0 on the server. Use only on trusted machines; omit it for public or semi-trusted clients.
- root_squash (the default) — maps remote root to
nobody. - all_squash — maps every remote user to
nobody/nogroup. Good for public read-only shares.
After editing, apply the changes without restarting the daemon:
sudo exportfs -rav
Enable and Start the NFS Server
Debian / Ubuntu
sudo systemctl enable --now nfs-kernel-server
Fedora / RHEL / Rocky / Arch
sudo systemctl enable --now nfs-server
Check that the daemon is healthy:
sudo systemctl status nfs-server # or nfs-kernel-server on Debian/Ubuntu
sudo exportfs -v # list active exports with their options
Open the Firewall
NFSv4 only needs TCP port 2049. NFSv3 additionally requires portmapper (111) and dynamically assigned ports — another reason to prefer v4.
firewalld (Fedora / RHEL / Rocky)
sudo firewall-cmd --permanent --add-service=nfs
sudo firewall-cmd --reload
ufw (Ubuntu / Debian)
sudo ufw allow from 192.168.1.0/24 to any port 2049 proto tcp
sudo ufw reload
nftables (manual / Arch)
sudo nft add rule inet filter input ip saddr 192.168.1.0/24 tcp dport 2049 accept
Mount the Share on a Client
One-time mount for testing
sudo mkdir -p /mnt/shared
sudo mount -t nfs4 -o rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 \
192.168.1.10:/shared /mnt/shared
Replace 192.168.1.10 with your server's IP or hostname. Notice the NFSv4 mount path is relative to the pseudo-root (/shared, not /srv/nfs/shared).
Persistent mount via /etc/fstab
192.168.1.10:/shared /mnt/shared nfs4 rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev 0 0
The _netdev option tells systemd to mount this only after the network is up — critical for avoiding boot hangs.
Persistent mount via systemd .mount unit (alternative)
# /etc/systemd/system/mnt-shared.mount
[Unit]
Description=NFS shared mount
After=network-online.target
Wants=network-online.target
[Mount]
What=192.168.1.10:/shared
Where=/mnt/shared
Type=nfs4
Options=rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now mnt-shared.mount
Performance Basics
- rsize / wsize: Read and write buffer sizes. 1 MiB (1048576) is a safe modern default; Gigabit+ networks can benefit from this over the kernel's default of 131072 bytes.
- nconnect: Available since Linux 5.3, this option opens multiple TCP connections to the server.
nconnect=4can significantly improve throughput on high-latency or high-bandwidth links. Add it to mount options. - Number of NFS threads: The default is 8 threads. On busy servers, increase this in
/etc/nfs.conf(or/etc/default/nfs-kernel-serveron older Debian):
# /etc/nfs.conf (modern, cross-distro)
[nfsd]
threads=32
sudo systemctl restart nfs-server # apply the change
- async exports: Use
asyncinstead ofsyncfor temporary or scratch shares to trade durability for speed. Never useasyncon a share containing databases or important data.
Verify the Setup
# On the server — confirm exports are live
sudo exportfs -v
# On the client — check the mount is active
mount | grep nfs
df -h /mnt/shared
# Quick read/write test
dd if=/dev/zero of=/mnt/shared/testfile bs=1M count=256 oflag=direct
rm /mnt/shared/testfile
Expected exportfs -v output will list each export path, client pattern, and active options. The dd test gives a rough write throughput figure — compare it against a local disk write to gauge network overhead.
Troubleshooting
Mount hangs or times out
- Confirm port 2049 is reachable:
nc -zv 192.168.1.10 2049 - Check the firewall on the server. On RHEL/Rocky,
firewall-cmd --list-servicesshould shownfs. - Verify the NFS server service is running:
systemctl status nfs-server
Permission denied on the mount
- Run
sudo exportfs -vand confirm the client's IP is covered by an export rule. - Check UIDs match between server and client for the relevant user. NFS has no user-translation layer by default — UID 1001 on the client maps to UID 1001 on the server.
- If you used
root_squash, root on the client will be mapped tonobodyand may lack write permission.
Stale file handle errors
These appear after the server reboots or the export path changes while a client has it mounted. Unmount and remount the share on the client: sudo umount -l /mnt/shared && sudo mount /mnt/shared. The -l flag does a lazy unmount, clearing the stale handle.
Boot hangs on client
Ensure _netdev is in the fstab mount options, or use a systemd .mount unit with After=network-online.target. Without this, the filesystem layer tries to mount before the network interface is configured.
Frequently asked questions
- Should I use NFSv3 or NFSv4?
- Use NFSv4. It needs only one TCP port (2049), supports stateful locking natively, handles firewalls better, and has a cleaner pseudo-root model. NFSv3 is useful only for legacy clients that cannot support v4.
- Why do files created on the client show the wrong owner?
- NFS maps users by UID number, not username. If UID 1001 is 'alice' on the client but 'bob' on the server, files will appear owned by 'bob' on the server. Keep UIDs in sync across machines, use LDAP/FreeIPA for centralised identity, or use all_squash for simpler setups.
- What is the difference between sync and async export options?
- sync tells the server to flush writes to disk before acknowledging the client, protecting against data loss if the server crashes. async acknowledges before the flush, giving higher throughput but risking corruption. Always use sync for important data.
- How do I check how many NFS threads are currently running?
- Run 'cat /proc/net/rpc/nfsd' and look at the 'th' line, or use 'nfsstat -s' to see server-side statistics including thread usage. If threads are consistently saturated, increase the count in /etc/nfs.conf.
- Can I export a ZFS or Btrfs dataset directly instead of using a bind mount?
- Yes. You can export any directory path regardless of the underlying filesystem. The bind-mount pattern is a convention for clean NFSv4 pseudo-root management, not a technical requirement. ZFS users often export datasets directly and set sharenfs= properties via zfs set.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.