diff --git a/dect-relay-agent/Dockerfile b/dect-relay-agent/Dockerfile index e2d8e02..6c23c50 100644 --- a/dect-relay-agent/Dockerfile +++ b/dect-relay-agent/Dockerfile @@ -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"]