From de03dbde9a7cc8c64621eb08d3b2e23147d209bf Mon Sep 17 00:00:00 2001 From: ced4568 Date: Tue, 18 Aug 2026 11:15:26 -0500 Subject: [PATCH] fix(lab): restore node-pty terminal runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. 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 --- .dockerignore | 17 +++++++++++++++++ Dockerfile | 23 ++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7165622 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +# Root cause of the node-pty "Cannot find module .../pty.node" failure (Phase 2A): with no +# .dockerignore, a host-side `npm install` (e.g. for local testing) leaves backend/node_modules +# on disk, built against the host's own Node/libnode — not the container's. The Dockerfile's +# later `COPY backend/ ./backend/` (done after the npm-install RUN layer, for build caching) +# then silently overwrites the correctly-built-in-container node_modules with that host copy, +# which fails to load at runtime with an ABI/architecture mismatch. Native modules (node-pty, +# better-sqlite3) MUST always be built inside the image, never leaked in from the host. +# +# IMPORTANT: bare `node_modules/` only matches a node_modules directory at the build context +# ROOT in Docker's .dockerignore matching rules — it does NOT recursively match +# backend/node_modules or frontend/node_modules. This bit us once already (fixed here with +# `**/`): a first pass at this file used the bare pattern and the leak still happened. +**/node_modules/ +frontend/dist/ +.git/ +*.db +*.db-journal diff --git a/Dockerfile b/Dockerfile index 67f4f53..b47fc9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,12 @@ 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 && \ @@ -41,8 +47,23 @@ RUN set -eux && \ 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. +# exists at all). Left uncorrected, node-gyp links native addons against -lnode, producing a +# binary that fails at require() time with "libnode.so.: 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 +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/