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

214 lines
11 KiB
Text

# Build context: repo root (website/).
# External path deps assembled by ctx-leptos into the build context:
# rustelo/ → /workspace/rustelo/ (Cargo path deps: ../rustelo/...)
#
# Build arguments — supplied via --build-arg from lian-build/build_directives.ncl.
# Canonical values live in the NCL; no defaults here to avoid drift.
# RUST_VERSION lamina catalog tag for leptos/node/nickel layers
# RUSTELO_VERSION lamina rustelo layer tag (rustelo framework version, e.g. 0.1.0)
# NODE_VERSION lamina/node tag (e.g. 22)
# LAMINA_REGISTRY lamina catalog registry base
# BIN_NAME [[bin]] name in crates/server/Cargo.toml
# LEPTOS_OUTPUT_NAME name in [[workspace.metadata.leptos]]
# BIN_FEATURES extra --bin-features passed to cargo leptos build
# SERVER_PORT port the server listens on (EXPOSE + LEPTOS_SITE_ADDR)
#
ARG RUST_VERSION
ARG RUSTELO_VERSION
ARG LEPTOS_VERSION
ARG NICKEL_VERSION
ARG NODE_VERSION
ARG LAMINA_REGISTRY
ARG BIN_NAME
ARG LEPTOS_OUTPUT_NAME
ARG BIN_FEATURES
ARG SERVER_PORT
# Named alias — BuildKit does not expand ARGs inside --from=; named stage is required.
FROM --platform=$BUILDPLATFORM ${LAMINA_REGISTRY}/nickel:${NICKEL_VERSION} AS nickel-bin
# ─── Stage 0: cargo-chef planner ─────────────────────────────────────────────
# lamina/leptos: cargo + cargo-chef + sccache + wasm32 + wasm-bindgen + wasm-opt + cargo-leptos
FROM --platform=$BUILDPLATFORM ${LAMINA_REGISTRY}/leptos:${LEPTOS_VERSION} AS planner
WORKDIR /workspace
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: dependency cache ────────────────────────────────────────────────
# rustelo layer (FROM leptos + chef cook for rustelo workspace) gives us
# ~/.cargo/registry pre-populated with rustelo's transitive crate downloads,
# so cargo chef cook here only needs to fetch website-specific deps.
FROM ${LAMINA_REGISTRY}/rustelo:${RUSTELO_VERSION} AS builder-deps
# Pin target dir: base images vary (leptos=/cargo-install-target, rustelo=/workspace/target).
# COPY paths in the runtime stage depend on this value being stable.
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/
# site/config needed by build.rs: nickel evaluates NCL during cargo compilation
COPY site/config/ /workspace/website/site/config/
# SSR target only — WASM cook pulls incompatible platform deps (mio/tokio-net);
# cargo-leptos handles WASM compilation with correct feature isolation.
ARG BIN_FEATURES
RUN --mount=type=secret,id=sccache_env,required \
--mount=type=cache,target=/workspace/website/target \
set -a && . /run/secrets/sccache_env && set +a && \
export RUSTC_WRAPPER=sccache && \
cargo chef cook --release --no-default-features --features "ssr,${BIN_FEATURES}" \
--recipe-path recipe.json && \
sccache --show-stats
# ─── Stage 2: CSS (unocss) ────────────────────────────────────────────────────
# lamina/node has node + pnpm pre-installed — no corepack setup needed.
# unocss scans .rs sources — WORKDIR and rustelo/ must match workspace layout.
FROM --platform=$BUILDPLATFORM ${LAMINA_REGISTRY}/node:${NODE_VERSION} AS css
WORKDIR /workspace/website
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY uno.config.ts .
COPY crates/ ./crates/
COPY rustelo/ /workspace/rustelo/
RUN pnpm run css:build
# ─── Stage 3: Rust build ──────────────────────────────────────────────────────
FROM builder-deps AS builder
WORKDIR /workspace
COPY Cargo.toml Cargo.lock ./website/
COPY crates/ ./website/crates/
COPY rustelo/ /workspace/rustelo/
COPY stratumiops/ /workspace/stratumiops/
# ManifestResolver (rustelo_utils) walks up from CARGO_MANIFEST_DIR looking for
# rustelo.manifest.toml at the workspace root — must be present for build scripts.
COPY rustelo.manifest.toml ./website/rustelo.manifest.toml
COPY rustelo/ ./website/rustelo/
# Pre-built static assets (CSS bundles, themes, highlight, logos, etc.)
# cargo-leptos copies site/public/ → target/site/ during build
COPY site/public/ ./website/site/public/
# Freshly generated unocss output overwrites any stale pre-built copy
COPY --from=css /workspace/website/site/public/styles/website.css \
./website/site/public/styles/website.css
# NCL config + runtime config files
COPY site/config/ ./website/site/config/
COPY site/rbac.ncl ./website/site/rbac.ncl
# content-static feature: build.rs bakes content and i18n at compile time
COPY site/content/ ./website/site/content/
COPY site/i18n/ ./website/site/i18n/
COPY site/templates/ ./website/site/templates/
WORKDIR /workspace/website
# build.rs (build_page_generator) reads SITE_CONFIG_PATH to locate config.ncl
ENV SITE_CONFIG_PATH=/workspace/website/site/config/index.ncl
# rustelo_config::nickel::resolve_import_path walks for rustelo/nickel/ but the
# actual layout is rustelo/resources/nickel/. Set NICKEL_IMPORT_PATH explicitly
# so the env-var fallback fires. site/config is included for cross-domain imports.
ENV NICKEL_IMPORT_PATH=/workspace/rustelo/resources/nickel:/workspace/website/site/config
# wasm-opt on linux/arm64 in this base image strips the externref table
# growability that wasm-bindgen needs at hydration init → runtime
# `WebAssembly.Table.grow(): failed to grow table by 4`. Neither v123 nor v129
# behaves correctly here, so we disable optimization entirely by stubbing
# wasm-opt with a passthrough script. cargo-leptos resolves wasm-opt via
# `from_global_path` first (cargo-leptos-0.3.6/src/ext/exe.rs:225), so a stub
# on PATH wins over any downloaded copy. cargo-leptos invokes it as
# `wasm-opt <flags> <file> -o <file>` (in-place), so a no-op exit 0 leaves the
# wasm-bindgen output untouched.
RUN printf '#!/bin/sh\nexit 0\n' > /usr/local/bin/wasm-opt && \
chmod +x /usr/local/bin/wasm-opt
ARG BIN_FEATURES
ARG BIN_NAME
ARG LEPTOS_OUTPUT_NAME
RUN --mount=type=secret,id=sccache_env,required \
--mount=type=cache,target=/workspace/website/target \
set -a && . /run/secrets/sccache_env && set +a && \
export RUSTC_WRAPPER=sccache && \
sccache --start-server && \
cargo leptos build --release --features "${BIN_FEATURES}" && \
cp /workspace/website/target/release/${BIN_NAME} /tmp/${BIN_NAME} && \
cp -r /workspace/website/target/site/ /tmp/site/ && \
sccache --show-stats
# ─── Stage 4: 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 LEPTOS_OUTPUT_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}
# 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.
# NICKEL_IMPORT_PATH must include this path so nickel resolves server/, site/, etc.
COPY --from=builder /workspace/rustelo/resources/nickel/ /usr/local/share/rustelo/nickel/
# Leptos WASM bundle paired with the server binary — both produced by the same
# cargo-leptos invocation in the builder stage. Source path is /tmp/site/pkg/
# because /workspace/website/target is a buildkit cache-mount, unmounted at
# the end of the RUN; only /tmp survives into later stages. Lives outside
# /var/www/ so the PV mount can't shadow it. The bootstrap recipe copies this
# into the PV at /var/www/site/pkg/ so binary and WASM stay version-matched by
# construction (Leptos server function hashes break under any binary↔WASM drift).
COPY --from=builder /tmp/site/pkg/ /usr/local/share/website/pkg/
WORKDIR /var/www
# Content-agnostic image: NO site/ baked in. The site tree (pkg/, public/, content/,
# config/, i18n/, templates/, rbac.ncl) is delivered to the PV mounted at
# /var/www/site/ by the publish tool — a gtar of website/site/ is packed,
# uploaded to the PV, unpacked, and the pod restarted to pick up the new tree.
# server.ncl public_dir = "site/public" resolves identically in dev (source tree)
# and prod (PV-delivered tree).
# The PV mount must be populated before first pod start; the K8s securityContext
# fsGroup=1000 (from the 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 \
LEPTOS_OUTPUT_NAME=${LEPTOS_OUTPUT_NAME} \
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-leptos-server "$@"
EOF
CMD ["/usr/local/bin/entrypoint.sh"]