bty via netboot (bty-web container deploy)

The network-flash flow needs a long-running bty-web: it serves the browser UI, the per-MAC PXE plans, the iPXE bootfiles, and the image bytes. The operator’s existing LAN DHCP server points PXE clients at this host; targets PXE-boot into the server’s catalog, flash themselves, reboot.

Run bty-web as a container. The deploy ships three pieces:

  1. bty-web (ghcr.io/safl/bty-web) – the policy / PXE layer: UI, PXE plans, boot artifacts, and images over HTTP on :8080.

  2. withcache (ghcr.io/safl/withcache) – a URL-keyed artifact cache that holds the image bytes, so a fleet pulls each image once.

  3. tftp (ghcr.io/safl/bty-tftp, optional) – a small TFTP sidecar that serves the ~1 MB iPXE bootfile for legacy BIOS / older UEFI clients that bootstrap over TFTP. It serves only the bootfile; the kernel / initrd / squashfs come from bty-web over HTTP.

The operator UI is gated by $BTY_ADMIN_PASSWORD (unset = open, with a startup warning). End state after up: a browser URL ready to register machines and serve images.

Set it up

sudo uvx bty-lab deploy /opt/bty
#   bty:       http://<host>:8080/ui     (login: bty-lab / bty-lab)
#   withcache: http://<host>:3000/       (login: bty-lab / bty-lab)

That’s the full bring-up. The remainder of this walkthrough – DHCP wiring, the PXE flash flow, known limitations – is what you do once the server is running. For the deploy mechanics (bind-mount layout, --systemd Quadlet install, upgrade semantics, password rotation), see deploy/README.md and walkthrough-server-docker.md.

Configure DHCP

bty runs no DHCP role: a working LAN DHCP server is a hard prerequisite, and its config carries the PXE / HTTP-Boot pointers (option 60 / 66 / 67) that direct clients at this host. The exact values for your deploy are on the bty-web Settings page under DHCP / Network boot.

PXE (TFTP)

UEFI HTTP Boot

Vendor class (option 60)

PXEClient

HTTPClient

Next-server (option 66)

host IP

host IP (still required)

Bootfile (option 67)

ipxe.efi

http://<host>:8080/boot/ipxe.efi

Option 66 stays pointed at the host for HTTP Boot too: bty’s iPXE binary chains on to http://<host>:8080/pxe-bootstrap.ipxe, so it needs the next-server even though the bootfile is a full URL. Once iPXE is running, both paths fetch /pxe-bootstrap.ipxe, then the per-MAC plan.

Flash a target over PXE

Once a target’s MAC is registered with an assigned image, set the target’s BIOS / UEFI to boot from the network (PXE) first. bty then drives every subsequent boot via boot_mode; you set the firmware order once. Mind the post-flash boot: with boot_mode=ipxe-exit (the default) bty boots the disk via iPXE – UEFI exits to the firmware boot order, legacy BIOS sanboots the drive – and the bty-flash-* modes boot the just-flashed disk the same way. On legacy BIOS the drive is 0x80 (first disk) unless you set sanboot_drive; on UEFI there’s nothing to set. (A flashed box that won’t boot is almost always a firmware / drive-number problem; see Firmware boot order.) On power-on it will:

  1. DHCP-discover from your LAN’s DHCP server, configured to return option 66/67 pointing at the bty host.

  2. Chain into the bty iPXE script. Cmdline carries bty.server=URL + bty.mac=MAC only.

  3. Boot the netboot kernel + initrd + squashfs trio. bty-on-tty1.service exec’s bty --server X --mac Y on tty1; bty GETs <server>/pxe/<mac>/plan, sees mode=flash (because boot_mode=bty-flash-always + ref + serial), writes the image to the local disk, POSTs /pxe/<mac>/done, reboots.

The server’s machine-detail page shows live progress + last flashed timestamp. What happens on subsequent boots depends on the boot_mode you picked:

  • bty-flash-once (the default for one-shot CI reflashes): the next boot still hits bty’s iPXE chain, sees the bound flash has already happened, and serves ipxe-exit so the firmware falls through to the freshly-flashed disk.

  • bty-flash-always (per-job CI cadence): the iPXE chain alternates flash/sanboot per /pxe GET. The PXE-first firmware order stays in effect; the alternation gives a freshly-flashed boot in between every flash so the operator’s job runs on the intended image and the next job starts from a clean reflash.

Either way, the freshly-flashed image runs whatever it provisions to.

What you can do today

  • PXE-flash any number of targets to a registered image, hands-free, in parallel.

  • Mix the network-flash flow (this walkthrough) with the USB-stick flow (bty via bty-usb): both run the same bty flash code, driven by the plan endpoint vs the local wizard.

  • Swap images per-target without rebooting the server.

Known limitations

  • DHCP stays with the operator’s LAN. bty runs no DHCP server (proxy or full); a working LAN DHCP server is a hard prerequisite, and its config must be extended with option 60 / 66 / 67 to direct PXE clients at the bty host.

  • UEFI Secure Boot isn’t supported: the bty netboot kernel isn’t shim-signed. Disable Secure Boot on targets you’re PXE-flashing, or use the USB stick flow.

  • Single bootfile, no DHCP userclass logic (UEFI). Stock iPXE re-DHCPs after it loads and re-fetches the DHCP bootfile – itself – unless the DHCP server hands iPXE a different bootfile by matching user-class=iPXE. bty sidesteps that: its custom ipxe.efi (baked into the bty-web and bty-tftp images) embeds chain http://${next-server}:8080/pxe-bootstrap.ipxe, so the operator’s DHCP only ever needs one bootfile and no userclass rules. The legacy-BIOS undionly.kpxe is stock, so BIOS still needs the userclass trick (UniFi / Kea client-classes, dnsmasq dhcp-userclass, ISC-DHCPd conditional if) – and BIOS is unverified anyway (below).

  • UEFI tested, legacy BIOS not yet. bty’s netboot path has so far been exercised only on UEFI targets. The legacy-BIOS branch (sanboot --drive instead of the UEFI hand-back to firmware) is implemented but not field-tested; treat BIOS as unverified for now.