docs(runbook): apply OpenPXE rebrand to network-boot runbook

The Linux network-boot runbook landed on origin/main while the v0.3.0
rebrand was in flight on local main. Runs the same string-rewrite
pass: PXEForge → OpenPXE, Gated → Queued, /api/gate → /api/queue,
PXEFORGE_ env vars → OPENPXE_, container path under
/var/lib/openpxe.
This commit is contained in:
Miles Ward
2026-05-06 14:14:09 -04:00
parent e3452fe976
commit 90a23a8c96
+41 -41
View File
@@ -1,13 +1,13 @@
# Runbook: Boot a Linux machine from an ISO over the network # Runbook: Boot a Linux machine from an ISO over the network
End-to-end walkthrough: spin up PXEForge, load an Ubuntu (or any End-to-end walkthrough: spin up OpenPXE, load an Ubuntu (or any
Linux) ISO into it, target a specific bare-metal or VM client by its Linux) ISO into it, target a specific bare-metal or VM client by its
MAC address, and have that machine PXE-boot the installer over the MAC address, and have that machine PXE-boot the installer over the
LAN — no USB stick, no console babysitting. LAN — no USB stick, no console babysitting.
This runbook assumes: This runbook assumes:
- You have **one Linux host** to run the PXEForge container (any - You have **one Linux host** to run the OpenPXE container (any
distro with Docker / Podman; 2 GB RAM, ~50 GB disk for the ISO distro with Docker / Podman; 2 GB RAM, ~50 GB disk for the ISO
library). library).
- That host sits on the **same broadcast domain / VLAN** as the - That host sits on the **same broadcast domain / VLAN** as the
@@ -15,7 +15,7 @@ This runbook assumes:
networks need a DHCP relay and are out of scope here. networks need a DHCP relay and are out of scope here.
- An **existing DHCP server** is already handing out IP leases on - An **existing DHCP server** is already handing out IP leases on
that VLAN (your home router, OPNsense, Windows Server, etc.). that VLAN (your home router, OPNsense, Windows Server, etc.).
PXEForge runs as a *DHCP proxy* — it never leases IPs, it only OpenPXE runs as a *DHCP proxy* — it never leases IPs, it only
layers the boot information on top of the existing DHCP exchange. layers the boot information on top of the existing DHCP exchange.
- The target client is configured to **PXE-boot** in BIOS/UEFI - The target client is configured to **PXE-boot** in BIOS/UEFI
firmware (usually `F12` boot menu → Network, or set as first boot firmware (usually `F12` boot menu → Network, or set as first boot
@@ -28,7 +28,7 @@ or [docs/architecture.md](../docs/architecture.md) first.
## 0. Pick your hosts LAN IP ## 0. Pick your hosts LAN IP
You need the IPv4 address PXEForge will advertise to clients. From You need the IPv4 address OpenPXE will advertise to clients. From
the host: the host:
```bash ```bash
@@ -40,33 +40,33 @@ example `10.0.0.5/24` on `eno1`. From here on we call it
`PXE_HOST_IP`. `PXE_HOST_IP`.
> **Why this matters.** Every URL handed to clients (TFTP server, > **Why this matters.** Every URL handed to clients (TFTP server,
> iPXE chain URL, ISO URL) is built from this IP. If PXEForge > iPXE chain URL, ISO URL) is built from this IP. If OpenPXE
> auto-detects the wrong interface or loopback, clients will fetch > auto-detects the wrong interface or loopback, clients will fetch
> from an unreachable address and silently fail. The startup will > from an unreachable address and silently fail. The startup will
> *fail loudly* if it can only auto-detect a loopback address. > *fail loudly* if it can only auto-detect a loopback address.
--- ---
## 1. Run PXEForge ## 1. Run OpenPXE
The MVP path is a single `docker run` against the published image, The MVP path is a single `docker run` against the published image,
with `--network host` so the container can see DHCP broadcasts on with `--network host` so the container can see DHCP broadcasts on
the LAN. the LAN.
```bash ```bash
mkdir -p ~/pxeforge/isos ~/pxeforge/work mkdir -p ~/openpxe/isos ~/openpxe/work
docker run -d --name pxeforge \ docker run -d --name openpxe \
--restart unless-stopped \ --restart unless-stopped \
--network host \ --network host \
-e PXEFORGE_PUBLIC_IP=10.0.0.5 \ -e OPENPXE_PUBLIC_IP=10.0.0.5 \
-e PXEFORGE_DHCP_MODE=proxy \ -e OPENPXE_DHCP_MODE=proxy \
-v ~/pxeforge/isos:/var/lib/pxeforge/isos \ -v ~/openpxe/isos:/var/lib/openpxe/isos \
-v ~/pxeforge/work:/var/lib/pxeforge/work \ -v ~/openpxe/work:/var/lib/openpxe/work \
ghcr.io/YOUR-ORG/pxeforge:0.2.0 ghcr.io/YOUR-ORG/openpxe:0.2.0
``` ```
Substitute your `PXEFORGE_PUBLIC_IP`, of course. If youre building Substitute your `OPENPXE_PUBLIC_IP`, of course. If youre building
from this repo instead of pulling, see the from this repo instead of pulling, see the
[README quick start](../README.md#quick-start--mvp-container-recommended). [README quick start](../README.md#quick-start--mvp-container-recommended).
@@ -84,7 +84,7 @@ otherwise. See [README — Container health probes](../README.md#container-healt
### Check the listening ports ### Check the listening ports
PXEForge holds three privileged UDP/TCP ports. From another shell on OpenPXE holds three privileged UDP/TCP ports. From another shell on
the host: the host:
```bash ```bash
@@ -93,7 +93,7 @@ sudo ss -lntp | grep ':80\b' # HTTP UI / boot scripts
``` ```
All four should be present. If port 67 is taken by `dnsmasq` or the All four should be present. If port 67 is taken by `dnsmasq` or the
hosts own DHCP, stop that service or run PXEForge on a separate box — hosts own DHCP, stop that service or run OpenPXE on a separate box —
two listeners on `:67` will fight. two listeners on `:67` will fight.
--- ---
@@ -114,7 +114,7 @@ Two options. Pick one.
- File size and SHA-256 - File size and SHA-256
Big ISOs stream — there is no 2 GB limit, but expect upload to be Big ISOs stream — there is no 2 GB limit, but expect upload to be
gated by your browser ↔ host link. The UI shows a progress bar; the throttled by your browser ↔ host link. The UI shows a progress bar; the
animated anvil on the Dashboard tab fires up while imaging is in animated anvil on the Dashboard tab fires up while imaging is in
flight. flight.
@@ -124,17 +124,17 @@ If you already have a folder of ISOs on the host, skip the browser:
```bash ```bash
# Dry run first — see what would be imported, no writes: # Dry run first — see what would be imported, no writes:
docker exec pxeforge pxeforge seed \ docker exec openpxe openpxe seed \
--from /seed \ --from /seed \
--dry-run --dry-run
# For real, mount the source dir read-only into the container: # For real, mount the source dir read-only into the container:
docker run --rm \ docker run --rm \
-v /my/iso-library:/seed:ro \ -v /my/iso-library:/seed:ro \
-v ~/pxeforge/isos:/var/lib/pxeforge/isos \ -v ~/openpxe/isos:/var/lib/openpxe/isos \
-v ~/pxeforge/work:/var/lib/pxeforge/work \ -v ~/openpxe/work:/var/lib/openpxe/work \
-e PXEFORGE_PUBLIC_IP=10.0.0.5 \ -e OPENPXE_PUBLIC_IP=10.0.0.5 \
ghcr.io/YOUR-ORG/pxeforge:0.2.0 seed --from /seed ghcr.io/YOUR-ORG/openpxe:0.2.0 seed --from /seed
``` ```
Each `*.iso` in `/seed` runs through the same upload pipeline as the Each `*.iso` in `/seed` runs through the same upload pipeline as the
@@ -182,13 +182,13 @@ Most BIOS/UEFI screens display the NIC MAC during the network-boot
attempt — usually as `MAC: AA-BB-CC-DD-EE-FF` flashing on the splash attempt — usually as `MAC: AA-BB-CC-DD-EE-FF` flashing on the splash
right before "PXE-E53: No boot filename received". Write it down. right before "PXE-E53: No boot filename received". Write it down.
### 3c. By letting it boot once and watching PXEForge ### 3c. By letting it boot once and watching OpenPXE
Easiest if the box is in front of you: Easiest if the box is in front of you:
1. Power on, hit `F12`, pick **Network boot**. 1. Power on, hit `F12`, pick **Network boot**.
2. Without any binding configured, the client will land on the 2. Without any binding configured, the client will land on the
PXEForge menu (Default / Installers / Tools / Gated Deployment). OpenPXE menu (Default / Installers / Tools / Queued Deployment).
3. Dont pick anything. On your laptop: 3. Dont pick anything. On your laptop:
```bash ```bash
curl -fsS http://10.0.0.5/api/clients | jq . curl -fsS http://10.0.0.5/api/clients | jq .
@@ -196,7 +196,7 @@ Easiest if the box is in front of you:
4. The most-recent entry is your target. Copy its `mac`. 4. The most-recent entry is your target. Copy its `mac`.
From here on we call this MAC `TARGET_MAC` (e.g. `aa:bb:cc:dd:ee:ff`). From here on we call this MAC `TARGET_MAC` (e.g. `aa:bb:cc:dd:ee:ff`).
Hyphens vs colons, upper vs lower case — PXEForge normalizes both. Hyphens vs colons, upper vs lower case — OpenPXE normalizes both.
--- ---
@@ -227,7 +227,7 @@ curl -fsS -X POST http://10.0.0.5/api/hosts \
}' | jq . }' | jq .
``` ```
The binding is persisted to `~/pxeforge/work/hosts.json` and survives The binding is persisted to `~/openpxe/work/hosts.json` and survives
container restart. container restart.
### Confirm ### Confirm
@@ -262,7 +262,7 @@ In order, with timing:
| iPXE banner | ~1 s | The blue iPXE splash, version string | | iPXE banner | ~1 s | The blue iPXE splash, version string |
| iPXE second-stage DHCP | ~1 s | `Configuring (net0 …)` then `ok` | | iPXE second-stage DHCP | ~1 s | `Configuring (net0 …)` then `ok` |
| HTTP boot script fetch | <1 s | `http://10.0.0.5/boot.ipxe?mac=…` | | HTTP boot script fetch | <1 s | `http://10.0.0.5/boot.ipxe?mac=…` |
| Per-MAC chain | <1 s | `PXEForge: per-MAC binding -> ubuntu-24-04-1-…` | | Per-MAC chain | <1 s | `OpenPXE: per-MAC binding -> ubuntu-24-04-1-…` |
| Kernel + initrd HTTP | 530 s | Two 200-OK fetches against `/iso/<id>/casper/vmlinuz` and `…/initrd` | | Kernel + initrd HTTP | 530 s | Two 200-OK fetches against `/iso/<id>/casper/vmlinuz` and `…/initrd` |
| Kernel boot | 510 s | Kernel banner, then the Ubuntu/cloud-init splash | | Kernel boot | 510 s | Kernel banner, then the Ubuntu/cloud-init splash |
| Installer comes up | 3060 s | The distros normal Live/installer environment | | Installer comes up | 3060 s | The distros normal Live/installer environment |
@@ -281,12 +281,12 @@ curl -N http://10.0.0.5/api/log/stream
Youll see each protocol step as it happens: Youll see each protocol step as it happens:
``` ```
INFO pxeforge::dhcp: reply mac=aa:bb:cc:dd:ee:ff arch=X8664Uefi target=tftp/snponly.efi INFO openpxe::dhcp: reply mac=aa:bb:cc:dd:ee:ff arch=X8664Uefi target=tftp/snponly.efi
INFO pxeforge::tftp: RRQ snponly.efi blksize=1468 windowsize=8 → 982 KiB in 412 ms INFO openpxe::tftp: RRQ snponly.efi blksize=1468 windowsize=8 → 982 KiB in 412 ms
INFO pxeforge::dhcp: reply mac=aa:bb:cc:dd:ee:ff (iPXE) target=http/boot.ipxe INFO openpxe::dhcp: reply mac=aa:bb:cc:dd:ee:ff (iPXE) target=http/boot.ipxe
INFO pxeforge::http: GET /boot.ipxe?mac=aa:bb:cc:dd:ee:ff → host binding hit INFO openpxe::http: GET /boot.ipxe?mac=aa:bb:cc:dd:ee:ff → host binding hit
INFO pxeforge::http: GET /iso/ubuntu-…/casper/vmlinuz Range=bytes=0- 200 OK 14 MiB INFO openpxe::http: GET /iso/ubuntu-…/casper/vmlinuz Range=bytes=0- 200 OK 14 MiB
INFO pxeforge::http: GET /iso/ubuntu-…/casper/initrd Range=bytes=0- 200 OK 75 MiB INFO openpxe::http: GET /iso/ubuntu-…/casper/initrd Range=bytes=0- 200 OK 75 MiB
``` ```
The **Terminal** tab in the web UI shows the same thing live, plus a The **Terminal** tab in the web UI shows the same thing live, plus a
@@ -300,14 +300,14 @@ boots from your LAN, but the underlying ISO can come from anywhere
your *host* can reach: your *host* can reach:
- **Direct upload** from a remote workstation via the web UI (HTTPS - **Direct upload** from a remote workstation via the web UI (HTTPS
reverse-proxied if you put PXEForge behind nginx/Caddy). reverse-proxied if you put OpenPXE behind nginx/Caddy).
- **NFS mount** of a remote share — Sidebar → **Storage** → **NFS** → - **NFS mount** of a remote share — Sidebar → **Storage** → **NFS** →
`nfs://files.lab.example.com/exports/isos`. Mounted ISOs show up in `nfs://files.lab.example.com/exports/isos`. Mounted ISOs show up in
the same list and are PXE-bootable directly without copying. the same list and are PXE-bootable directly without copying.
- **Pre-seed** from a CI job that `curl`s a vendor mirror and runs - **Pre-seed** from a CI job that `curl`s a vendor mirror and runs
`pxeforge seed --from`. `openpxe seed --from`.
PXEForge itself never reaches out to the internet at boot time — all OpenPXE itself never reaches out to the internet at boot time — all
client traffic stays on the LAN, served from the host. client traffic stays on the LAN, served from the host.
--- ---
@@ -337,21 +337,21 @@ curl -fsS -X POST http://10.0.0.5/api/hosts \
-d '{ "mac": "aa:bb:cc:dd:ee:ff", "target": "_local", "label": "lab-rack3-node07 (installed)" }' -d '{ "mac": "aa:bb:cc:dd:ee:ff", "target": "_local", "label": "lab-rack3-node07 (installed)" }'
``` ```
Now if anyone hits `F12 → Network` by accident, PXEForge replies Now if anyone hits `F12 → Network` by accident, OpenPXE replies
with a script that says *"chain back to local HDD"* and the box with a script that says *"chain back to local HDD"* and the box
boots its real OS instead of re-imaging itself. This is the safest boots its real OS instead of re-imaging itself. This is the safest
default for production hardware. default for production hardware.
--- ---
## 7. Re-imaging — the “Gated Deployment” flow ## 7. Re-imaging — the “Queued Deployment” flow
Different scenario: you have **a rack of 30 servers** to image Different scenario: you have **a rack of 30 servers** to image
identically, all at once. Dont bind 30 MACs by hand. Use the gate. identically, all at once. Dont bind 30 MACs by hand. Use the gate.
1. **Dont** create host bindings. 1. **Dont** create host bindings.
2. PXE-boot every machine. They land on the menu. 2. PXE-boot every machine. They land on the menu.
3. On each: select **Gated Deployment**. They get position #1, #2, 3. On each: select **Queued Deployment**. They get position #1, #2,
…, #30 and start long-polling. …, #30 and start long-polling.
4. In the UI: **Forge Gate** tab shows all 30 lined up. Pick the 4. In the UI: **Forge Gate** tab shows all 30 lined up. Pick the
ISO, click **Assign to all waiting**. ISO, click **Assign to all waiting**.
@@ -376,7 +376,7 @@ still in the kernel-fetch phase.
| Release binding | `DELETE /api/hosts/<mac>` | | Release binding | `DELETE /api/hosts/<mac>` |
| Live log | `curl -N http://$IP/api/log/stream` | | Live log | `curl -N http://$IP/api/log/stream` |
| Prometheus metrics | `curl http://$IP/metrics` | | Prometheus metrics | `curl http://$IP/metrics` |
| Bulk import folder | `pxeforge seed --from /path` | | Bulk import folder | `openpxe seed --from /path` |
--- ---
@@ -387,7 +387,7 @@ still in the kernel-fetch phase.
`--network host`, check the host firewall on UDP 67/69/4011. `--network host`, check the host firewall on UDP 67/69/4011.
- **iPXE shows `No more network devices`** — firmware NIC isnt in - **iPXE shows `No more network devices`** — firmware NIC isnt in
PXE mode, or VLAN tagging is wrong. PXE mode, or VLAN tagging is wrong.
- **iPXE prints `Connection timed out (http://…)`** — `PXEFORGE_PUBLIC_IP` - **iPXE prints `Connection timed out (http://…)`** — `OPENPXE_PUBLIC_IP`
is wrong. Clients cant reach that IP. Check `/api/status` → is wrong. Clients cant reach that IP. Check `/api/status` →
`public_base_url` and `ping` it from the client subnet. `public_base_url` and `ping` it from the client subnet.
- **Kernel panics during initrd load** — corrupt ISO upload. Check - **Kernel panics during initrd load** — corrupt ISO upload. Check