
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Manually installing operating systems via USB drives is unscalable, error-prone, and incompatible with modern infrastructure demands. PXE Boot for Automated OS Installs solves this by enabling network-based provisioning that delivers identical, auditable configurations across your entire fleet. This guide covers building a production-grade PXE environment using dnsmasq and iPXE, integrating it with configuration management workflows discussed in my Ansible automation guide to ensure every node starts in a known-good state.
How does PXE Boot for Automated OS Installs actually work?
PXE (Preboot Execution Environment) is not magic; it is a standardized sequence of network protocols that replaces local storage during the initial boot phase. Understanding this handshake prevents most troubleshooting headaches. When a server powers on with PXE enabled in firmware, the NIC broadcasts a DHCPDISCOVER packet with a specific option 60 tag identifying it as a PXE client.
Your DHCP server (typically dnsmasq in dedicated PXE setups) responds with an IP address plus two critical options: next-server (the TFTP server IP) and filename (the bootloader path). The client then contacts the TFTP server to download this bootloader—usually iPXE or GRUB—which contains logic to fetch the actual kernel and initramfs over HTTP. This separation matters: TFTP handles only the tiny initial payload because it lacks authentication and reliability features, while HTTP serves multi-gigabyte OS images efficiently.
In practice, I always use iPXE rather than legacy PXELINUX. iPXE supports HTTPS, iSCSI, FCoE, and scriptable menus, making it viable for complex environments where you need conditional logic based on hardware model or MAC address. Legacy PXELINUX is essentially dead in 2026; if you are starting fresh, skip it entirely.
How do you configure dnsmasq as a PXE server?
Dnsmasq is the standard choice for PXE infrastructure because it combines DHCP, TFTP, and DNS proxying in a single lightweight binary. On Ubuntu 24.04 LTS, installation is straightforward, but configuration requires precision to avoid conflicting with existing DHCP servers on your network.
sudo apt update && sudo apt install -y dnsmasq tftpd-hpa nginx Create a dedicated configuration file at /etc/dnsmasq.d/pxe.conf. Never edit the main dnsmasq.conf directly; modular configs survive upgrades and simplify auditing.
# /etc/dnsmasq.d/pxe.conf
interface=eth1 # Dedicated provisioning NIC only
bind-interfaces # Critical: prevents binding to all interfaces
dhcp-range=10.10.50.100,10.10.50.200,12h
dhcp-option=66,10.10.50.1 # next-server (TFTP IP)
dhcp-boot=ipxe.efi # UEFI bootloader filename
enable-tftp
tftp-root=/var/lib/tftpboot
log-dhcp # Essential for debugging PXE handshakes
dhcp-match=set:bios,option:client-arch,0
dhcp-boot=tag:bios,undionly.kpxe
dhcp-match=set:efi64,option:client-arch,9
dhcp-boot=tag:efi64,ipxe.efi The bind-interfaces directive is non-negotiable in multi-homed environments. Without it, dnsmasq may respond to DHCP requests on your production network, causing outages. I have seen this mistake take down entire datacenter segments during lab setup. Always verify with ss -ulnp | grep :67 that dnsmasq listens only on the intended interface.
For BIOS/legacy clients, you need undionly.kpxe; for UEFI, use ipxe.efi. Modern hardware is predominantly UEFI, but keeping both ensures compatibility with older inventory. Test both paths before declaring production readiness.
How do you create iPXE boot menus for unattended installs?
iPXE scripts transform PXE from a simple loader into a programmable provisioning platform. Place your menu at /var/lib/tftpboot/boot.ipxe and reference it from the embedded iPXE binary or chainload it from the initial DHCP filename.
#!ipxe
# /var/lib/tftpboot/boot.ipxe
menu PXE Boot for Automated OS Installs
item --gap --- Available Distributions ---
item ubuntu2404 Ubuntu 24.04 LTS Server (Auto)
item rocky9 Rocky Linux 9.4 (Kickstart)
item rescue SystemRescue 11.x
item shell Drop to iPXE shell
choose target || goto shell
goto ${target}
:ubuntu2404
kernel http://mirror.local/ubuntu/24.04/vmlinuz auto=true priority=critical preseed/url=http://mirror.local/preseed/ubuntu24.cfg netcfg/get_hostname=${hostname}
initrd http://mirror.local/ubuntu/24.04/initrd.gz
boot
:rocky9
kernel http://mirror.local/rocky/9.4/BaseOS/x86_64/os/images/pxeboot/vmlinuz inst.ks=http://mirror.local/kickstart/rocky9.ks ip=dhcp
initrd http://mirror.local/rocky/9.4/BaseOS/x86_64/os/images/pxeboot/initrd.img
boot
:rescue
chain http://mirror.local/rescue/sysresccd.ipxe
:shell
echo Type 'exit' to return to menu
shell This menu demonstrates three key patterns: parameterized kernel arguments for unattended installs, variable substitution (${hostname}) pulled from DHCP or iPXE variables, and fallback safety via the shell option. The auto=true priority=critical flags tell Debian/Ubuntu installers to suppress all interactive prompts when combined with a valid preseed file.
A common mistake is hardcoding IPs in kernel lines. Always use hostnames resolvable by your PXE network's DNS (which dnsmasq can serve locally). This makes mirror migrations transparent to boot configs. For deeper integration with configuration management after install, pass cloud-init user-data URLs directly in kernel parameters rather than embedding configs in initrds.
What is the difference between preseed, kickstart, and cloud-init?
Choosing the right automation method depends on your distribution and long-term maintenance strategy. Each has distinct strengths, and mixing them incorrectly causes subtle failures.
| Method | Distributions | Best For | Limitations |
|---|---|---|---|
| Preseed | Debian, Ubuntu | Full offline installs, partitioning control | Deprecated in Ubuntu 24.04+, verbose syntax |
| Kickstart | RHEL, Rocky, Alma | Enterprise RHEL clones, %post scripting | RHEL-specific, limited cross-distro portability |
| Autoinstall | Ubuntu 20.04+ | Modern Ubuntu, YAML-based, Subiquity | Requires curtin/cloud-init integration |
| cloud-init | All major distros | Post-install config, user setup, package install | Not a full installer replacement, runs after base OS |
In 2026, my recommendation is clear: use Autoinstall for Ubuntu and Kickstart for RHEL derivatives to handle disk partitioning and base package selection, then delegate all post-install configuration to cloud-init. This separation means your installer config rarely changes, while cloud-init user-data evolves with your application needs. It also aligns with immutable infrastructure principles covered in my golden images with Packer guide, where the base image stays static and runtime configuration is injected.
Never embed secrets in preseed or kickstart files served over HTTP. Use cloud-init's ability to fetch secrets from HashiCorp Vault or AWS Secrets Manager at runtime instead. If you must include credentials during install, serve those files over HTTPS with client certificate authentication—a pattern I detail in my SSH hardening guide for defense-in-depth.
How do you troubleshoot PXE boot failures systematically?
PXE failures fall into four categories, each with distinct diagnostic approaches. Work through them in order; jumping ahead wastes time.
- No DHCP response: Verify
bind-interfacesis set, check firewall rules for UDP 67/68, and confirm the client's NIC is PXE-enabled in firmware. Usetcpdump -i eth1 port 67 or port 68on the server to see if DISCOVER packets arrive. Missing responses usually mean wrong interface binding or VLAN misconfiguration. - TFTP timeout: Confirm file permissions (
chmod 644, owned bytftp:tftp), verify SELinux/AppArmor isn't blocking access, and test manually withtftp 10.10.50.1 -c get ipxe.efifrom another host. TFTP has no error codes beyond "file not found," so permission issues manifest as timeouts. - iPXE script errors: Enable debug output by adding
set debug allat the top of your boot.ipxe. Common issues include missing#!ipxeshebang, incorrect variable expansion, or HTTP 404s on kernel paths. Always validate URLs with curl before testing in PXE. - Installer hangs: Switch to verbose console output by removing
quiet splashfrom kernel lines. For Ubuntu Autoinstall, adddebug-cloudinitto kernel params. Check virtual consoles (Ctrl+Alt+F2-F6) for installer logs. Most hangs stem from missing preseed answers or network connectivity during package download.
Keep a known-good iPXE shell entry in your menu indefinitely. When automation fails, dropping to the shell lets you manually test DHCP (dhcp), ping mirrors (ping mirror.local), and fetch files (chain http://...) without rebooting. This alone cuts troubleshooting time by 80% in my experience.
Implementing PXE Boot for Automated OS Installs in Production
PXE Boot for Automated OS Installs transforms bare-metal provisioning from a manual chore into a repeatable, auditable engineering process. Start with a dedicated provisioning VLAN isolated from production traffic, validate every boot path with both BIOS and UEFI clients, and integrate cloud-init early to avoid installer bloat. Monitor your dnsmasq logs and mirror bandwidth—PXE storms during fleet deployments can saturate links faster than expected. If you are designing new infrastructure or migrating legacy provisioning systems and need hands-on guidance, reach out to discuss your environment.