# syntax=docker/dockerfile:1.7 # # PXEForge — 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 pxeforge 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.82 ########## 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 pxeforge ########## FROM rust:${RUST_VERSION}-bookworm AS build WORKDIR /src # 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. COPY Cargo.toml rust-toolchain.toml ./ COPY crates/ crates/ COPY --from=fetch /src/assets/ipxe /src/assets/ipxe # Cache cargo registry + target across builds. The `--no-edit` 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 --bin pxeforge && \ cp target/release/pxeforge /pxeforge && \ ls -l /pxeforge ########## 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/pxeforge --shell /usr/sbin/nologin pxeforge \ && mkdir -p /var/lib/pxeforge/isos /var/lib/pxeforge/work /var/lib/pxeforge/smb \ && chown -R pxeforge:pxeforge /var/lib/pxeforge # Runtime deps explained: # wimtools - provides `wimlib-imagex`, used to inject startnet.cmd into boot.wim. # samba - `smbd` serves extracted Windows install media on :445 for WinPE # to `net use`. Guest read-only, scoped to /var/lib/pxeforge/smb. # nfs-common - provides `mount.nfs` / `mount.nfs4` for the Storage tab's # NFS share manager. Mount also requires the container to run # with CAP_SYS_ADMIN — without it, mount(2) returns EPERM and # the manager surfaces a clear error in the UI instead of # failing silently. # 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). # Windows-specific tools only activate when the WebUI toggle is on. COPY --from=build /pxeforge /usr/local/bin/pxeforge 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 PXEForge needs for proxy-mode DHCP + TFTP + HTTP. RUN setcap cap_net_bind_service=+ep /usr/local/bin/pxeforge # IMPORTANT: we do NOT `USER pxeforge` here. The entrypoint runs as root, # chowns the mounted data dirs, then execs the binary via gosu as pxeforge. # OpenShift ignores USER directives anyway (it injects its own uid), and # there entrypoint.sh's non-root branch just execs directly. WORKDIR /var/lib/pxeforge ENV PXEFORGE_ISO_DIR=/var/lib/pxeforge/isos \ PXEFORGE_WORK_DIR=/var/lib/pxeforge/work \ PXEFORGE_LOG=info,pxeforge=info EXPOSE 67/udp 69/udp 4011/udp 80/tcp 445/tcp ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]