Rustelo/templates/website-htmx-ssr/lian-build/Dockerfile.htmx-ssr
2026-07-18 19:57:03 +01:00

169 lines
8.3 KiB
Text

# 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.
FROM --platform=$BUILDPLATFORM ${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/ /workspace/rustelo/
COPY stratumiops/ /workspace/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/ /workspace/rustelo/
COPY stratumiops/ /workspace/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).
ARG BIN_FEATURES
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 && \
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
# 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=/workspace/rustelo/resources/nickel:/workspace/website/site/config
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 && \
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 /workspace/rustelo/crates/foundation/crates/rustelo_content_graph_ssr/Cargo.toml && \
cp /workspace/website/target/release/gen-content-graph /tmp/gen-content-graph && \
sccache --show-stats
# ─── 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 /workspace/rustelo/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"]