From a3c6d33ecf46cb4ac3a5e8601c75bc21d844f31c Mon Sep 17 00:00:00 2001 From: Jonathon Wright Date: Mon, 27 Jul 2026 11:10:20 +1000 Subject: [PATCH] add CSP-clean static build, hardened nginx config, and container image --- .gitignore | 1 + Dockerfile | 22 ++++++++++++ README.md | 3 +- build.sh | 70 +++++++++++++++++++++++++++++++++++++ nginx/default.conf | 51 +++++++++++++++++++++++++++ nginx/security-headers.conf | 19 ++++++++++ 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100755 build.sh create mode 100644 nginx/default.conf create mode 100644 nginx/security-headers.conf diff --git a/.gitignore b/.gitignore index f59f179..3f0d5d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ *.log assets/work/ +dist/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4455c65 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:1 +# Static teaching site: bash assembles dist/ from the split source, then a pinned +# nginx-unprivileged serves it. No Node build — build.sh is plain bash + coreutils. + +# ---- build stage ---------------------------------------------------------- +# Reusing the digest the site image already pins: it's a bookworm-slim base that is +# verified and cached in the zot pull-through mirror, so this layer costs nothing +# extra. Only bash/sed/find/install are used here — node itself is incidental. +FROM node:22-bookworm-slim@sha256:d9f850096136edbc402debdd8729579a288aac64574ada0ff4db26b6ae58b0b2 AS build +WORKDIR /app +COPY . . +RUN bash build.sh + +# ---- runtime stage -------------------------------------------------------- +# Same vetted digest as the site. Renovate keeps it current. +FROM ghcr.io/nginx/nginx-unprivileged:1.28.0-alpine@sha256:c97ff0bf7cbae369953c6da1232ec14ad9f971d66360c5698db0856a4cd657a0 + +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY nginx/security-headers.conf /etc/nginx/security-headers.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 8080 diff --git a/README.md b/README.md index 9ca9258..fe042c8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Interactive scroll-driven teardown of Kubernetes / OpenShift / Docker, in the es Bat-Computer visual world (void `#070b14`, cyan `#3fbaf5` → magenta `#e879f9`). - **v2** (tag `v2`) — CSS `preserve-3d` diagrams. Parked: eyeballed px positions, misaligned at some widths. -- **v3** (in progress) — Nano Banana 2 exploded hero shots, hand-sliced into `clip-path` parts and +- **v3** (in progress) — ships PUBLIC at **learn.bztmon.com**. +- **v3 build** — Nano Banana 2 exploded hero shots, hand-sliced into `clip-path` parts and scrubbed collapsed → hero with GSAP. Spec: `homelab/specs/SPEC-exploded-cluster-v3.md`. ## Layout diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..946229c --- /dev/null +++ b/build.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Assemble dist/ — the CSP-clean static build served at learn.bztmon.com. +# +# The artifact build inlines GSAP and the app JS because the artifact CSP blocks every +# external request. nginx has no such constraint, so here they stay SEPARATE files: +# `script-src 'self'` holds with no 'unsafe-inline' exception, and the two GSAP bundles +# (116KB, unchanged between deploys) become independently cacheable. +set -euo pipefail +IFS=$'\n\t' + +trap 'printf "build failed at line %s\n" "$LINENO" >&2' ERR + +here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +cd -- "$here" + +dist="$here/dist" +src_markup="ec2-part1.html" +src_app="ec2-part2.html" + +for f in "$src_markup" "$src_app" gsap.min.js st.min.js; do + [[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; } +done + +rm -rf -- "$dist" +mkdir -p -- "$dist" + +# --- app.js: strip the single block; served standalone it must be raw JS. +sed -e '1{/^[[:space:]]* + + + + +HTML +} > "$dist/index.html" + +# No inline JS may survive into the served HTML, or the CSP silently kills the page. +# Every