website-htmx-rustelo-code/lian-build/Dockerfile.htmx-ssr

200 lines
10 KiB
Text
Raw Normal View History

2026-07-10 03:44:13 +01:00
# htmx-ssr runtime image. Disjoint layer graph from Dockerfile.leptos-hydration: no
# wasm32 target, no wasm-bindgen, no wasm-opt, no cargo-leptos, no node/css
# stage. The server renders the full body server-side; interactivity is htmx
# attributes + /api/htmx/* endpoints, so the image bakes only the binary.
#
# Base: lamina/rust (rust-slim + sccache + cargo-chef + just + mold/clang) — NOT
# lamina/leptos/lamina/rustelo. The rustelo pre-cook layer buys nothing here:
# the framework source is COPY'd in regardless (path dep), so cargo-chef cooks
# the full recipe (rustelo + website deps) over the thin rust base.
#
# Content-agnostic: NO site/ baked in, and unlike the leptos image there is no
# WASM pkg/ either. The entire site tree (content/, config/, i18n/, templates/,
# public/ incl. freshly-built styles/website.css, rbac.ncl) is delivered to the
# PV at /var/www/site/ by the publish tool. website.css is built during distro
# assembly (just distro / content.nu run uno), never inside this image.
#
# Build arguments — supplied via --build-arg from lian-build/build_directives.ncl.
# RUST_VERSION lamina/rust tag
# NICKEL_VERSION lamina/nickel tag (runtime nickel + build.rs NCL eval)
# LAMINA_REGISTRY lamina catalog registry base
# BIN_NAME [[bin]] name in crates/server/Cargo.toml
# BIN_FEATURES full feature expression for the SSR build (e.g. "htmx-ssr")
# SERVER_PORT port the server listens on (EXPOSE + LEPTOS_SITE_ADDR)
ARG RUST_VERSION
ARG NICKEL_VERSION
ARG LAMINA_REGISTRY
ARG BIN_NAME
ARG BIN_FEATURES
ARG SERVER_PORT
# Named alias — BuildKit does not expand ARGs inside --from=; named stage required.
# No --platform override: nickel is invoked both by build.rs (native, in-build,
# must match the current target arch) and copied into the runtime image (same
# arch again) — $BUILDPLATFORM pinned it to the buildkit worker's own arch
# instead, which breaks when one worker native-builds a non-native target arch
# (e.g. lian-02 serving both amd64 and arm64 claims).
FROM ${LAMINA_REGISTRY}/nickel:${NICKEL_VERSION} AS nickel-bin
# ─── Stage 0: cargo-chef planner ─────────────────────────────────────────────
FROM ${LAMINA_REGISTRY}/rust:${RUST_VERSION} AS planner
WORKDIR /workspace
ENV CARGO_TARGET_DIR=/workspace/website/target
COPY Cargo.toml Cargo.lock ./website/
COPY crates/ ./website/crates/
COPY rustelo/ /rustelo/
COPY stratumiops/ /stratumiops/
WORKDIR /workspace/website
RUN cargo chef prepare --recipe-path recipe.json
# ─── Stage 1: build (deps cook + SSR binary) ─────────────────────────────────
FROM ${LAMINA_REGISTRY}/rust:${RUST_VERSION} AS builder
# Pin target dir so the runtime-stage COPY path is stable (lamina/rust defaults
# to /cargo-install-target).
ENV CARGO_TARGET_DIR=/workspace/website/target
COPY --from=nickel-bin /usr/local/bin/nickel /usr/local/bin/nickel
WORKDIR /workspace/website
COPY --from=planner /workspace/website/recipe.json recipe.json
COPY rustelo/ /rustelo/
COPY stratumiops/ /stratumiops/
# Cook deps for the exact feature set the binary is built with — no WASM target,
# so a single native cook covers everything (contrast: the leptos image cooks
# ssr deps separately because cargo-leptos handles the wasm32 pass).
# sccache DISABLED TEMPORARILY (2026-07-06): the S3 backend (Garage, libre-wuji)
# is not yet reachable from the fleet's buildkit runners — cross-network routing
# unresolved. RUSTC_WRAPPER unset, builds run uncached until that's fixed. When
# re-enabling, keep SCCACHE_S3_KEY_PREFIX="${TARGETARCH}/" — this is a native
# (non-cross-compiled) multi-arch build, so cache entries must stay namespaced
# per architecture.
ARG BIN_FEATURES
ARG TARGETARCH
# Original (sccache-enabled) — restore once Garage is reachable:
# RUN --mount=type=secret,id=sccache_env,required \
# --mount=type=cache,target=/workspace/website/target \
# --mount=type=cache,target=/usr/local/cargo/registry \
# set -a && . /run/secrets/sccache_env && set +a && \
# export RUSTC_WRAPPER=sccache && \
# export SCCACHE_S3_KEY_PREFIX="${TARGETARCH}/" && \
# RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" \
# cargo chef cook --release --no-default-features --features "${BIN_FEATURES}" \
# --recipe-path recipe.json && \
# sccache --show-stats
RUN --mount=type=cache,target=/workspace/website/target,id=cargo-target-${TARGETARCH} \
--mount=type=cache,target=/usr/local/cargo/registry \
RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" \
cargo chef cook --release --no-default-features --features "${BIN_FEATURES}" \
--recipe-path recipe.json
# Full sources for the actual build.
COPY Cargo.toml Cargo.lock ./
COPY crates/ ./crates/
# ManifestResolver (rustelo_utils) walks up from CARGO_MANIFEST_DIR for this.
COPY rustelo.manifest.toml ./rustelo.manifest.toml
# build.rs (build_page_generator) evaluates site/config NCL during compilation.
COPY site/config/ ./site/config/
COPY site/rbac.ncl ./site/rbac.ncl
ARG BIN_NAME
# build.rs reads SITE_CONFIG_PATH; nickel resolves framework contracts via
# NICKEL_IMPORT_PATH (rustelo layout is resources/nickel/, not nickel/).
ENV SITE_CONFIG_PATH=/workspace/website/site/config/index.ncl
ENV NICKEL_IMPORT_PATH=/rustelo/code/resources/nickel:/workspace/website/site/config
# Original (sccache-enabled) — restore once Garage is reachable:
# RUN --mount=type=secret,id=sccache_env,required \
# --mount=type=cache,target=/workspace/website/target \
# --mount=type=cache,target=/usr/local/cargo/registry \
# set -a && . /run/secrets/sccache_env && set +a && \
# export RUSTC_WRAPPER=sccache && \
# export SCCACHE_S3_KEY_PREFIX="${TARGETARCH}/" && \
# sccache --start-server && \
# RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" \
# cargo build --release -p rustelo-htmx-server --no-default-features --features "${BIN_FEATURES}" && \
# cp /workspace/website/target/release/${BIN_NAME} /tmp/${BIN_NAME} && \
# CARGO_TARGET_DIR=/workspace/website/target \
# cargo build --release -p rustelo_content_graph_ssr --features gen \
# --manifest-path /rustelo/code/crates/foundation/crates/rustelo_content_graph_ssr/Cargo.toml && \
# cp /workspace/website/target/release/gen-content-graph /tmp/gen-content-graph && \
# sccache --show-stats
RUN --mount=type=cache,target=/workspace/website/target,id=cargo-target-${TARGETARCH} \
--mount=type=cache,target=/usr/local/cargo/registry \
RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" \
cargo build --release -p rustelo-htmx-server --no-default-features --features "${BIN_FEATURES}" && \
cp /workspace/website/target/release/${BIN_NAME} /tmp/${BIN_NAME} && \
CARGO_TARGET_DIR=/workspace/website/target \
cargo build --release -p rustelo_content_graph_ssr --features gen \
--manifest-path /rustelo/code/crates/foundation/crates/rustelo_content_graph_ssr/Cargo.toml && \
cp /workspace/website/target/release/gen-content-graph /tmp/gen-content-graph
# ─── Stage 2: runtime ─────────────────────────────────────────────────────────
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libssl3 curl \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -u 1000 www
ARG BIN_NAME
ARG SERVER_PORT
# Server binary → /usr/local/bin/ so data PV mounts never shadow it.
COPY --from=builder /tmp/${BIN_NAME} /usr/local/bin/${BIN_NAME}
# Content-graph generator: regenerates content_graph.json from site/content + ontoref
# without a rebuild. Run: gen-content-graph [out.json] (needs ONTOREF_NICKEL_IMPORT_PATH).
COPY --from=builder /tmp/gen-content-graph /usr/local/bin/gen-content-graph
# nickel required at runtime: server calls `nickel export` to parse site/config/*.ncl
# on startup and site/rbac.ncl on RBAC hot-reload.
COPY --from=nickel-bin /usr/local/bin/nickel /usr/local/bin/nickel
# Rustelo framework nickel contracts — required by site/config/*.ncl at runtime.
COPY --from=builder /rustelo/code/resources/nickel/ /usr/local/share/rustelo/nickel/
WORKDIR /var/www
# Content-agnostic image: NO site/ baked in, NO WASM pkg/. The site tree
# (public/ incl. styles/website.css, content/, config/, i18n/, templates/,
# rbac.ncl) is delivered to the PV mounted at /var/www/site/ by the publish
# tool. server.ncl public_dir = "site/public" resolves identically in dev
# (source tree) and prod (PV-delivered tree). The htmx shell loads its runtime
# from public/ (htmx.min.js, extensions, reinit handlers) — all PV-served.
# The PV mount must be populated before first pod start; the K8s securityContext
# fsGroup=1000 (catalog deployment template) handles ownership at mount.
USER www
EXPOSE ${SERVER_PORT}
ENV RUST_LOG=info \
LEPTOS_ENV=PROD \
LEPTOS_SITE_ADDR=0.0.0.0:${SERVER_PORT} \
LEPTOS_SITE_ROOT=site \
SITE_CONFIG_PATH=/var/www/site/config/index.ncl \
NICKEL_IMPORT_PATH=/usr/local/share/rustelo/nickel:/var/www/site/config \
SITE_SERVER_CONTENT_URL=/r \
SITE_SERVER_ROOT_CONTENT=r \
SITE_SERVER_CONTENT_ROOT=site/r \
NATS_ADMIN_CREDENTIALS_FILE=
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -sf http://localhost:${SERVER_PORT}/health > /dev/null || exit 1
# Entrypoint: wait for the PV-delivered site tree before exec'ing the server.
# Image is content-agnostic; site/config/index.ncl must exist for the binary to
# boot. While the PV is empty (first install, before publish-tool delivery),
# this keeps the container alive and logs a clear "waiting" message instead of
# crash-looping. Signals propagate via exec when the server takes over.
COPY --chmod=755 <<'EOF' /usr/local/bin/entrypoint.sh
#!/bin/sh
set -eu
CONFIG="${SITE_CONFIG_PATH:-/var/www/site/config/index.ncl}"
while [ ! -f "$CONFIG" ]; do
printf '[%s] site config missing at %s — waiting for PV bootstrap (publish tool to deliver site tarball)\n' \
"$(date -Iseconds)" "$CONFIG" >&2
sleep 10
done
printf '[%s] site config found at %s — starting server\n' "$(date -Iseconds)" "$CONFIG" >&2
exec /usr/local/bin/rustelo-htmx-server "$@"
EOF
CMD ["/usr/local/bin/entrypoint.sh"]