Skip to content
Go back

Network Booting Diskless Nodes with iPXE

By SumGuy 12 min read
Network Booting Diskless Nodes with iPXE

You Don’t Actually Need That USB Drive

Here’s a scenario you’ve probably lived: it’s 2 AM, you need to reinstall Debian on a node, and the only USB drive you can find is either holding a vacation photo backup or running a different OS from a week ago. So you spend 20 minutes burning a new image, walk it over to the machine, boot from it, and wonder why you’re doing this in 2026.

Network booting exists. iPXE specifically exists. And honestly, once you set it up, you’ll never reach for Ventoy again — at least not for lab nodes.

iPXE is a modern, open-source PXE replacement that speaks HTTP(S), iSCSI, AoE, and NFS instead of just TFTP. It supports scripted boot menus, chainloading, and can be embedded with your own config baked in. The result: power on a machine, it pulls your menu.ipxe over HTTP, you pick Ubuntu or Talos OS or memtest, and you’re off. No USB. No running to a drawer. No dd if=/dev/zero-ing a drive you forgot had data on it.

Let’s build this properly.


Why Bother: The Real Use Cases

Before we dive into config files, let’s talk about why this is worth your time beyond “it’s cool.”

Diskless k3s/Talos clusters — Talos OS in particular is designed to PXE boot well. You can run a Talos node without a local disk at all, booting its squashfs from the network. This is genuinely useful when you’re spinning up temporary compute nodes or testing cluster topologies.

Recovery and reinstall without physical access — If your nodes are in another room, a rack, or a colocation facility, having a PXE boot server means you can wipe and reinstall remotely. Combine with IPMI/iDRAC and you’ve got a proper out-of-band workflow.

Standardized provisioning — One boot menu with canonical Ubuntu/Debian images means all your nodes start from the same known-good point. No “was it the 22.04.3 ISO or 22.04.4 that I burned six months ago?”

The “any OS” shortcut — Chainload netboot.xyz and you get a curated, always-updated menu of 100+ OS images without hosting anything yourself. Instant win.


How iPXE Actually Works

The boot flow looks like this:

NIC firmware (legacy PXE or UEFI)
→ DHCP request
→ DHCP server replies with next-server + bootfile
→ Client downloads bootfile (iPXE binary) via TFTP
→ iPXE runs, sends another DHCP request (with option 175 set)
→ DHCP server sees option 175, replies with HTTP URL
→ iPXE fetches menu.ipxe over HTTP
→ menu.ipxe presents options or boots directly
→ Kernel + initrd downloaded over HTTP
→ OS boots

The key insight is that iPXE does two DHCP requests. The first one is from the NIC firmware (dumb, TFTP only). The second is from iPXE itself, which sets DHCP option 175 to signal “I speak iPXE.” Your DHCP server hands out a different bootfile in response — the HTTP URL to your menu script.

This is why the dnsmasq config has two dhcp-boot lines. One for clients that haven’t loaded iPXE yet, one for clients that have.


The DHCP Side: dnsmasq in ProxyDHCP Mode

You almost certainly already have a DHCP server — your router, pfSense, whatever. You don’t want to replace it. ProxyDHCP mode lets dnsmasq listen for DHCP requests and inject boot options without handing out IP addresses. Your real DHCP server keeps doing what it does.

Here’s a working dnsmasq.conf for this setup:

# Run as a DHCP proxy, not a full DHCP server
# Replace 192.168.1.0 with your actual subnet
dhcp-range=192.168.1.0,proxy
# Match clients that have already loaded iPXE (they set option 175)
dhcp-match=set:ipxe,175
# For clients that haven't loaded iPXE yet: serve the iPXE binary via TFTP
# undionly.kpxe for legacy BIOS, use ipxe.efi for UEFI (see UEFI section)
dhcp-boot=tag:!ipxe,undionly.kpxe
# For clients that have loaded iPXE: serve the HTTP menu script
dhcp-boot=tag:ipxe,http://192.168.1.50/menu.ipxe
# TFTP server to serve the iPXE binary itself
enable-tftp
tftp-root=/var/lib/tftpboot
# Log DHCP activity so you can debug
log-dhcp

