Reflashing an SD card every time you rebuild BusyBox or add one file gets old fast. An NFS root filesystem lets your target board mount its / straight from a directory on your host over the network — edit a file on the host, and it’s live on the target instantly, no re-imaging required. This lecture in our free linux development course covers kernel configuration, host-side NFS server setup, and testing on both QEMU and real hardware.
What You Will Learn
Lecture Outcomes
Prerequisites
You’ll need a staging directory from earlier in this root filesystem image series, a Linux host that can run an NFS server, and either QEMU or a real board with a network interface and a bootloader that can set kernel command-line arguments. A working host-to-target network path — a QEMU tap interface or a real Ethernet cable — is required before any of this will boot.
Why Use NFS Root at All
Once your board can reach the network at boot, mounting root over NFS turns the entire development loop into a simple file save. There’s no image to rebuild, no card to remove and reflash, and any debug tool you drop into the exported directory shows up on the target on the next access — no reboot needed either, in most cases. The trade-off is that this is strictly a development technique: production devices should ship a self-contained image, not depend on a development host being reachable at boot.
Kernel Configuration
The NFS client has to be built into the kernel itself — not as a module — because root has to be mounted before any module can be loaded from the (not yet mounted) root filesystem. Enable it with:
CONFIG_NFS_FS=y
CONFIG_ROOT_NFS=y
CONFIG_IP_PNP=y
CONFIG_IP_PNP is optional if you plan to set the IP address explicitly on the kernel command line (which is what we do below), but it’s worth enabling if you ever want DHCP-based boot instead.
The Three Boot Parameters
| Parameter | Purpose | Example |
|---|---|---|
root=/dev/nfs | Tells the kernel the root device is an NFS mount, not a block device | root=/dev/nfs |
nfsroot= | Server IP and exported path to mount as / | nfsroot=192.168.7.1:/srv/nfs/target-root |
ip= | Brings the target’s network interface up early enough for the NFS mount to succeed, before init runs | ip=192.168.7.101::192.168.7.1:255.255.255.0::eth0:off |
The full ip= form is ip=<client-ip>:<server-ip>:<gateway>:<netmask>:<hostname>:<device>:<autoconf>. Leaving fields empty (as above) just skips them; here we’re setting a static IP with no hostname and no DHCP fallback (off).
Setting Up the NFS Server on Your Host
Install the server package and point it at a directory holding a full staging tree — a fresh copy, not your working staging directory, so build tooling doesn’t collide with a live target:
$ sudo apt-get install nfs-kernel-server
$ sudo cp -a staging /srv/nfs/target-root
Export the directory by adding a line to /etc/exports:
/srv/nfs/target-root 192.168.7.0/24(rw,sync,no_subtree_check,no_root_squash)
| Option | Meaning |
|---|---|
rw | Target can write to the export, not just read |
sync | Server acknowledges writes only once they’re actually on disk — safer, slightly slower |
no_subtree_check | Skips a subtree-consistency check that’s mostly relevant when exporting a subdirectory of a larger filesystem |
no_root_squash | Lets the target’s root user act as root on the export — needed during development, dangerous on an untrusted network |
Reload the exports and restart the service:
$ sudo exportfs -ra
$ sudo systemctl restart nfs-kernel-server
Testing Under QEMU
QEMU needs a virtual network device bridged to a tap interface so the guest kernel can actually reach the host’s NFS export. This original script sets up the tap interface, then launches an aarch64 target under QEMU with an NFS root:
#!/bin/bash
KERNEL=Image
DTB=virt.dtb
EXPORT_DIR=/srv/nfs/target-root
HOST_IP=192.168.7.1
TARGET_IP=192.168.7.101
NET=192.168.7.0
MASK=255.255.255.0
sudo ip tuntap add dev tap0 mode tap user "$(whoami)"
sudo ip addr add ${HOST_IP}/24 dev tap0
sudo ip link set tap0 up
sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
qemu-system-aarch64 -M virt -cpu cortex-a53 -m 512M -nographic \
-kernel "$KERNEL" -dtb "$DTB" \
-append "console=ttyAMA0 root=/dev/nfs rw nfsroot=${HOST_IP}:${EXPORT_DIR} ip=${TARGET_IP}::${HOST_IP}:${MASK}::eth0:off" \
-netdev tap,id=net0,ifname=tap0,script=no \
-device virtio-net-device,netdev=net0
Save this as run-qemu-nfsroot.sh. On boot, the guest brings up its virtio NIC using the static ip= parameters, then mounts /srv/nfs/target-root straight off the host as /. Any file you edit in that directory on the host is immediately visible inside the running guest.
Testing on Real Hardware
The same three parameters apply at a real board’s U-Boot prompt — set them as environment variables, then load and boot the kernel as usual:
=> setenv serverip 192.168.7.1
=> setenv ipaddr 192.168.7.101
=> setenv nfspath /srv/nfs/target-root
=> setenv bootargs console=ttyS0,115200 root=/dev/nfs rw nfsroot=${serverip}:${nfspath} ip=${ipaddr}::${serverip}:255.255.255.0::eth0:off
=> tftpboot 0x82000000 zImage
=> tftpboot 0x83000000 board.dtb
=> bootz 0x82000000 - 0x83000000
The File Ownership Problem
NFS root exposes a real ownership headache during development: files already in your staging tree are owned by your own host UID, typically 1000, so on the target they show up owned by that same numeric UID rather than root. Anything the target itself creates as root shows up owned by UID 0. The two views of the filesystem disagree about who owns what, and it only gets more confusing the longer the export is shared between a build host and a live target.
There’s no clean fix, only a working pattern: keep a separate, root-owned copy of the export dedicated to the running target, and rebuild it from your staging directory instead of exporting your staging directory directly:
$ sudo rm -rf /srv/nfs/target-root
$ sudo cp -a staging /srv/nfs/target-root
$ sudo chown -R 0:0 /srv/nfs/target-root
NFS Root vs. Image-Based Root
| Approach | Iteration speed | Needs host reachable at boot | Use case |
|---|---|---|---|
| NFS root | Instant — edit and rerun | Yes | Active development |
| SD/eMMC image | Minutes — rebuild and reflash | No | Field testing, staging |
| Signed production image | N/A — released, not iterated | No | Shipped devices |
Common Mistakes and Troubleshooting
- Forgetting
ip=— the kernel has no address configured yet at the point it tries to mount root, so the mount times out even though the server is fine. - Firewall blocking NFS/RPC ports — NFS uses several ports beyond 2049 for the older protocol versions; a host firewall dropping them looks identical to a network cabling problem from the target’s side.
- Exporting the wrong path or forgetting
exportfs -ra— changes to/etc/exportsdon’t take effect until you reload them. - Building NFS client support as a module — it must be built in (
=y), since modules can’t load before root is mounted.
Best Practices
- Keep the NFS export subnet isolated from anything untrusted —
no_root_squasheffectively hands out root on the export to anyone who can reach it. - Automate the “rebuild the root-owned export copy” step as part of your normal build script instead of doing it manually after every change.
- Never carry NFS-root bootargs into a production image build — treat it strictly as a development-time root method.
Summary
An NFS root filesystem turns the rebuild-reflash-reboot loop into a simple file edit by mounting the target’s / straight from your host. It needs the NFS client built into the kernel, three boot parameters (root=/dev/nfs, nfsroot=, ip=), and a properly exported directory on the host — plus a deliberate workaround for the UID mismatch between your build host and the target. It’s purely a development technique; production devices still ship a real image, which is exactly what the previous lecture in this free linux device drivers course covered.
FAQ
Can NFS root support be built as a kernel module?
No — root has to mount before any module can load from the (not yet available) root filesystem, so NFS client support must be built in.
Why does my board hang waiting for the NFS mount?
Almost always a missing or wrong ip= parameter — without it the target has no network address yet when it tries to mount root.
Is no_root_squash safe to use?
Only on a trusted, isolated development network. It lets the target’s root user act as root on the export, which is exactly what you don’t want on a shared or untrusted network.
Should I ship a product with an NFS root filesystem?
No — production devices need to boot standalone. Use NFS root during development, then switch to a real image (like the one built in the previous lecture) for anything that ships.
Why do files on the target show up owned by a random-looking UID?
Because the export directory was built by your ordinary host user; the target simply sees the same numeric UID with no matching account, which is why a dedicated root-owned copy of the export avoids the confusion.
Does NFS root work the same way over QEMU and real hardware?
Yes — the kernel-side configuration and bootargs are identical; only how you bring up the network (tap interface for QEMU, a real cable for hardware) differs.
Continue the Free Embedded Linux Course
Next up: loading the kernel itself over the network with TFTP, so you’re not reflashing that either.
Next Lecture Browse the Full Course
2 Comments