fix(lab): restore node-pty terminal runtime

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]>
This commit is contained in:
2026-08-18 11:15:26 -05:00
co-authored by Claude Sonnet 5
parent a15897c78f
commit de03dbde9a
2 changed files with 39 additions and 1 deletions
+22 -1
View File
@@ -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.<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
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/