Replace 192.168.1.50 with the IP of your HTTP server. Run dnsmasq with --no-daemon for initial debugging so you can watch the DHCP handshake happen in real time.

Start dnsmasq:

Terminal window
# Test your config first
dnsmasq --test --conf-file=/etc/dnsmasq.d/pxe.conf
# Run it
systemctl restart dnsmasq

Grabbing the iPXE Binaries

You need the TFTP-served iPXE binary that your NIC firmware downloads on first boot. The easiest way is to grab the prebuilt ones:

Terminal window
# Create your TFTP root
mkdir -p /var/lib/tftpboot
# Download undionly.kpxe (legacy BIOS chainloader)
curl -o /var/lib/tftpboot/undionly.kpxe \
https://boot.ipxe.org/undionly.kpxe
# Download UEFI version
curl -o /var/lib/tftpboot/ipxe.efi \
https://boot.ipxe.org/ipxe.efi
# Verify they downloaded correctly
ls -lh /var/lib/tftpboot/

If you don’t trust random internet binaries (fair), see the “Building Your Own iPXE Binary” section below.


Hosting the Boot Files: A Tiny nginx Container

You need an HTTP server to serve menu.ipxe and your OS kernels/initrds. A single nginx container is the least-friction option:

docker-compose.yml
services:
pxe-http:
image: nginx:1.27-alpine
container_name: pxe-http
ports:
- "80:80"
volumes:
- ./pxe-root:/usr/share/nginx/html:ro
restart: unless-stopped

Your directory structure:

Terminal window
pxe-root/
├── menu.ipxe
├── ubuntu/
├── vmlinuz
└── initrd
├── debian/
├── linux
└── initrd.gz
├── talos/
├── vmlinuz
└── initramfs.xz
└── memtest/
└── memtest86+-7.20.iso

Grab your OS kernels from the official netinstall images. For Ubuntu 24.04:

Terminal window
mkdir -p pxe-root/ubuntu
cd pxe-root/ubuntu
# Extract just the kernel and initrd from the netinstall ISO
wget https://releases.ubuntu.com/24.04/ubuntu-24.04.2-live-server-amd64.iso
sudo mount -o loop ubuntu-24.04.2-live-server-amd64.iso /mnt
cp /mnt/casper/vmlinuz .
cp /mnt/casper/initrd .
sudo umount /mnt

For Talos OS 1.9.x, they publish kernel/initramfs directly:

Terminal window
mkdir -p pxe-root/talos
cd pxe-root/talos
TALOS_VERSION=v1.9.5
wget https://github.com/siderolabs/talos/releases/download/${TALOS_VERSION}/vmlinuz-amd64 -O vmlinuz
wget https://github.com/siderolabs/talos/releases/download/${TALOS_VERSION}/initramfs-amd64.xz -O initramfs.xz

Verify your HTTP server is working before expecting PXE to:

Terminal window
curl -I http://192.168.1.50/menu.ipxe
# Should return HTTP/1.1 200 OK
curl -I http://192.168.1.50/ubuntu/vmlinuz
# Should return 200 and a reasonable Content-Length

If curl fails, iPXE will fail. Check your firewall too — port 80 needs to be open on your PXE server.


Writing the Boot Menu

Here’s a real menu.ipxe with multiple OS options:

