# Bootnode web console, image ghcr.io/hanzoai/bootnode:-web. # # Serves the Next.js STATIC EXPORT via ghcr.io/hanzoai/static, matching the # house pattern (hanzo.ai/Dockerfile.production). No nginx, no caddy, no Node # server in the runtime image. # # The build context is the REPOSITORY ROOT and this file is named by path, which # is how the build door takes it (hanzo.yml `images:`, context `.`): # docker build -f console/Dockerfile . # ---- build stage: Next.js static export ---------------------------------- # Node 24 = Active LTS (26 is Current, not LTS). Digest-pinned so the base # cannot drift the way the unpinned pnpm did. pnpm 11 requires Node >= 22. FROM node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS build WORKDIR /app RUN apk add --no-cache libc6-compat # pnpm-workspace.yaml carries the pnpm settings (allowBuilds) that pnpm >= 11 no # longer reads from package.json, and .npmrc is auth/registry only under 11. It # must land before install or sharp's build script is skipped and the install # fails ERR_PNPM_IGNORED_BUILDS. package.json's `packageManager` pins which pnpm # corepack fetches, so a newly published pnpm can never change this build. COPY console/package.json console/pnpm-lock.yaml console/pnpm-workspace.yaml ./ RUN corepack enable pnpm && pnpm install --frozen-lockfile COPY console/ . # ONE image, every brand — and therefore NO brand build args. # # Brand, theme, API origin, IAM issuer and OIDC client are all resolved from # window.location.hostname at runtime (console/lib/brand.ts). This image is # byte-identical for bootno.de, web3.hanzo.ai, lux.cloud, zoo.cloud and # cloud.zoo.network. # # What used to be here — ARG/ENV NEXT_PUBLIC_BRAND, _API_URL, _WS_URL — could # not express that. NEXT_PUBLIC_* is inlined into the client bundle at build # time, so each value is one global shared by every host the image serves; # picking `bootnode` made the image wrong for the other four. It was also # inert: the runtime stage is the Go hanzoai/static binary serving a static # export, which never reads container env at all, so the deployment manifests # that set these had no effect on the shipped bundle whatsoever. ENV NEXT_TELEMETRY_DISABLED=1 RUN pnpm run build # Clean URLs on hanzoai/static. The Next export is FLAT (out/pricing.html, …). # static serves a *directory's* index.html, and in --spa mode a MISS on an # extensionless /pricing falls through to index.html — i.e. every deep page # would render the homepage. Materialize a directory index per page so # root.Open("/pricing") finds a directory and serves the real page; the flat # .html files stay, so /pricing and /pricing.html both work. RUN cd out && for f in $(find . -type f -name '*.html' ! -name 'index.html' ! -name '404.html'); do \ d="${f%.html}"; mkdir -p "$d"; cp "$f" "$d/index.html"; \ done # ---- serve stage: hanzoai/static (scratch + single Go binary) ------------- # Port 3001 is the contract the bootnode-web values in universe declare # (containerPort/servicePort 3001), and the host routes to it by number. FROM ghcr.io/hanzoai/static:0.5.15 COPY --from=build /app/out /srv EXPOSE 3001 ENTRYPOINT ["/static"] CMD ["--root=/srv", "--spa", "--port=3001"]