# syntax=docker/dockerfile:1.7 # BloxOS hub image. # # One build of this checkout produces the hub binary and the agent binaries # the hub serves to enrolling machines, so a running hub always offers # agents from the same commit. Every image, whatever its own architecture, # carries Linux agents for both amd64 and arm64 (cross-compiled, no # emulation) plus the Windows amd64 agent, so one hub serves a mixed fleet; # see docker/README.md. # # Builds are deterministic for a given commit and toolchain (-trimpath, # no VCS stamping, stripped), so rebuilding does not change the served # agent hash and does not trigger a fleet rollout. FROM --platform=$BUILDPLATFORM golang:1.26.8-alpine AS build ARG TARGETARCH ARG BLOXOS_BUILD_VERSION=development ARG BLOXOS_BUILD_REVISION=unknown ENV CGO_ENABLED=0 \ GOTOOLCHAIN=local \ GOFLAGS="-trimpath -buildvcs=false" WORKDIR /src COPY proto/ proto/ COPY hub/go.mod hub/go.sum hub/ COPY agent/go.mod agent/go.sum agent/ RUN --mount=type=cache,target=/go/pkg/mod \ (cd hub && go mod download) && (cd agent && go mod download) COPY hub/ hub/ COPY agent/ agent/ RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ mkdir -p /out/agents \ && (cd hub && GOOS=linux GOARCH="$TARGETARCH" go build -ldflags="-s -w -X main.buildVersion=$BLOXOS_BUILD_VERSION -X main.buildRevision=$BLOXOS_BUILD_REVISION -X main.agentBundleRequired=packaged" -o /out/bloxos-hub .) \ && (cd agent && GOOS=linux GOARCH=amd64 go build -ldflags='-s -w' -o /out/agents/bloxos-agent-linux-amd64 .) \ && (cd agent && GOOS=linux GOARCH=arm64 go build -ldflags='-s -w' -o /out/agents/bloxos-agent-linux-arm64 .) \ && (cd agent && GOOS=windows GOARCH=amd64 go build -ldflags='-s -w' -o /out/agents/bloxos-agent-windows-amd64.exe .) # The catalog that the hub reads at startup, built from the bytes just # produced. It runs in its own stage so the runtime image never gains a Python # interpreter, and it uses the SAME generator and format as the release # catalog — one artifact identity, not a parallel in-image format. # # It omits the image digest, because the image it would name does not exist # yet; a plain local `docker build` also has no release tag or commit SHA. Those # fields are left out rather than filled with a placeholder, so nothing # downstream can mistake a stand-in for provenance. The artifact checks — # sha256, size, architecture and one consistent release marker across all three # platforms — are enforced here exactly as they are for a release. FROM alpine:3.22 AS catalog RUN apk add --no-cache python3 COPY scripts/agent_bundle.py /tmp/agent_bundle.py COPY --from=build /out/agents/ /out/agents/ ARG BLOXOS_BUILD_VERSION=development ARG BLOXOS_BUILD_REVISION=unknown RUN sha=$(python3 /tmp/agent_bundle.py manifest --bundle /out/agents \ --source "$BLOXOS_BUILD_REVISION" --version "$BLOXOS_BUILD_VERSION") \ && python3 /tmp/agent_bundle.py check --bundle /out/agents --manifest-sha256 "$sha" \ --allow-missing-provenance >/dev/null FROM alpine:3.22 RUN apk add --no-cache ca-certificates \ && addgroup -g 65532 -S nonroot \ && adduser -u 65532 -S -G nonroot -h /data -s /sbin/nologin nonroot \ && mkdir -p /data && chown nonroot:nonroot /data && chmod 0700 /data # The hub's served-binary resolver requires root-owned, non-writable # artifacts and ancestors; the hub itself runs unprivileged. The Linux # agents sit at the resolver's per-architecture defaults, # /usr/local/lib/bloxos/linux//bloxos-agent. COPY --from=build --chown=root:root --chmod=0755 /out/bloxos-hub /usr/local/bin/bloxos-hub # The managed bundle and the backward-compatible per-architecture paths, all # created inside ONE layer. # # agents/ sits beside the hub executable, the SAME hub-relative layout the # native server archive uses (/hub/agents), so both deploy modes # resolve agents the same way and the one production hub build can require a # bundle in either. # # The compatibility paths are HARD LINKS, not symlinks: the trusted-binary # resolver rejects a symlinked final path but accepts a regular file. Sharing an # inode also means those paths cannot drift from the managed payloads the way a # second copy of the same bytes silently could. # # The single RUN is what makes the links survive. Copying the payloads in one # layer and linking them in another put the file and its links on opposite # sides of a layer boundary; the copy-up and diff then materialised them as # separate files, and CI measured a link count of 1 — the duplication this was # meant to avoid, silently. A bind mount supplies the bytes read-only without # committing a second copy of them to any layer. RUN --mount=type=bind,from=catalog,source=/out/agents,target=/tmp/agent-input,ro \ set -eu \ && mkdir -p /usr/local/bin/agents /usr/local/lib/bloxos/linux/amd64 \ /usr/local/lib/bloxos/linux/arm64 /usr/local/lib/bloxos/windows \ && cp /tmp/agent-input/bloxos-agent-linux-amd64 /tmp/agent-input/bloxos-agent-linux-arm64 \ /tmp/agent-input/bloxos-agent-windows-amd64.exe /tmp/agent-input/agent-manifest.json \ /usr/local/bin/agents/ \ && ln /usr/local/bin/agents/bloxos-agent-linux-amd64 /usr/local/lib/bloxos/linux/amd64/bloxos-agent \ && ln /usr/local/bin/agents/bloxos-agent-linux-arm64 /usr/local/lib/bloxos/linux/arm64/bloxos-agent \ && ln /usr/local/bin/agents/bloxos-agent-linux-amd64 /usr/local/lib/bloxos/linux/bloxos-agent \ && ln /usr/local/bin/agents/bloxos-agent-windows-amd64.exe /usr/local/lib/bloxos/windows/bloxos-agent.exe \ && chown -R root:root /usr/local/bin/agents /usr/local/lib/bloxos \ && chmod 0755 /usr/local/bin/agents/bloxos-agent-* \ && chmod 0644 /usr/local/bin/agents/agent-manifest.json \ && chmod -R go-w /usr/local/bin/agents /usr/local/lib/bloxos # State lives in one volume: bloxos.db in the working directory, secrets, # setup token and signing key under $HOME/.bloxos. ENV HOME=/data \ HUB_LISTEN=0.0.0.0:4000 WORKDIR /data VOLUME ["/data"] USER nonroot EXPOSE 4000 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget -qO- http://127.0.0.1:4000/health >/dev/null || exit 1 ENTRYPOINT ["/usr/local/bin/bloxos-hub"]