#!ipxe
# Set your HTTP server base URL
set base http://192.168.1.50
:start
menu SumGuy Home Lab Boot Menu
item ubuntu Ubuntu 24.04 LTS Server (netinstall)
item debian Debian 12 Bookworm (netinstall)
item talos Talos OS v1.9.5 (diskless cluster node)
item memtest Memtest86+ 7.20 (RAM check)
item gparted GParted Live (disk rescue)
item netbootxyz netboot.xyz (every other OS ever)
item shell iPXE shell (for debugging)
item --gap -- ---
item reboot Reboot
choose --timeout 30 --default ubuntu target && goto ${target}
:ubuntu
kernel ${base}/ubuntu/vmlinuz
initrd ${base}/ubuntu/initrd
imgargs vmlinuz autoinstall quiet splash ---
boot || goto failed
:debian
kernel ${base}/debian/linux
initrd ${base}/debian/initrd.gz
imgargs linux auto=true priority=critical vga=788 ---
boot || goto failed
:talos
kernel ${base}/talos/vmlinuz
initrd ${base}/talos/initramfs.xz
imgargs vmlinuz talos.platform=metal console=tty0 console=ttyS0,115200
boot || goto failed
:memtest
# Memtest boots as an ISO image via isofile
set isofile ${base}/memtest/memtest86+-7.20.iso
kernel https://boot.ipxe.org/wimboot
initrd ${isofile} memtest.iso
boot || goto failed
:gparted
kernel ${base}/gparted/vmlinuz
initrd ${base}/gparted/initrd.img
imgargs vmlinuz boot=live union=overlay username=user quiet splash vga=788 fetch=${base}/gparted/filesystem.squashfs
boot || goto failed
:netbootxyz
chain --autofree https://boot.netboot.xyz
goto start
:shell
shell
:reboot
reboot
:failed
echo Boot failed. Press any key to return to menu.
prompt
goto start

The || syntax means “if this fails, jump to failed.” Always include it — a failed boot attempt without a fallback locks the machine.

The --timeout 30 on the choose line means it auto-boots Ubuntu after 30 seconds if you don’t pick anything. Remove --default if you want it to wait forever.


The netboot.xyz Shortcut

If you just want “I can boot any OS without hosting anything,” chainloading netboot.xyz takes 30 seconds to set up. Replace your entire menu.ipxe with:

#!ipxe
chain --autofree https://boot.netboot.xyz

Done. netboot.xyz maintains a curated, regularly-updated menu with Ubuntu, Debian, Arch, Alpine, Fedora, FreeBSD, Rocky, Alma, CoreOS variants, and more. It’s genuinely excellent. The only reason not to use it as your primary setup is if you want custom automation (like Talos diskless configs or preseed/cloud-init URL injection) or offline capability.

You can also run netboot.xyz locally — they publish Docker images and a self-hosted version if you want the menu without the external dependency:

Terminal window
docker run -d \
--name netbootxyz \
-p 3000:3000 \
-p 69:69/udp \
-p 8080:8080 \
ghcr.io/netbootxyz/netbootxyz:latest

Building a Custom iPXE Binary

The prebuilt iPXE binaries work, but rolling your own gives you embedded scripts, HTTPS support with trusted certs, and the ability to bake in your menu URL so the DHCP option isn’t even necessary.

Terminal window
# Install build deps (Debian/Ubuntu)
sudo apt install -y git build-essential liblzma-dev
# Clone iPXE source
git clone https://github.com/ipxe/ipxe.git /usr/src/ipxe
cd /usr/src/ipxe/src
# Create your embedded script
cat > /tmp/embedded.ipxe << 'EOF'
#!ipxe
dhcp
chain http://192.168.1.50/menu.ipxe
EOF
# Build legacy BIOS chainloader with embedded script
make bin/undionly.kpxe EMBED=/tmp/embedded.ipxe
# Build UEFI binary with embedded script
make bin-x86_64-efi/ipxe.efi EMBED=/tmp/embedded.ipxe
# Copy to TFTP root
cp bin/undionly.kpxe /var/lib/tftpboot/
cp bin-x86_64-efi/ipxe.efi /var/lib/tftpboot/

To add HTTPS support (so you can serve menus over TLS), enable the crypto features in the build:

Terminal window
make bin-x86_64-efi/ipxe.efi \
EMBED=/tmp/embedded.ipxe \
CONFIG=general \
"EXTRA_CFLAGS=-DDOWNLOAD_PROTO_HTTPS"

Build time is a few minutes on modern hardware. The resulting binaries are drop-in replacements for the prebuilt ones.


UEFI vs Legacy BIOS: The Annoying Part

Legacy BIOS is simpler. You have one bootfile: undionly.kpxe. It works, full stop.

