Fix DECT relay container: install node_modules at /workspace, not under agent

The runtime container failed with ERR_MODULE_NOT_FOUND: axios when
integrations/cisco-dect/client.js tried to load. Root cause is
Node's ESM resolver: it walks UP from the IMPORTING file looking
for node_modules, never sideways into siblings.

Container filesystem before:
  /workspace/dect-relay-agent/node_modules/       <- axios lives here
  /workspace/dect-relay-agent/index.js            <- ok, finds it by walking up
  /workspace/integrations/cisco-dect/client.js    <- walks up to /, never sees axios

Node 20.20 has --experimental-detect-module ON by default, so
client.js is still treated as ESM (starts with `import`), and the
resolver correctly reports "cannot find package 'axios'" rather
than syntax-erroring on the import keyword. But it still can't find
the package — the location is wrong.

Fix: install node_modules at /workspace/ so BOTH the agent AND the
shared modules can find it by walking up.

  /workspace/node_modules/                        <- axios here now
  /workspace/package.json                         <- also here, "type":"module" for all descendants
  /workspace/dect-relay-agent/index.js            <- walks up to /workspace/node_modules ✓
  /workspace/integrations/cisco-dect/client.js    <- walks up to /workspace/node_modules ✓
  /workspace/utils/httpDigestAuth.js              <- same ✓

WORKDIR moves from /workspace/dect-relay-agent to /workspace, and
CMD changes accordingly:
  node --enable-source-maps dect-relay-agent/index.js

Rebuild + reship the bundle with `./dect-relay-agent/bundle.sh` and
`./install.sh` on the DC host — it's an idempotent upgrade.

Verified locally (agent modules import cleanly using the same
directory shape as the container).
This commit is contained in:
Joseph McQueen 2026-07-03 10:15:57 -04:00
parent 4f9ebdb5fb
commit 59460b849b

View file

@ -28,6 +28,22 @@
# it as a tarball, and ships the tarball. The DC only runs
# `docker load` + `docker compose up -d` — zero network calls beyond
# the initial docker load.
#
# WHERE node_modules LIVES (subtle but critical):
# /workspace/node_modules ← NOT under dect-relay-agent/
#
# The agent imports `../integrations/cisco-dect/client.js`, which
# in turn does `import axios from 'axios'`. Node's ESM resolver
# walks UP from the IMPORTING file (client.js) looking for
# node_modules — it does NOT search siblings. So if node_modules
# lived at /workspace/dect-relay-agent/node_modules, then
# client.js (at /workspace/integrations/cisco-dect/client.js) would
# never find axios and blow up with ERR_MODULE_NOT_FOUND at runtime.
# Placing node_modules one level higher fixes it: both the agent
# AND the shared integrations resolve axios via /workspace/node_modules.
# The package.json at /workspace/ also declares "type":"module" so
# every .js file under /workspace/ is treated as ESM without needing
# its own package.json.
# ─── Stage 1: builder ────────────────────────────────────────────────
# Installs prod deps in a full node image (has python/build-essentials
@ -37,10 +53,12 @@
FROM node:20-alpine AS builder
WORKDIR /workspace/dect-relay-agent
WORKDIR /workspace
# Copy just the package manifest first so this layer caches across
# code-only changes.
# code-only changes. The agent's package.json IS the workspace
# package.json — same "type":"module", same deps (ws / axios /
# dotenv), just placed one directory higher.
COPY dect-relay-agent/package.json ./package.json
# Install only production deps. --ignore-scripts because we don't run
@ -60,21 +78,26 @@ FROM node:20-alpine AS runtime
# baseline hardening we get essentially for free.
USER node
# Match the repo layout so relative imports (`../integrations/...`)
# resolve exactly as they do in development.
WORKDIR /workspace/dect-relay-agent
# WORKDIR is the workspace root so `node dect-relay-agent/index.js`
# resolves correctly AND node_modules at /workspace/node_modules is
# discoverable by both the agent and the shared modules.
WORKDIR /workspace
# Ship the node_modules we built in stage 1. Ownership goes to `node`
# so the process can read them without needing root.
COPY --from=builder --chown=node:node /workspace/dect-relay-agent/node_modules ./node_modules
# Shared node_modules (see the header comment for why it's here and
# not under dect-relay-agent/). Ownership goes to `node` so the
# process can read them without needing root.
COPY --from=builder --chown=node:node /workspace/node_modules ./node_modules
# Agent source + manifest.
# Package manifest at workspace root — Node uses this to determine
# "type":"module" for every .js file under /workspace/**.
COPY --chown=node:node dect-relay-agent/package.json ./package.json
COPY --chown=node:node dect-relay-agent/index.js ./index.js
# Agent source.
COPY --chown=node:node dect-relay-agent/index.js ./dect-relay-agent/index.js
# Shared modules the agent imports from the parent workspace.
COPY --chown=node:node integrations/cisco-dect /workspace/integrations/cisco-dect
COPY --chown=node:node utils/httpDigestAuth.js /workspace/utils/httpDigestAuth.js
COPY --chown=node:node integrations/cisco-dect ./integrations/cisco-dect
COPY --chown=node:node utils/httpDigestAuth.js ./utils/httpDigestAuth.js
# Optional metadata that shows up in `docker inspect` output — useful
# in the DC for "which build am I running?" without needing to poke
@ -92,4 +115,4 @@ LABEL org.opencontainers.image.title="dect-relay-agent" \
# Node handles SIGTERM natively when the process installs handlers
# (which we do in index.js). --enable-source-maps improves stack
# traces if something crashes at runtime — cheap and always-on.
CMD ["node", "--enable-source-maps", "index.js"]
CMD ["node", "--enable-source-maps", "dect-relay-agent/index.js"]