Root cause, confirmed empirically (not guessed): 1. node-gyp generates an incorrect build/config.gypi for this NodeSource-packaged Node 20 build — it reports node_shared: true (expects a shared libnode.so), but the actual installed Node is statically linked (no libnode.so.<abi> exists anywhere on the system; process.config.variables.node_shared on the real running Node is false). node-gyp trusted the wrong config and linked node-pty's native addon against -lnode, producing a binary that fails at require() time with "libnode.so.127: cannot open shared object file" — confirmed by loading the compiled .node file directly, bypassing node-pty's own path-search/error-masking logic (which collapses this into a generic "Cannot find module .../pty.node" across every path it checks). 2. A second, independent bug compounded it: no .dockerignore existed. A host-side `npm install` (run earlier for backend testing) left backend/node_modules on disk, built against the host's own environment. The Dockerfile's `COPY backend/ ./backend/` (placed after the npm-install RUN layer, for build-cache efficiency) silently overwrote the correctly-built node_modules with that host copy — including after a first .dockerignore fix using the pattern `node_modules/`, which only matches a node_modules directory at the build context ROOT in Docker's matching rules, not at any depth (unlike .gitignore, where the same-looking pattern does match any depth — a real, confirmed gotcha, not an assumption). Fix: - Dockerfile: `ENV npm_config_node_shared=false` (image-level, not a one-off RUN prefix, so it can't be silently lost if a native module ever gets rebuilt again for any reason) + a build-time `node -e "require(...)"` check that fails the build loudly if this regresses, instead of shipping a silent runtime failure. - .dockerignore: `**/node_modules/` (recursive), not `node_modules/`. No terminal architecture change, no fake shell, no changes to backend/server.js's PTY handling. RootNode branding and upstream KubeKosh attribution untouched. The single-container dev workflow (`docker build -t rootnode-academy:test .`) is the same command as before — it now just works. Verified against the real image end-to-end, not a stand-in: build -> container starts -> k3s reaches Ready -> backend starts, no crash -> nginx starts -> browser UI 200s -> terminal WebSocket opens showing the RootNode banner -> `kubectl get nodes` run through the terminal returns real cluster state -> deploy-nginx scenario solved via the terminal -> all 5 validation checks pass. Co-Authored-By: Claude Sonnet 5 <[email protected]>
106 lines
5.4 KiB
Docker
106 lines
5.4 KiB
Docker
FROM ubuntu:22.04
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
ENV KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
|
ENV PROGRESS_FILE=/data/progress.json
|
|
# k3s writes its kubeconfig here; make kubectl pick it up automatically
|
|
ENV K3S_KUBECONFIG_MODE=644
|
|
|
|
# ── System deps ─────────────────────────────────────────────────────────────────────
|
|
RUN apt-get update && apt-get install -y \
|
|
curl wget git vim nano jq bash bash-completion \
|
|
ca-certificates gnupg lsb-release \
|
|
nginx \
|
|
iproute2 iptables iputils-ping \
|
|
procps htop \
|
|
mount kmod \
|
|
python3 python3-yaml make g++ \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# ── Node.js 20 ────────────────────────────────────────────────────────────────
|
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
&& apt-get install -y nodejs \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# node-gyp fix (Phase 2A root cause, see the note on the backend npm-install line below):
|
|
# set as a persistent image ENV, not a one-off RUN prefix, so it applies no matter when or how
|
|
# many times a native module gets (re)built in this image — inline `VAR=x npm install` only
|
|
# scopes the var to that one RUN command, which isn't a strong enough guarantee.
|
|
ENV npm_config_node_shared=false
|
|
|
|
# ── k3s (bundles kubectl, containerd, everything) ────────────────────────────
|
|
# We download the binary and the airgap images so the cluster starts offline.
|
|
RUN set -eux && \
|
|
case "$(uname -m)" in \
|
|
x86_64) K3S_BIN="k3s" ;; \
|
|
aarch64) K3S_BIN="k3s-arm64" ;; \
|
|
*) echo "Unsupported arch: $(uname -m)" && exit 1 ;; \
|
|
esac && \
|
|
curl -fsSL "https://github.com/k3s-io/k3s/releases/latest/download/${K3S_BIN}" \
|
|
-o /usr/local/bin/k3s && \
|
|
chmod +x /usr/local/bin/k3s && \
|
|
ln -sf /usr/local/bin/k3s /usr/local/bin/kubectl && \
|
|
ln -sf /usr/local/bin/k3s /usr/local/bin/crictl
|
|
|
|
|
|
# ── App files ─────────────────────────────────────────────────────────────────
|
|
WORKDIR /app
|
|
|
|
# Install backend dependencies
|
|
#
|
|
# npm_config_node_shared=false (set above, image-level) works around a real node-gyp bug
|
|
# affecting native modules (node-pty, better-sqlite3): node-gyp generates build/config.gypi
|
|
# from a Node headers/config reference it downloads for the target version, and for this
|
|
# NodeSource-packaged Node build that reference incorrectly reports node_shared=true (a shared
|
|
# libnode.so build) when the actual installed Node is statically linked (no libnode.so.<abi>
|
|
# exists at all). Left uncorrected, node-gyp links native addons against -lnode, producing a
|
|
# binary that fails at require() time with "libnode.so.<abi>: cannot open shared object file"
|
|
# — this is the prior node-pty runtime failure ("Cannot find module .../pty.node"),
|
|
# root-caused during Phase 2A: node-pty's own error handling collapses the real dlopen error
|
|
# into a generic "module not found" message across all the paths it checks, which is what made
|
|
# this look like a missing-file problem rather than a link-time misconfiguration. The
|
|
# `node -e require(...)` check below makes this a hard build failure if it ever regresses,
|
|
# instead of a silent runtime failure discovered later.
|
|
COPY backend/package.json ./backend/
|
|
RUN cd backend && npm install --production \
|
|
&& node -e "require('./node_modules/node-pty'); console.log('node-pty loads OK')"
|
|
|
|
# Build frontend
|
|
COPY frontend/package.json frontend/vite.config.js ./frontend/
|
|
RUN cd frontend && npm install
|
|
|
|
COPY frontend/ ./frontend/
|
|
# ARG is declared here so it only busts the cache for this layer
|
|
ARG VITE_APP_VERSION=dev
|
|
RUN cd frontend && VITE_APP_VERSION=${VITE_APP_VERSION} npm run build
|
|
|
|
# Copy everything else
|
|
COPY backend/ ./backend/
|
|
COPY scenarios/ ./scenarios/
|
|
COPY scripts/entrypoint.sh /entrypoint.sh
|
|
COPY scripts/nginx.conf /etc/nginx/nginx.conf
|
|
|
|
# Strip any Windows-style \r from the entrypoint so heredocs inside it
|
|
# don't produce scripts with \r in shebang lines (causes execvp ENOENT).
|
|
RUN sed -i 's/\r//' /entrypoint.sh && chmod +x /entrypoint.sh
|
|
|
|
|
|
# ── Directories & k3s static config ─────────────────────────────────────────
|
|
RUN mkdir -p /root/.kube /data /var/log /tmp/k8s-state \
|
|
&& mkdir -p /var/log/nginx \
|
|
&& mkdir -p /etc/rancher/k3s \
|
|
&& mkdir -p /var/lib/rancher/k3s
|
|
|
|
# Tell k3s to use the native snapshotter.
|
|
# We rely on k3s to generate its full default containerd config.toml
|
|
# (which includes CNI paths, runtimes, etc.) and only override the snapshotter.
|
|
# Do NOT place a custom config.toml.tmpl here — a minimal template breaks
|
|
# node registration by omitting the CNI and internal-opt sections.
|
|
RUN printf 'snapshotter: "native"\nwrite-kubeconfig-mode: "644"\n' \
|
|
> /etc/rancher/k3s/config.yaml
|
|
|
|
# ── Expose ────────────────────────────────────────────────────────────────────
|
|
# Single port - nginx proxies everything
|
|
EXPOSE 80
|
|
|
|
ENTRYPOINT ["/entrypoint.sh"] |