UEFI is where things get interesting. Modern machines use UEFI by default, and UEFI PXE uses a different bootfile format (.efi). You need to detect which type the client is and serve accordingly.

# dnsmasq: detect architecture and serve the right bootfile
# Option 93 is the client architecture type
# 0 = x86 BIOS, 7 = x64 UEFI, 9 = x64 UEFI (alternate)
dhcp-match=set:efi-x86_64,option:client-arch,7
dhcp-match=set:efi-x86_64,option:client-arch,9
dhcp-match=set:bios,option:client-arch,0
# Serve UEFI binary to UEFI clients (before iPXE loads)
dhcp-boot=tag:efi-x86_64,tag:!ipxe,ipxe.efi
# Serve legacy binary to BIOS clients (before iPXE loads)
dhcp-boot=tag:bios,tag:!ipxe,undionly.kpxe
# Once iPXE is loaded (option 175 set), always serve the menu URL
dhcp-boot=tag:ipxe,http://192.168.1.50/menu.ipxe

Secure Boot is the elephant in the room. If Secure Boot is enabled and you’re not using a Microsoft-signed iPXE binary, it will refuse to load your .efi. Your options:

  1. Disable Secure Boot (the realistic home lab answer)
  2. Use the Secure Boot-compatible iPXE build (requires SHIM and a proper signing setup — overkill for most)
  3. Enable Secure Boot only on production machines, disable on lab gear

Most home lab nodes: disable Secure Boot in the BIOS/UEFI settings. Life is too short for SHIM signing in a home lab.


Diskless Talos OS: The Full Loop

Here’s what the full workflow looks like for running Talos OS nodes without local disks. This is where iPXE goes from “neat trick” to “genuinely useful infrastructure.”

Generate your Talos config:

Terminal window
# Generate cluster config
talosctl gen config sumguy-cluster https://192.168.1.100:6443 \
--output-dir ./talos-config
# Create a machineconfig that skips disk install for diskless nodes
talosctl gen config sumguy-cluster https://192.168.1.100:6443 \
--config-patch '[{"op":"add","path":"/machine/install","value":{"disk":"","wipe":false,"bootloader":false}}]' \
--output-dir ./talos-diskless

Serve the machineconfig from your HTTP server:

Terminal window
cp talos-diskless/worker.yaml pxe-root/talos/worker.yaml

Update your Talos iPXE entry to point at it:

:talos
kernel ${base}/talos/vmlinuz
initrd ${base}/talos/initramfs.xz
imgargs vmlinuz talos.platform=metal console=tty0 talos.config=${base}/talos/worker.yaml
boot || goto failed

Power on your diskless node. It boots the Talos kernel from the network, pulls its config from HTTP, and joins the cluster. No disk touched. Reload the machine and it PXE boots again clean. It’s a genuinely different operational model — nodes become cattle instead of pets in the most literal sense.


Should You Bother?

If you have more than two or three lab nodes, yes. The setup time is an afternoon — getting dnsmasq configured, building your TFTP + HTTP setup, writing the menu — but you never burn another USB stick.

The real value compounds over time. Every new machine you add just boots from the menu. Every OS reinstall is a power cycle and a menu selection. Talos OS diskless nodes become trivially easy to manage.

Do bother if:

Skip it if:

For most people who’ve been running a home lab longer than six months, this is squarely in the “should have done this sooner” category. The 2 AM USB hunt is optional.


The Bottom Line

iPXE is what PXE should have always been — HTTP-capable, scriptable, and not a nightmare to configure. The dnsmasq proxyDHCP setup means zero conflict with your existing DHCP server. The boot menu is a plain text file you can version control. And netboot.xyz gives you an escape hatch to every major OS without hosting anything yourself.

Pick your starting point: start with the netboot.xyz chainloader to verify the plumbing works, then add your own hosted images as you need them. You don’t have to host everything on day one. Get the DHCP/TFTP/HTTP triangle working first, confirm a successful boot, then build out from there.

Your USB drawer will miss you. Your 2 AM self won’t.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
iperf3 + nload: Network Diagnosis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts