collabSupport/dect-relay-agent/Dockerfile
Joseph McQueen 4f9ebdb5fb Rework DECT relay bundle to ship a pre-built Docker image
The previous packager (scripts/packageDectRelayAgent.js) shipped a
source-only bundle and expected the DC host to build the image with
`docker compose up --build`. That fails hard in corporate DCs with
TLS-intercepted egress: Alpine's apk fetch of dl-cdn.alpinelinux.org
can't verify the intercepted certificate ("apk: TLS: server
certificate not trusted"), and npm install would fail the same way
if apk had succeeded.

New approach: build the image ONCE on the dev machine (where TLS
works), save it as a gzipped tarball, and ship a ZIP whose install
step is `docker load` + `docker compose up -d`. Zero network calls
inside the DC container, ever.

Bundling (dev-machine):
- dect-relay-agent/bundle.sh: build → docker save → gzip → zip.
  Auto-derives version from package.json, records git sha + dirty
  flag + build date into image labels. Cross-arch friendly
  (--platform=linux/amd64 by default; --platform linux/arm64 for
  ARM DCs). Output: dect-relay-agent-bundle-<YYYYMMDD-HHMMSS>.zip
  at repo root (typically 40-60MB).
- dect-relay-agent/Dockerfile: multi-stage node:20-alpine build.
  No apk add. No runtime npm install. Non-root `node` user (uid
  1000). Node handles SIGTERM natively via index.js handlers, so
  no tini/dumb-init needed. Designed to build from the REPO ROOT
  (not the agent folder) because the agent imports shared modules
  from ../integrations/cisco-dect and ../utils.
- dect-relay-agent/Dockerfile.dockerignore: per-Dockerfile ignore
  (BuildKit ≥ 23.0) with a whitelist that keeps the build context
  to ~50KB. Older Docker daemons fall through to the repo-root
  .dockerignore, which already excludes secrets — nothing sensitive
  can leak either way.
- package.json: `npm run package:relay` now invokes bundle.sh.

Runtime (DC-host):
- dect-relay-agent/docker-compose.yml: pins IMAGE_TAG from .env
  (install.sh writes it there — never falls back to :latest), reads
  the rest of the config via env_file, restart: unless-stopped,
  host networking (needed to reach 10.x/8 without userland proxy
  translation, and the agent doesn't listen on anything). Hardened:
  read_only: true rootfs with a 16MB /tmp tmpfs, cap_drop: ALL,
  no-new-privileges, log rotation at 10MB × 5 files.
- dect-relay-agent/install.sh: preflight (docker + compose present,
  daemon reachable, bundle files intact), docker load, pin loaded
  tag into .env, validate .env has the three required values not
  still set to placeholder strings, docker compose up -d, tail last
  40 log lines. Idempotent — safe to re-run on upgrades.

Cleanup:
- scripts/packageDectRelayAgent.js: deleted (superseded).
- .gitignore: drops the scripts/* + !packageDectRelayAgent.js dance
  since we no longer need to whitelist that one file; add pattern
  for the datestamped bundle zips + staging dirs at repo root.
- dect-relay-agent/README.md: replaces the deploy section with the
  new dev-machine-build → DC-host-load workflow, plus a
  troubleshooting section keyed on the exact error messages seen
  during the failed in-DC build (TLS cert not trusted, docker perm
  denied, DIGEST_401).

Verified: all 113 existing tests still pass. Docker build itself
requires a Docker daemon (dev machine) so can't be exercised in
this sandbox — the bash scripts pass `bash -n` syntax checks.
2026-07-03 10:05:05 -04:00

95 lines
4.3 KiB
Docker

# syntax=docker/dockerfile:1.6
#
# DECT relay agent — production image.
#
# BUILD CONTEXT: the REPO ROOT (not this folder). The agent imports
# `../integrations/cisco-dect/*` and `../utils/httpDigestAuth.js`, so
# we mirror the repo's layout under /workspace/ inside the image and
# the relative paths just work.
#
# BUILD FROM REPO ROOT:
# docker build \
# --platform=linux/amd64 \
# -f dect-relay-agent/Dockerfile \
# -t collabsupport/dect-relay-agent:0.1.0 \
# .
#
# Or use bundle.sh which wraps this + `docker save` + zip.
#
# WHY NO `apk add`: corporate DCs commonly TLS-intercept HTTPS. Alpine's
# apk fetch of dl-cdn.alpinelinux.org fails inside the container when
# the CA chain includes a proxy cert the container doesn't trust. We
# avoid the problem entirely by not fetching anything from Alpine at
# build time. Signal handling (SIGTERM / SIGINT / SIGUSR2) is done in
# index.js so we don't need tini/dumb-init.
#
# WHY NO RUNTIME `npm install`: the bundle.sh workflow builds this
# image ONCE outside the DC (where npm registry access works), saves
# 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.
# ─── Stage 1: builder ────────────────────────────────────────────────
# Installs prod deps in a full node image (has python/build-essentials
# just in case a native module needs building — currently `ws` ships
# pre-built optional deps for common arches but we keep the option
# open for future deps).
FROM node:20-alpine AS builder
WORKDIR /workspace/dect-relay-agent
# Copy just the package manifest first so this layer caches across
# code-only changes.
COPY dect-relay-agent/package.json ./package.json
# Install only production deps. --ignore-scripts because we don't run
# arbitrary postinstall from transitive deps in the container build;
# any needed build steps are pinned in this Dockerfile.
RUN npm install --omit=dev --ignore-scripts \
&& npm cache clean --force
# ─── Stage 2: runtime ────────────────────────────────────────────────
# Same base as builder, but only the artifacts we actually need at
# run time (node_modules + agent source + shared integrations + utils).
FROM node:20-alpine AS runtime
# node:20-alpine ships a `node` user (uid 1000) that we can just use —
# no need to install anything extra. Running as a non-root user is a
# 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
# 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
# Agent source + manifest.
COPY --chown=node:node dect-relay-agent/package.json ./package.json
COPY --chown=node:node dect-relay-agent/index.js ./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
# Optional metadata that shows up in `docker inspect` output — useful
# in the DC for "which build am I running?" without needing to poke
# inside the container.
ARG AGENT_VERSION=dev
ARG BUILD_DATE
ARG GIT_COMMIT
LABEL org.opencontainers.image.title="dect-relay-agent" \
org.opencontainers.image.description="Data-center-resident WSS bridge from CollabSupport bot (cloud) to Cisco DBS-210 DECT base stations on 10.x/8" \
org.opencontainers.image.version="${AGENT_VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${GIT_COMMIT}" \
org.opencontainers.image.source="https://git.joesjavajoint.com/jmcqueen/collabSupport"
# 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"]