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 bootsThe 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 subnetdhcp-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 scriptdhcp-boot=tag:ipxe,http://192.168.1.50/menu.ipxe
# TFTP server to serve the iPXE binary itselfenable-tftptftp-root=/var/lib/tftpboot
# Log DHCP activity so you can debuglog-dhcpReplace 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:
# Test your config firstdnsmasq --test --conf-file=/etc/dnsmasq.d/pxe.conf
# Run itsystemctl restart dnsmasqGrabbing 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:
# Create your TFTP rootmkdir -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 versioncurl -o /var/lib/tftpboot/ipxe.efi \ https://boot.ipxe.org/ipxe.efi
# Verify they downloaded correctlyls -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:
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-stoppedYour directory structure:
pxe-root/├── menu.ipxe├── ubuntu/│ ├── vmlinuz│ └── initrd├── debian/│ ├── linux│ └── initrd.gz├── talos/│ ├── vmlinuz│ └── initramfs.xz└── memtest/ └── memtest86+-7.20.isoGrab your OS kernels from the official netinstall images. For Ubuntu 24.04:
mkdir -p pxe-root/ubuntucd pxe-root/ubuntu
# Extract just the kernel and initrd from the netinstall ISOwget https://releases.ubuntu.com/24.04/ubuntu-24.04.2-live-server-amd64.isosudo mount -o loop ubuntu-24.04.2-live-server-amd64.iso /mntcp /mnt/casper/vmlinuz .cp /mnt/casper/initrd .sudo umount /mntFor Talos OS 1.9.x, they publish kernel/initramfs directly:
mkdir -p pxe-root/taloscd pxe-root/talos
TALOS_VERSION=v1.9.5wget https://github.com/siderolabs/talos/releases/download/${TALOS_VERSION}/vmlinuz-amd64 -O vmlinuzwget https://github.com/siderolabs/talos/releases/download/${TALOS_VERSION}/initramfs-amd64.xz -O initramfs.xzVerify your HTTP server is working before expecting PXE to:
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-LengthIf 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 URLset base http://192.168.1.50
:startmenu SumGuy Home Lab Boot Menuitem 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 Rebootchoose --timeout 30 --default ubuntu target && goto ${target}
:ubuntukernel ${base}/ubuntu/vmlinuzinitrd ${base}/ubuntu/initrdimgargs vmlinuz autoinstall quiet splash ---boot || goto failed
:debiankernel ${base}/debian/linuxinitrd ${base}/debian/initrd.gzimgargs linux auto=true priority=critical vga=788 ---boot || goto failed
:taloskernel ${base}/talos/vmlinuzinitrd ${base}/talos/initramfs.xzimgargs vmlinuz talos.platform=metal console=tty0 console=ttyS0,115200boot || goto failed
:memtest# Memtest boots as an ISO image via isofileset isofile ${base}/memtest/memtest86+-7.20.isokernel https://boot.ipxe.org/wimbootinitrd ${isofile} memtest.isoboot || goto failed
:gpartedkernel ${base}/gparted/vmlinuzinitrd ${base}/gparted/initrd.imgimgargs vmlinuz boot=live union=overlay username=user quiet splash vga=788 fetch=${base}/gparted/filesystem.squashfsboot || goto failed
:netbootxyzchain --autofree https://boot.netboot.xyzgoto start
:shellshell
:rebootreboot
:failedecho Boot failed. Press any key to return to menu.promptgoto startThe || 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:
#!ipxechain --autofree https://boot.netboot.xyzDone. 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:
docker run -d \ --name netbootxyz \ -p 3000:3000 \ -p 69:69/udp \ -p 8080:8080 \ ghcr.io/netbootxyz/netbootxyz:latestBuilding 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.
# Install build deps (Debian/Ubuntu)sudo apt install -y git build-essential liblzma-dev
# Clone iPXE sourcegit clone https://github.com/ipxe/ipxe.git /usr/src/ipxecd /usr/src/ipxe/src
# Create your embedded scriptcat > /tmp/embedded.ipxe << 'EOF'#!ipxedhcpchain http://192.168.1.50/menu.ipxeEOF
# Build legacy BIOS chainloader with embedded scriptmake bin/undionly.kpxe EMBED=/tmp/embedded.ipxe
# Build UEFI binary with embedded scriptmake bin-x86_64-efi/ipxe.efi EMBED=/tmp/embedded.ipxe
# Copy to TFTP rootcp 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:
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,7dhcp-match=set:efi-x86_64,option:client-arch,9dhcp-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 URLdhcp-boot=tag:ipxe,http://192.168.1.50/menu.ipxeSecure 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:
- Disable Secure Boot (the realistic home lab answer)
- Use the Secure Boot-compatible iPXE build (requires SHIM and a proper signing setup — overkill for most)
- 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:
# Generate cluster configtalosctl gen config sumguy-cluster https://192.168.1.100:6443 \ --output-dir ./talos-config
# Create a machineconfig that skips disk install for diskless nodestalosctl 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-disklessServe the machineconfig from your HTTP server:
cp talos-diskless/worker.yaml pxe-root/talos/worker.yamlUpdate your Talos iPXE entry to point at it:
:taloskernel ${base}/talos/vmlinuzinitrd ${base}/talos/initramfs.xzimgargs vmlinuz talos.platform=metal console=tty0 talos.config=${base}/talos/worker.yamlboot || goto failedPower 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:
- You have 3+ lab nodes
- You’re running or planning Talos/k3s clusters
- Your nodes are in a location where walking over is annoying
- You ever find yourself burning ISOs more than once a month
Skip it if:
- You have one or two machines you almost never reinstall
- Your router doesn’t support proxyDHCP passthrough (some consumer gear blocks this)
- Everything you run is a VM with a hypervisor that already handles install media
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.