# 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 -o ` (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"]