# 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 ########## 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 openpxe ########## FROM rust:${RUST_VERSION}-bookworm AS build WORKDIR /src # v0.4.5: build a fully static musl binary (matches Bootimus v0.1.70's # move). The resulting `/openpxe` has no glibc dependency at all, which: # - Lets the runtime stage be any Linux distro (we still ship Debian # slim for the `samba` / `wimtools` / `nfs-common` shellouts, but a # scratch/distroless variant becomes a one-line swap). # - Cuts a class of "GLIBC_2.39 not found" surprises when running on # older RHEL/Rocky hosts that don't match Debian 12's libc version. # - Sidesteps cross-compilation snags (the binary is its own world). # # x86_64-unknown-linux-musl is fully static by default (no extra # RUSTFLAGS needed). musl-tools provides the linker. RUN apt-get update \ && apt-get install -y --no-install-recommends musl-tools \ && rm -rf /var/lib/apt/lists/* \ && rustup target add x86_64-unknown-linux-musl # Copy the whole workspace in one go. We used to do a two-pass "cache-prime # with stubs, then real build" dance for dep-compile reuse; that turned out # to silently serve stale stub binaries when cargo's fingerprint didn't # notice the source swap. A single build is ~1.5 min longer on cold cache # but guarantees the binary reflects the sources we copied. # 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 # `cargo build`, which is slow and can exhaust small Colima/CI disks. COPY Cargo.toml Cargo.lock ./ COPY crates/ crates/ COPY --from=fetch /src/assets/ipxe /src/assets/ipxe # Cache cargo registry + target across builds. The mtime touch is # belt-and-suspenders: cargo occasionally misses mtime-only changes on # networked FS; this forces a fingerprint check. RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/src/target,sharing=locked \ find crates -name '*.rs' -exec touch {} + && \ cargo build --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 nfs-common \ && 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 four 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. # nfs-common - `mount.nfs` / `mount.nfs4` for the Storage tab's NFS # share manager. Mount requires CAP_SYS_ADMIN; without it # mount(2) returns EPERM and the manager surfaces a clear # error in the UI. # 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). # A future "openpxe-static" variant could drop everything except the # binary onto distroless once we move the Windows + NFS 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"]