When Do You Actually Need PXE Boot?
The first time I needed to install Ubuntu on 12 brand-new servers all at once, the only option was to plug a USB drive into each one and sit there clicking Next → Next → Install all afternoon. Nearly 4 hours gone — and that was before manually configuring each machine afterward. That day, I decided to set up a PXE Boot Server.
PXE (Preboot eXecution Environment) lets machines boot over their network card instead of USB or DVD, pulling the OS installer directly from an internal server. For a 50-person office and a small datacenter like mine, this has been the single biggest time-saver I’ve ever set up. Install 10 machines simultaneously. No USB drives. No babysitting — just boot them up and walk away.
How PXE Boot Architecture Works
Understanding this flow upfront will save you a lot of debugging time when things go wrong:
- The client machine boots; BIOS/UEFI sends a DHCP Discover with PXE option
- The DHCP server responds with an IP address, TFTP server address, and bootloader filename
- The client downloads the bootloader (pxelinux.0 or grubx64.efi) via TFTP
- The bootloader displays a menu; the user selects the OS to install
- The kernel and initrd are downloaded via TFTP; the OS installer runs and mounts NFS or HTTP to fetch installation files
Three required components: a DHCP server (dnsmasq or isc-dhcp-server), a TFTP server (tftpd-hpa), and an NFS/HTTP server to serve OS images. If you need a refresher on how IP addressing, subnets, and DHCP fit together, that foundation will make this setup much easier to follow.
Installing the Required Components
I’m using Ubuntu 22.04 as the PXE server. Install everything in one shot:
sudo apt update
sudo apt install -y dnsmasq tftpd-hpa nfs-kernel-server syslinux-common pxelinux
If you’re running systemd-resolved, disable it first — it occupies port 53 and conflicts directly with dnsmasq. This is the same port used by a dedicated DNS server like Bind9, so any service listening there will cause the same conflict:
sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
Assign a static IP to the PXE server’s network interface — I’m using 192.168.10.1 on interface eth1 (a dedicated management network interface):
# /etc/netplan/99-pxe.yaml
network:
version: 2
ethernets:
eth1:
addresses: [192.168.10.1/24]
dhcp4: false
sudo netplan apply
Detailed Configuration for Each Component
1. DHCP + TFTP with dnsmasq
I chose dnsmasq over isc-dhcp-server because its config is cleaner, easier to read, and it includes built-in TFTP proxy support without additional packages. Create the config file:
sudo nano /etc/dnsmasq.d/pxe.conf
# Interface to serve
interface=eth1
bind-interfaces
# DHCP range for management network
dhcp-range=192.168.10.100,192.168.10.200,12h
# TFTP server
enable-tftp
tftp-root=/srv/tftp
# PXE boot file — BIOS
dhcp-boot=pxelinux/pxelinux.0
# PXE boot file — UEFI (uncomment if UEFI support is needed)
# dhcp-match=set:efi-x86_64,option:client-arch,7
# dhcp-boot=tag:efi-x86_64,grubx64.efi
# Logging for debugging
log-dhcp
log-facility=/var/log/dnsmasq.log
sudo systemctl restart dnsmasq
sudo systemctl enable dnsmasq
2. TFTP Directory Structure
Copy the bootloader into the TFTP root:
sudo mkdir -p /srv/tftp/pxelinux
# Copy pxelinux bootloader
sudo cp /usr/lib/PXELINUX/pxelinux.0 /srv/tftp/pxelinux/
# Copy required modules
sudo cp /usr/lib/syslinux/modules/bios/{ldlinux,menu,vesamenu,libutil,chain}.c32 /srv/tftp/pxelinux/
# Create directory for menu config
sudo mkdir -p /srv/tftp/pxelinux/pxelinux.cfg
3. Creating the PXE Boot Menu
The default menu file applies to all clients without a specific config. TIMEOUT 100 means 10 seconds before automatically booting the first entry:
sudo nano /srv/tftp/pxelinux/pxelinux.cfg/default
UI vesamenu.c32
TIMEOUT 100
PROMPT 0
MENU TITLE PXE Boot Menu — IT From Zero
LABEL ubuntu2204
MENU LABEL Ubuntu 22.04 LTS (x64)
KERNEL ubuntu22.04/vmlinuz
APPEND initrd=ubuntu22.04/initrd root=/dev/nfs nfsroot=192.168.10.1:/srv/nfs/ubuntu22.04 ip=dhcp quiet splash
LABEL ubuntu2404
MENU LABEL Ubuntu 24.04 LTS (x64)
KERNEL ubuntu24.04/vmlinuz
APPEND initrd=ubuntu24.04/initrd root=/dev/nfs nfsroot=192.168.10.1:/srv/nfs/ubuntu24.04 ip=dhcp quiet splash
LABEL local
MENU LABEL Boot from local disk
LOCALBOOT 0
4. Mounting the ISO and Preparing NFS
Download the Ubuntu ISO, mount it, and export it via NFS:
# Create directories
sudo mkdir -p /srv/nfs/ubuntu22.04
sudo mkdir -p /mnt/ubuntu22.04
# Download ISO (or copy from USB)
wget https://releases.ubuntu.com/22.04/ubuntu-22.04.3-live-server-amd64.iso -O /tmp/ubuntu22.04.iso
# Mount the ISO
sudo mount -o loop /tmp/ubuntu22.04.iso /mnt/ubuntu22.04
# Copy full ISO contents to NFS share
sudo rsync -av /mnt/ubuntu22.04/ /srv/nfs/ubuntu22.04/
# Copy kernel and initrd to TFTP
sudo mkdir -p /srv/tftp/ubuntu22.04
sudo cp /srv/nfs/ubuntu22.04/casper/vmlinuz /srv/tftp/ubuntu22.04/
sudo cp /srv/nfs/ubuntu22.04/casper/initrd /srv/tftp/ubuntu22.04/
Configure NFS exports:
sudo nano /etc/exports
/srv/nfs/ubuntu22.04 192.168.10.0/24(ro,sync,no_subtree_check,no_root_squash)
/srv/nfs/ubuntu24.04 192.168.10.0/24(ro,sync,no_subtree_check,no_root_squash)
sudo exportfs -ra
sudo systemctl restart nfs-kernel-server
sudo systemctl enable nfs-kernel-server
5. Pro Tip: Per-MAC-Address PXE Configuration
This is the feature I use most often when different machines need different OS installations. The filename must match the MAC address exactly, with the 01- prefix fixed for Ethernet:
# Format: 01-aa-bb-cc-dd-ee-ff
sudo nano /srv/tftp/pxelinux/pxelinux.cfg/01-aa-bb-cc-dd-ee-ff
# This machine will automatically boot Ubuntu 24.04
DEFAULT ubuntu2404
LABEL ubuntu2404
KERNEL ubuntu24.04/vmlinuz
APPEND initrd=ubuntu24.04/initrd root=/dev/nfs nfsroot=192.168.10.1:/srv/nfs/ubuntu24.04 ip=dhcp quiet splash
Incredibly convenient when a new batch of machines arrives — pre-create a config for each MAC address, power them on, and the right OS installer runs automatically. No need to babysit each one.
Testing and Monitoring
Verify TFTP Is Working
# Install tftp client for testing
sudo apt install tftp-hpa
# Test downloading a file from the TFTP server
tftp 192.168.10.1
tftp> get pxelinux/pxelinux.0
tftp> quit
# Verify the file was downloaded
ls -lh pxelinux.0
Watch DHCP Logs in Real Time
# Monitor which clients are sending PXE requests
sudo tail -f /var/log/dnsmasq.log | grep -E "DHCP|PXE|TFTP"
# Or use journalctl
sudo journalctl -u dnsmasq -f
A successful PXE client boot produces logs like this — seeing the sent pxelinux.0 line means everything is working. For deeper packet-level inspection of what’s crossing the wire during the boot sequence, tcpdump and Wireshark are invaluable for tracing exactly where DHCP or TFTP exchanges break down:
dnsmasq-dhcp: DHCPDISCOVER(eth1) aa:bb:cc:dd:ee:ff
dnsmasq-dhcp: DHCPOFFER(eth1) 192.168.10.105 aa:bb:cc:dd:ee:ff
dnsmasq-tftp: sent /srv/tftp/pxelinux/pxelinux.0 to 192.168.10.105
Verify NFS Exports
# List current exports
showmount -e 192.168.10.1
# Test mounting from a client
sudo mount -t nfs 192.168.10.1:/srv/nfs/ubuntu22.04 /mnt/test
ls /mnt/test
Quick Health Check Script for the Entire Stack
#!/bin/bash
# check-pxe.sh — quick PXE server health check
echo "=== PXE Server Health Check ==="
echo -n "[dnsmasq] "
systemctl is-active dnsmasq
echo -n "[tftpd] "
systemctl is-active tftpd-hpa
echo -n "[nfs] "
systemctl is-active nfs-kernel-server
echo -n "[TFTP dir] "
ls /srv/tftp/pxelinux/pxelinux.0 &>/dev/null && echo "OK" || echo "MISSING"
echo -n "[NFS exp] "
showmount -e localhost 2>/dev/null | grep -q srv && echo "OK" || echo "NO EXPORTS"
echo "===="
chmod +x check-pxe.sh
./check-pxe.sh
Lessons Learned from Real-World Use
- Firewall first: Open UDP 67/68 (DHCP), UDP 69 (TFTP), TCP 2049 (NFS). Forgetting this will have you debugging forever — I learned that the hard way. Running through a Linux server security checklist before exposing the PXE server to the network is time well spent.
- UEFI vs BIOS: Most machines from 2020 onward are UEFI. You’ll need a separate dhcp-boot entry for EFI and use grubx64.efi instead of pxelinux.0. It’s already commented out in the config file above — just uncomment it.
- Network switch and client BIOS: You must enable PXE Boot in each client’s BIOS and verify the correct VLAN is set. I once wasted a full hour just because I forgot to enable this option on a managed switch — sat there watching the client broadcast endlessly while the DHCP server stayed completely silent. If your environment uses network segmentation, the article on configuring VLANs on Linux covers exactly how to wire this up correctly.
- NFS instead of TFTP for large files: The Ubuntu Server ISO is 1.4GB+. TFTP is slow and unreliable for large files. Use TFTP only for the bootloader and kernel; let NFS handle everything else.
- Take it further with preseed/kickstart: Combine preseed (Ubuntu) or kickstart (RHEL/Rocky) for a fully unattended install — boot the machine, go make coffee, come back to find it fully installed and waiting at the login prompt.
After getting the PXE server up and running, I installed Rocky Linux on 15 datacenter machines in about 45 minutes — coffee break included. There’s simply no comparison to the old way of plugging in USB drives one by one.
