# syntax=docker/dockerfile:1.7 # # OpenPXE — multi-stage build. # # Design: # - stage `fetch`: runs scripts/fetch-ipxe.sh to pull official iPXE binaries # into assets/ipxe/ so the rust build can embed them via rust-embed. # - stage `build`: compiles the workspace with cargo in release mode. # - stage `runtime`: Debian slim image with setcap for NET_BIND_SERVICE, # running as a non-root UID. No shell in PATH for the service user; # attacker surface is just the openpxe binary + libc. # # Why not distroless? We want setcap support and easy debug (`oc rsh`). # Debian slim at ~75 MB + binary ~25 MB is fine for a PXE server that # spends most of its life idle. ARG RUST_VERSION=1.95 ########## fetch iPXE binaries + wimboot ########## # Pulls the upstream boot.ipxe.org pre-builds (no PNG support) plus # wimboot. These cover the arches we don't build from source here: # BIOS undionly.kpxe and i386-efi (which need a 32-bit x86 toolchain), # and serve as the baseline that the PNG-enabled x86_64/arm64 UEFI # binaries from the `ipxe-build` stage overlay on top of. FROM debian:12-slim AS fetch RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /src COPY scripts/fetch-ipxe.sh scripts/fetch-ipxe.sh RUN mkdir -p assets/ipxe && bash scripts/fetch-ipxe.sh ########## build PNG-enabled iPXE from source ########## # v0.4.69: THE graphical-boot-menu unlock. iVentoy paints a PNG # background on the PXE screen using stock iPXE built with # CONSOLE_FRAMEBUFFER + IMAGE_PNG + CONSOLE_CMD; the public iPXE # binaries omit those, so `console --picture` is a no-op on them. # We build our own from upstream with that thin config delta. # # The historical blocker was cc1 segfaulting when an amd64 gcc ran # under QEMU emulation on an arm64 host. The fix: pin this stage to # $BUILDPLATFORM (the NATIVE builder arch — arm64 on an Apple-Silicon # Mac, amd64 in x86 CI) and cross-compile with a real cross toolchain # (CROSS_COMPILE=x86_64-linux-gnu-). The compiler runs native and # emits x86_64 — no emulation, no segfault. arm64-efi builds natively. FROM --platform=$BUILDPLATFORM debian:12-slim AS ipxe-build # libc6-dev is REQUIRED and easy to miss under --no-install-recommends: # iPXE's host utilities (elf2efi, zbin) compile with the native gcc and # pull ; without the native libc headers gcc's #include_next # falls through to iPXE's freestanding headers and dies on bits/stdint.h. # The target (iPXE firmware) code is -ffreestanding/-nostdinc, so the # x86_64 cross toolchain needs NO cross libc headers. RUN apt-get update && apt-get install -y --no-install-recommends \ git make perl gcc binutils libc6-dev \ gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu \ ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /src COPY scripts/build-ipxe.sh scripts/build-ipxe.sh COPY deploy/ipxe/local/ deploy/ipxe/local/ RUN mkdir -p assets/ipxe && bash scripts/build-ipxe.sh /src/assets/ipxe ########## build openpxe ########## # v0.5.2: cross-compile the Rust binary NATIVELY — no QEMU. # # This stage is pinned to $BUILDPLATFORM (the builder's native arch — arm64 # on an Apple-Silicon Mac, amd64 in x86 CI), exactly like `ipxe-build`. The # Rust compiler therefore runs at full native speed and emits an # x86_64-unknown-linux-musl binary via `cargo-zigbuild`, which uses `zig cc` # as the cross-linker (it bundles the musl sysroot for every target, so # there's no fiddly cross-gcc toolchain to assemble). # # Why this replaced the old `FROM rust ... --platform=linux/amd64` build: # that ran the *entire* compiler under QEMU x86_64 emulation on the arm64 # host. It was ~15x slower (a single crate took >20 min) and the emulated # gcc/linker intermittently SIGSEGV'd or hung mid-link. Cross-compiling # sidesteps emulation entirely — the build is minutes, not half an hour, # and is deterministic. # # The output is still a fully static musl binary with no glibc dependency, # so the runtime stage stays free to be any Linux distro. FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-bookworm AS build WORKDIR /src # zig (via the `ziglang` pip package — cargo-zigbuild auto-discovers it as # `python3 -m ziglang`) supplies the x86_64 musl sysroot + linker. # cargo-zigbuild is the thin cargo wrapper that wires zig in as the linker. RUN apt-get update \ && apt-get install -y --no-install-recommends python3 python3-pip \ && rm -rf /var/lib/apt/lists/* \ && rustup target add x86_64-unknown-linux-musl \ && pip3 install --no-cache-dir --break-system-packages ziglang \ && cargo install --locked cargo-zigbuild # Do not copy rust-toolchain.toml into the image. The local workspace pins # developer tooling, but inside Docker we intentionally use the Rust version # selected by the base image. Copying rust-toolchain.toml with # `channel = "stable"` makes rustup download a second full toolchain during # the build, which is slow and can exhaust small Colima/CI disks. COPY Cargo.toml Cargo.lock ./ COPY crates/ crates/ # Baseline binaries (BIOS / i386 / wimboot), then overlay the # PNG-enabled x86_64 + arm64 UEFI binaries built from source. The # overlay wins for snponly.efi / ipxe.efi / snponly-arm64.efi so the # common modern clients get the graphical background; the rest keep the # upstream no-PNG binaries and the menu's `|| console` text fallback. COPY --from=fetch /src/assets/ipxe /src/assets/ipxe COPY --from=ipxe-build /src/assets/ipxe/snponly.efi /src/assets/ipxe/snponly.efi COPY --from=ipxe-build /src/assets/ipxe/ipxe.efi /src/assets/ipxe/ipxe.efi # Cache cargo registry + target across builds. `cargo zigbuild` runs the # native rustc (fast) and links for x86_64-musl with zig — no emulation. RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/src/target,sharing=locked \ cargo zigbuild --release --target x86_64-unknown-linux-musl --bin openpxe && \ cp target/x86_64-unknown-linux-musl/release/openpxe /openpxe && \ ls -l /openpxe ########## runtime ########## FROM debian:12-slim AS runtime RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates libcap2-bin tini gosu iproute2 \ wimtools samba smbclient \ && rm -rf /var/lib/apt/lists/* \ && useradd --system --uid 10001 --home-dir /var/lib/openpxe --shell /usr/sbin/nologin openpxe \ && mkdir -p /var/lib/openpxe/isos /var/lib/openpxe/work /var/lib/openpxe/smb \ && chown -R openpxe:openpxe /var/lib/openpxe # v0.4.5: the openpxe binary itself is now built against musl and is # fully static — no glibc dependency. The runtime stage still ships # Debian slim because OpenPXE shells out to the packages below for # functionality we deliberately don't reimplement in-process: # # wimtools - `wimlib-imagex`, used to inject startnet.cmd into boot.wim. # samba - `smbd` serves extracted Windows install media on :445 so # WinPE can `net use`. Guest read-only, scoped to # /var/lib/openpxe/smb. This package provides the SERVER # side only; the client CLI is a separate package below. # smbclient - v0.4.66: Samba's `smbclient` userspace CLI, used by # the Storage tab's SMB shares manager to list and stream # ISOs from remote SMB servers without ever mounting them # in the kernel. In Debian 12 `smbclient` is NOT pulled # in by the `samba` package — they're siblings, not # parent/child. v0.4.65 shipped without this line and # every "Add share" attempt surfaced # `could not exec smbclient: No such file or directory` # until this landed. # iproute2 - `ip addr` / `ip route` for the auto-detected Network # tab fields (NIC name, subnet mask, default gateway). # Tiny, always available; we don't pull in netlink crates # for this one-shot startup probe. # gosu - drops privileges cleanly from root after the entrypoint # fixes bind-mount ownership (common OpenShift/Docker UX # issue). # # v0.4.65 dropped `nfs-common` — kernel-mount NFS is gone. The SMB # shares replacement uses userspace `smbclient` and needs no kernel # helpers. # # A future "openpxe-static" variant could drop everything except the # binary onto distroless once we move the Windows + SMB legs to # in-process Rust crates. COPY --from=build /openpxe /usr/local/bin/openpxe COPY deploy/docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh # Grant the binary the ability to bind <1024 ports as a non-root user. # This is the only capability OpenPXE needs for proxy-mode DHCP + TFTP + HTTP. RUN setcap cap_net_bind_service=+ep /usr/local/bin/openpxe # IMPORTANT: we do NOT `USER openpxe` here. The entrypoint runs as root, # chowns the mounted data dirs, then execs the binary via gosu as openpxe. # OpenShift ignores USER directives anyway (it injects its own uid), and # there entrypoint.sh's non-root branch just execs directly. WORKDIR /var/lib/openpxe ENV OPENPXE_ISO_DIR=/var/lib/openpxe/isos \ OPENPXE_WORK_DIR=/var/lib/openpxe/work \ OPENPXE_LOG=info,openpxe=info EXPOSE 67/udp 69/udp 4011/udp 80/tcp 445/tcp ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]