We’ve all been there: it’s 11:45 PM, your rack is humming with half a dozen fresh white-box servers or repurposed optiplexes, and you are elbow-deep in a graveyard of dusty USB flash drives. One of them is missing a valid partition table, another has a corrupted ISO from a botched write operation, and your KVM over IP switch refuses to pass through the virtual media properly. You realize you are spending more time walking between machines with a piece of silicon memory strapped to your keyring than actually building your cluster. There has to be a cleaner way to provision iron without touching physical media.
Deploying a self-hosted netboot.xyz infrastructure using Docker and dnsmasq eliminates physical media entirely. By configuring DHCP options to point bare-metal firmware straight to an iPXE chainloader, you can stream installers, rescue environments, and unattended OS configurations across your entire rack over the network in minutes.
How Does the Network Boot Sequence Actually Work Under the Hood?
Grab a fresh cup of coffee, and let’s demystify what happens between the moment you press the power button on a bare-metal node and the appearance of an interactive installer menu. Understanding this lifecycle is the single best way to troubleshoot why a machine is hanging at a blinking cursor or dropping a mysterious timeout error.
When a server initializes, its motherboard firmware (whether legacy BIOS or modern UEFI) steps through its boot order. When network boot (PXE) is prioritized, the system’s network interface card (NIC) initializes its Option ROM, brings up the onboard link, and broadcasts a DHCP Discover packet across your local broadcast domain. This packet contains specific vendor class identifiers indicating it is looking for a network bootloader.
Your network’s DHCP server responds with an Offer, but for a PXE boot to succeed, that offer must include two vital parameters known as DHCP options:
- DHCP Option 66 (Next Server / TFTP Server): Tells the client machine the IP address of the server hosting the initial bootstrap program.
- DHCP Option 67 (Bootfile Name): Tells the client the exact filename of the binary it needs to download and execute via TFTP or HTTP (such as
undionly.kpxefor legacy BIOS oripxe.efifor UEFI).
Once the client fetches that small bootloader file, execution jumps straight into iPXE. Unlike the constrained, primitive environment of standard PXE, iPXE is a fully-featured open-source network boot firmware that supports scripting, VLAN configuration, HTTPS, and most importantly, HTTP downloading. Instead of dragging massive kernel images over the excruciatingly slow Trivial File Transfer Protocol, iPXE immediately pivots to HTTP. It pulls down the netboot.xyz menu system, presenting you with a gorgeous, comprehensive list of operating systems, rescue tools, and utility disks directly on your monitor.
# Bare-Metal Network Boot Packet Flow Sequence
[Bare-Metal Node] [DHCP / dnsmasq] [TFTP / HTTP Server]
| | |
|--- 1. DHCP Discover (PXE Request) -->| |
|<-- 2. DHCP Offer + Opt 66/67 --------| |
| | |
|--- 3. TFTP Read Request (ipxe.efi) ----------------------------------->|
|<-- 4. Transfer iPXE Binary --------------------------------------------|
| | |
| (Execution jumps to iPXE runtime) | |
|--- 5. HTTP GET (netboot.xyz.efi) ------------------------------------->|
|<-- 6. Render Interactive Menu System ----------------------------------|
Why Do Most Homelab PXE Deployments Fail on Day One?
The internet is littered with tutorials written a decade ago that instruct you to install a monolithic TFTP daemon, compile ISC DHCP server from source, manually unpack kernel initrds into nested directory structures, and pray to the gods of file permissions. This approach is brittle, painful to maintain, and breaks the second an upstream distribution changes its kernel naming convention.
Another classic trap is relying entirely on the public netboot.xyz upstream endpoints without a local mirror. While convenient for an occasional machine, pulling multi-gigabyte installer ISOs and kernels over your home internet connection every time you provision a node will saturate your bandwidth and leave you dead in the water if your ISP drops out or upstream rate-limits your requests.
Let's look at how modern systems engineering approaches this problem by comparing traditional monolithic TFTP setups with a modern containerized iPXE and local mirror architecture:
| Architecture Dimension | Legacy TFTP / Monolithic PXE | Modern Docker + iPXE + Local Mirror |
|---|---|---|
| Deployment Complexity | High (Manual package management, root file permissions) | Low (Containerized stack via Docker Compose) |
| Transfer Protocol | TFTP (Unencrypted, single-block, excruciatingly slow) | HTTP / HTTPS (Multi-stream, high throughput) |
| WAN Dependency | Varies, but manual kernel updates are required | Zero (Self-contained local web server mirrors assets) |
| UEFI / BIOS Coexistence | Painful architecture-specific DHCP routing rules | Seamless runtime architecture detection via iPXE |
| Automation & Unattended Installs | Complex kernel append-line overrides per distro | Clean custom JSON/iPXE menu templating |
How Do You Build a Self-Contained Local Netboot Environment Using Docker?
To achieve true infrastructure independence, we are going to spin up a dedicated provisioning host on your network (this could be a low-power mini-PC, a Raspberry Pi 4, or an existing home server running Ubuntu or Debian). This host will run a Docker Compose stack containing a lightweight DNS/DHCP server with integrated TFTP capabilities (dnsmasq) alongside a local netboot.xyz web asset server.
Create a directory on your provisioning host at /opt/netboot-stack and populate your configuration files. First, let's write out a robust docker-compose.yml file that handles the web frontend and TFTP asset delivery:
version: '3.8'
services:
netboot-xyz:
image: linuxserver/netboot.xyz
container_name: netboot-xyz
environment:
- PUID=1000
- PGID=1000
- TZ=UTC
- MENU_VERSION=latest
volumes:
- /opt/netboot-stack/config:/config
ports:
- "80:80"
- "69:69/udp"
restart: unless-stopped
Next, we need to handle the network routing side of things. If your main gateway router (like a firewalled pfSense or OPNsense box) allows you to customize DHCP Option 66 and 67, you can point them directly at the IP address of your Docker host and the appropriate bootfile name. However, if you want your provisioning host to act as a self-contained PXE island—ideal for isolated lab VLANs—you can configure dnsmasq to serve both DHCP and PXE routing.
Create a configuration file at /opt/netboot-stack/config/dnsmasq.conf with the following production-grade rules:
# Disable DNS features, focus purely on DHCP and TFTP port=0 # Interface to listen on (adjust to your lab network interface) interface=eth0 bind-interfaces # DHCP IP Range allocation for booting nodes dhcp-range=192.168.10.100,192.168.10.200,12h # Enable built-in TFTP server provided by netboot container volume enable-tftp tftp-root=/config/tftp # Architecture-aware PXE boot file assignments dhcp-match=set:bios,option:client-arch,0 dhcp-match=set:efi32,option:client-arch,6 dhcp-match=set:efiboot,option:client-arch,7 dhcp-match=set:efi64,option:client-arch,9 # Point clients to respective iPXE chainloaders dhcp-boot=tag:bios,undionly.kpxe,192.168.10.50 dhcp-boot=tag:efiboot,ipxe.efi,192.168.10.50 dhcp-boot=tag:efi64,ipxe.efi,192.168.10.50 # Log DHCP and PXE traffic for forensic debugging log-dhcp
Replace 192.168.10.50 with the actual static IP address of your Docker provisioning host. Once these files are in place, spin up the container stack by running docker compose up -d in your terminal. The LinuxServer container will automatically pull down the latest stable release of the netboot.xyz binaries, generate the TFTP assets, and stage the web root.
How Do You Configure Automated Unattended Linux Installs via iPXE Menus?
Interactive menus are fantastic when you are experimenting, but true homelab mastery involves unattended, zero-touch provisioning. When you drop a brand-new bare-metal node onto your rack, you want it to boot over the network, pull down Ubuntu Server or Debian, execute a cloud-init or autoinstall script, partition the disks, configure your SSH keys, and drop you into a fully configured operating system without a single keystroke.
Netboot.xyz makes this possible through custom menu overrides. Inside your configuration volume at /opt/netboot-stack/config/custom.ipxe, you can write custom iPXE scripts that bypass the default interactive picker and stream your preferred distro straight into memory with automated parameters.
Here is a production-ready custom iPXE menu script for automated Ubuntu 24.04 LTS installation:
#!ipxe
# Custom Automated Ubuntu 24.04 LTS Autoinstall Entry
set ubuntu_version noble
set mirror_url http://192.168.10.50/mirrors/ubuntu
item --gap -- ---------------------------------------
item --gap Automated Bare-Metal Provisioning
item --gap -- ---------------------------------------
item ubuntu_auto Provision Ubuntu 24.04 LTS (Unattended)
item shell Drop to iPXE Shell
choose target || goto cancel
goto ${target}
:ubuntu_auto
echo Booting Automated Ubuntu ${ubuntu_version} Kernel...
kernel ${mirror_url}/casper/vmlinuz autoinstall ds=nocloud-net;s=http://192.168.10.50/config/cloud-init/
initrd ${mirror_url}/casper/initrd
boot
goto autodefault
:shell
echo Type 'exit' to return to menu.
shell
goto autodefault
:cancel
echo Boot cancelled. Rebooting in 5 seconds...
sleep 5
reboot
To complement this iPXE script, host a minimal user-data cloud-init configuration file inside /opt/netboot-stack/config/cloud-init/user-data. This tells the Ubuntu installer how to handle storage, networking, and user creation autonomously:
#cloud-config
autoinstall:
version: 1
identity:
hostname: baremetal-node-01
password: "$6$rounds=4096$saltstring$encryptedpasswordhashhere"
username: sysadmin
ssh:
install-server: yes
allow-pw: yes
disk-setup:
layout:
name: direct
storage:
layout:
name: lvm
With this pipeline active, your provisioning workflow shifts from a tedious manual chore into an elegant, repeatable infrastructure-as-code routine. You plug in a node, verify the link lights, power it on, and watch the serial console or monitor stream the installation packages directly from your local container mirror.
Frequently Asked Questions
Can I use netboot.xyz if my homelab switches do not support DHCP Option forwarding?
Yes. If your network switches prevent cross-VLAN DHCP helper (IP helper) configurations, you can run dnsmasq or an equivalent DHCP/PXE helper directly on the target subnet, or use iPXE USB burning tools to boot the initial iPXE payload from a tiny thumb drive. Once iPXE boots locally, it will successfully pull your netboot.xyz menus over HTTP across VLAN boundaries.
How do I handle UEFI Secure Boot validation during network boots?
Secure Boot enforces cryptographic signature verification on every stage of the bootloader. Because standard community iPXE binaries are not signed by Microsoft's UEFI CA certificate, you must either disable Secure Boot in your bare-metal server BIOS settings during provisioning or compile a custom iPXE binary signed with your own enrolled keys.
Why is my client node hanging indefinitely after receiving the IP address via DHCP?
This is almost always caused by a firewall rule blocking UDP port 69 (TFTP) or an incorrect filename specified in DHCP Option 67. Verify that your container port mappings are correct, check your firewall logs for dropped UDP packets, and ensure your client firmware matches the bootloader architecture (e.g., serving ipxe.efi for UEFI nodes and undionly.kpxe for legacy BIOS nodes).
Automating compute provisioning across modern clusters requires intelligent orchestration and fast node bootstrap. For automated bare-metal PXE installation workflows, see our analysis on Karpenter vs Cluster Autoscaler: Kubernetes FinOps.
