Install NFS Server and Client Packages
On Debian/Ubuntu systems the server package is nfs-kernel-server and the client is nfs-common. On RHEL/AlmaLinux/Rocky the single package nfs-utils covers both roles.
After installation, enable the server with systemctl. On systemd-based distros the relevant units are nfs-server.service and rpcbind.service. NFSv4 does not strictly require rpcbind for mounts, but rpcbind is still needed for NFSv3 compatibility and for rpcinfo diagnostics, so keep it running unless you are certain your environment is pure v4.
Verify the kernel NFS server is loaded after enabling the service. You should see nfsd in the output of lsmod. If you only see rpc_pipefs mounted under /var/lib/nfs/rpc_pipefs and nothing else, the nfsd module did not load, usually because another NFS daemon is blocking the port.
# Ubuntu/Debian
apt install -y nfs-kernel-server nfs-common
# RHEL/AlmaLinux/Rocky
dnf install -y nfs-utils
# Enable and start
systemctl enable --now nfs-server rpcbind
# Confirm kernel server is active
cat /proc/fs/nfsd/versions
# Expected output on a healthy NFSv4.2 server:
# -2 -3 +4 +4.1 +4.2
Configure /etc/exports: Options That Matter
The /etc/exports file is where you define which directories are shared, to which hosts, and under what conditions. Every field matters; a misconfigured export is one of the fastest ways to lose data or expose it to the wrong host.
The rw vs ro flag is obvious. What trips people up is root_squash vs no_root_squash. With root_squash (the default), root on the client maps to the nobody user on the server. With no_root_squash, root on the client has root access to the export. Never use no_root_squash on a multi-tenant network or any export reachable by untrusted hosts. We use it only in isolated build clusters where every client is a controlled CI runner.
The sync option forces the server to confirm writes to stable storage before acknowledging them to the client. The async option acknowledges writes before they hit disk, which gives a measurable throughput boost but risks data corruption on server crash. On our test server running a 10GbE link, async gave us 1.1 GB/s sequential write vs 680 MB/s with sync on an NVMe array. Use sync for anything that matters and async only for scratch or cache volumes where you can afford to lose in-flight data.
The subtree_check option is deprecated and slow. Always use no_subtree_check. It was the default in older kernels but is now explicitly recommended off by the kernel NFS team. If you see subtree_check in a legacy /etc/exports, remove it.
fsid=0 marks the root of the NFSv4 pseudofilesystem. Every NFSv4 server needs exactly one export with fsid=0. All other exports are bound into this tree. If you skip fsid=0, clients will mount individual paths fine with explicit paths but the NFSv4 pseudoroot will be broken.
After editing /etc/exports, apply changes without restarting the server using exportfs -ra. Use exportfs -v to confirm what the kernel sees.
# /etc/exports example
# NFSv4 pseudoroot
/srv/nfs *(ro,sync,no_subtree_check,fsid=0)
# Data export - specific subnet only
/srv/nfs/data 192.168.10.0/24(rw,sync,no_subtree_check,no_root_squash)
# Home directories
/srv/nfs/homes 192.168.10.0/24(rw,sync,no_subtree_check,root_squash)
# Scratch/cache - async acceptable here
/srv/nfs/scratch 192.168.10.0/24(rw,async,no_subtree_check,root_squash)
# Bind-mount the real paths into the pseudoroot
mount --bind /data/shared /srv/nfs/data
mount --bind /home /srv/nfs/homes
mount --bind /scratch /srv/nfs/scratch
# Reload exports
exportfs -ra
exportfs -v
Firewall Rules for NFS
NFSv4 uses only TCP port 2049. If your environment is pure NFSv4, that is the only port you need to open. NFSv3 adds rpcbind on port 111 plus dynamically assigned ports for mountd, statd, and lockd, which makes static firewall rules painful unless you pin those ports in /etc/sysconfig/nfs or /etc/default/nfs-kernel-server.
On systems running firewalld, the nfs service definition already includes port 2049. Add it to your zone with a single command. If you need NFSv3 as well, also add the mountd, rpc-bind, and rpc-statd services.
On iptables-based systems, insert rules for TCP and UDP 2049. For NFSv3 compatibility pin the variable ports first in the daemon configuration files, then open those specific ports rather than allowing all high ports from the NFS server.
After adjusting firewall rules, verify from the client with rpcinfo -p serverhost to see what ports are registered. A clean NFSv4-only server will show portmapper, nfs, and nfs_acl on port 2049.
# firewalld - NFSv4 only
firewall-cmd --permanent --add-service=nfs
firewall-cmd --reload
# firewalld - NFSv3 compatibility
firewall-cmd --permanent --add-service=nfs
firewall-cmd --permanent --add-service=mountd
firewall-cmd --permanent --add-service=rpc-bind
firewall-cmd --reload
# iptables - NFSv4 only
iptables -A INPUT -p tcp --dport 2049 -s 192.168.10.0/24 -j ACCEPT
iptables -A INPUT -p udp --dport 2049 -s 192.168.10.0/24 -j ACCEPT
# Verify from client
rpcinfo -p 192.168.10.10
Mount NFS on the Client
Manual mounts are useful for testing. Production systems use /etc/fstab or autofs. Never skip testing a manual mount before committing to fstab, because a bad fstab NFS entry will hang boot if the server is unreachable.
The mount options you choose on the client side have more performance impact than most people realize. The rsize and wsize parameters control the maximum read and write block sizes in bytes. The kernel default is 131072 (128 KB) on recent kernels. On high-latency links you may want to leave these at default; on low-latency 10/25GbE links you can experiment up to 1048576 (1 MB) though the gains flatten out past 256 KB in our testing.
The hard mount option means the client will retry indefinitely if the server becomes unreachable, which is correct for persistent storage. The soft option returns an error to the application after retrans retries, which avoids hung processes but risks data corruption if a write was in flight. Use hard for data volumes. Use soft only for read-only reference data or in environments where hung clients are worse than potential data loss.
The timeo option sets the initial timeout in tenths of a second (timeo=600 means 60 seconds). The retrans option sets the number of retries before the client reports a major timeout.
For NFSv4, also set nfsvers=4.2 explicitly to prevent fallback negotiation from silently dropping to v3 if the server also has v3 enabled. The proto=tcp option is redundant for v4 since v4 is TCP-only, but being explicit avoids surprises.
The fstab entry for a permanent NFS mount should also include _netdev in the mount options, which tells systemd to wait for the network before attempting the mount during boot. Without _netdev an NFS mount in fstab will cause a boot hang on a headless server if the NFS server is momentarily unavailable.
# Manual test mount
mount -t nfs -o nfsvers=4.2,rsize=262144,wsize=262144,hard,timeo=600 \
192.168.10.10:/data /mnt/nfstest
# Verify version negotiated
nfsstat -m
# Should show vers=4.2
# fstab entry (persistent)
# : nfs 0 0
192.168.10.10:/data /mnt/data nfs nfsvers=4.2,rsize=262144,wsize=262144,hard,timeo=600,retrans=3,_netdev 0 0
# Mount all fstab entries matching nfs type
mount -a -t nfs
Autofs for On-Demand Mounting
Autofs mounts filesystems on access and unmounts them after a configurable idle period. This is the right approach for home directories served over NFS, for multi-server environments where not every mount is needed on every client, and for laptops that are not always on the corporate network.
Install autofs with apt install autofs or dnf install autofs, then configure two files: the master map (/etc/auto.master or /etc/auto.master.d/*.autofs) and the indirect map for each mount point.
The timeout option in the master map controls how many seconds of idle time triggers an unmount. 300 seconds (5 minutes) is a reasonable default for most environments. Set it lower for desktop workstations and higher for servers where the overhead of remounting is more disruptive than keeping a volume mounted.
For Kerberos-authenticated NFS (NFSv4 with krb5), autofs handles the credential acquisition automatically via pam_krb5 if the autofs service account is properly configured in /etc/krb5.conf. That setup is out of scope here but worth noting as the correct architecture for multi-tenant NFS environments.
After editing the maps, reload autofs with systemctl reload autofs. Then trigger a mount by accessing the path - ls /nfs/data should mount the share within a second.
# /etc/auto.master.d/nfs.autofs
/nfs /etc/auto.nfs --timeout=300
# /etc/auto.nfs
data -rw,nfsvers=4.2,rsize=262144,wsize=262144,hard 192.168.10.10:/data
homes -rw,nfsvers=4.2,rsize=262144,wsize=262144,hard 192.168.10.10:/homes
scratch -rw,async,nfsvers=4.2 192.168.10.10:/scratch
# Reload and test
systemctl reload autofs
ls /nfs/data
mount | grep autofs
NFS Performance Tuning
The number of nfsd kernel threads is the most impactful single tuning parameter on the server side. The default on most distributions is 8 threads. On a server handling more than 20 concurrent clients or doing heavy parallel I/O, 8 threads will saturate quickly. We run 64 threads on our test server with 32 active NVMe-backed clients and see consistent throughput without queue buildup.
Set the thread count in /etc/nfs.conf on RHEL-family systems or /etc/default/nfs-kernel-server on Debian-family systems. The relevant key is RPCNFSDCOUNT on Debian and [nfsd] threads= in /etc/nfs.conf on RHEL.
On the client side, beyond rsize/wsize, the number of slot table entries affects parallelism. NFSv4.1 introduced session slots which allow multiple outstanding RPCs per connection. The kernel parameter sunrpc.max_reqs_per_slot defaults to 16. On high-bandwidth links with many parallel workloads you can double this, but watch server memory consumption - each slot takes memory on both ends.
For workloads that write many small files (CI artifact caches, package mirrors), the attribute cache timeout settings matter. actimeo sets both acregmax and acdirmax simultaneously. The default is 60 seconds. Lowering it to 5 seconds improves coherency between clients at the cost of more GETATTR RPCs. For exclusive single-writer workloads you can raise it to 120 seconds to reduce chatter.
We benchmarked NFS read throughput on our 10GbE test setup using fio with 8 parallel 4 MB sequential reads. Default settings: 6.2 GB/s aggregate. With 64 nfsd threads, rsize=1048576, and max_reqs_per_slot=32: 9.1 GB/s aggregate. The bottleneck at that point shifted to the 10GbE NIC, not the NFS stack.
# RHEL/AlmaLinux - /etc/nfs.conf
[nfsd]
threads=64
# Ubuntu/Debian - /etc/default/nfs-kernel-server
RPCNFSDCOUNT=64
# Apply without full restart
systemctl restart nfs-server
# Verify thread count
cat /proc/fs/nfsd/threads
# Client-side slot tuning (apply at runtime)
sysctl -w sunrpc.max_reqs_per_slot=32
# Make permanent
echo 'sunrpc.max_reqs_per_slot=32' >> /etc/sysctl.d/90-nfs.conf
# Quick fio benchmark from client
fio --name=nfsread --directory=/mnt/data --rw=read --bs=4M \
--numjobs=8 --size=2G --runtime=30 --time_based --group_reporting
Security Hardening
Default NFS installations are not hardened. Root squashing helps but does not stop a compromised client from reading data it is authorized to access as a non-root user. Real NFS security requires network isolation combined with AUTH_SYS or, better, Kerberos.
AUTH_SYS (the default) trusts the UID and GID sent by the client. Any client with network access can claim any UID. This is acceptable only on networks where every machine is trusted and managed. For anything else, use Kerberos authentication. With krb5 export options, the server cryptographically verifies the client's identity before granting access.
The three Kerberos security flavors in NFS are krb5 (authentication only), krb5i (authentication plus integrity checking on every packet), and krb5p (authentication plus integrity plus encryption). krb5i is the right balance for internal networks: it prevents tampering in transit without the full CPU overhead of per-packet encryption. krb5p is appropriate for data crossing untrusted network segments.
Beyond authentication, restrict exports to specific IP ranges or hostnames rather than using the wildcard * for anything with write access. The export file snippet in the exports section above shows the correct pattern: use /24 CIDR notation to restrict to a management VLAN rather than exposing to 0.0.0.0/0.
Also disable NFSv2 and v3 if you do not need them. On RHEL-family systems this is controlled in /etc/nfs.conf. On the server, after disabling v3, verify with the /proc/fs/nfsd/versions file that v2 and v3 report as disabled.
For environments running automated infrastructure with tools like taskbotshub.ai, NFS mounts are commonly provisioned as part of CI pipeline setup. If you are automating NFS client configuration through such tooling, parametrize the server IP and export path rather than hardcoding them, and use Vault or a secrets manager to distribute Kerberos keytabs to agents rather than baking them into images.
Finally, enable NFS audit logging. The kernel does not log NFS access natively, but you can use auditd rules on the server to watch the exported directories for specific UID activity, or deploy nfswatch for live traffic inspection.
# /etc/nfs.conf - disable NFSv2 and v3
[nfsd]
vers2=no
vers3=no
vers4=yes
threads=64
# Kerberos export options in /etc/exports
/srv/nfs/secure 192.168.10.0/24(rw,sync,no_subtree_check,sec=krb5i)
# Verify security flavor negotiated on client
nfsstat -m | grep sec
# Confirm NFSv2/v3 disabled on server
cat /proc/fs/nfsd/versions
# Should show: -2 -3 +4 +4.1 +4.2
# auditd rule: watch NFS export directory for writes by UID >= 1000
auditctl -w /srv/nfs/data -p w -k nfs_writes
Diagnosing Common NFS Problems
NFS issues fall into a handful of categories: mount failures, stale file handles, permission errors, and performance degradation. Knowing which diagnostic to reach for first saves significant time.
Mount failures usually come down to firewall rules, exportfs not having applied the config, or a version mismatch. Run showmount -e serverip from the client to verify the server is advertising the export. If showmount hangs, port 111 (rpcbind) is blocked. If it returns cleanly but mount still fails with "access denied", the client's IP is not matched by the export rule - check that the client's actual source IP is within the CIDR range in /etc/exports.
Stale file handles (ESTALE errors) occur when a file or directory is deleted on the server while a client still holds a reference to it, or after an export path changes. The fix is to unmount and remount on the client. To prevent them in automated environments, use NFSv4 which handles stale handle recovery more gracefully than v3.
Permission denied errors when root_squash is active are expected behavior, not bugs. If a process on the client runs as root and tries to write to an export with root_squash, it will fail. Run the operation as the appropriate non-root UID or switch the export to no_root_squash only if the security model permits it.
Performance degradation investigation should start with nfsiostat on the client, which shows per-mount I/O statistics including bytes read/written per second, average operation latency, and retransmit counts. High retransmit counts indicate network packet loss or server overload. High average latency on small operations usually means the rsize/wsize settings are too small or the nfsd thread count on the server is too low.
The mountstats file in /proc/self/mountstats gives more detailed per-mount statistics than nfsiostat, including the breakdown of time spent in each RPC operation type.
# Check what server is exporting
showmount -e 192.168.10.10
# Check RPC service availability
rpcinfo -p 192.168.10.10
# Client I/O statistics
nfsiostat -m 5
# Refreshes every 5 seconds
# Detailed per-mount stats
cat /proc/self/mountstats
# Check for stale mounts
df -h 2>&1 | grep -i stale
# Server-side: what is currently mounted
showmount --all
# Server-side: check nfsd error counters
cat /proc/net/rpc/nfsd
# Force re-export without restart
exportfs -ra && exportfs -v
Persistent Configuration and Automation
A production NFS server needs its configuration reproducible from a clean install. Store /etc/exports, /etc/nfs.conf, and any bind-mount configuration in version control. The bind mounts that map real filesystem paths into the NFSv4 pseudoroot should be in /etc/fstab with the bind option, not done by hand in a startup script.
For Ansible-managed infrastructure, a two-task role handles the server: install the package, template /etc/exports, run exportfs -ra as a handler. The exports template should take a list of dictionaries with path, clients, and options fields so adding a new export is a single YAML entry without touching the template.
For teams managing many NFS-backed environments, naming exports and mount points consistently from the start prevents a lot of confusion at scale. The same discipline applies when naming internal DNS records that point to NFS servers - if you are setting up new infrastructure and registering domain names for internal services, the naming choices you make early persist for years. Tools like nicename.me can help evaluate whether a chosen hostname or subdomain is clean and available before you commit it to DNS and start embedding it in fstab entries across dozens of servers.
Monitoring an NFS server should cover at minimum: nfsd thread utilization (from /proc/fs/nfsd/threads vs active threads), export availability (alert if showmount -e fails from a remote host), and client-side mount latency via nfsiostat scraping. If you are running Prometheus, the node_exporter has NFS collector support that exposes nfsd RPC statistics as gauges and counters with no additional configuration beyond enabling the collector.
# fstab entries for bind mounts (persistent across reboot)
/data/shared /srv/nfs/data none bind 0 0
/home /srv/nfs/homes none bind 0 0
/scratch /srv/nfs/scratch none bind 0 0
# Ansible task snippet for NFS server
- name: Install NFS server
package:
name: nfs-utils
state: present
- name: Template exports file
template:
src: exports.j2
dest: /etc/exports
owner: root
group: root
mode: '0644'
notify: reload exports
# Handler
- name: reload exports
command: exportfs -ra
# Prometheus node_exporter with NFS collector
./node_exporter --collector.nfs --collector.nfsd