pediatric-ai-scribe-v3/Dockerfile
Daniel fb609f6672 fix(infra): Dockerfile compiles TS, SPA fallback works on Express 4
Rebuilding the pediatric-ai-scribe container surfaced three long-
standing bugs that pre-date today's work but only showed up when
the image was actually rebuilt for the first time since April.
Fixing them in one commit so the live app reflects the React
changes shipped earlier today.

Dockerfile (compiles TypeScript now):
  • COPY package.json → COPY package.json package-lock.json .npmrc
    so npm ci can do a deterministic install.
  • Drop --omit=dev from the install step (tsc is a devDep and we
    need it to compile); prune devDeps AFTER the build runs.
  • RUN npm run build — compiles server.ts + src/ to dist/.
    server.ts was renamed from server.js in commit d60a29f but no
    one updated the Dockerfile; CMD has been "node server.js"
    against a file that didn't exist. The running e2e container
    survived because it was built before the rename.
  • cp package.json dist/package.json — server.ts does
    require('./package.json') which resolves relative to the
    compiled file; keeping a copy next to it is simpler than
    making the source path-aware.
  • ln -s /app/public /app/dist/public — express.static /
    sendFile calls use path.join(__dirname, 'public', …) and
    __dirname at runtime is /app/dist.
  • CMD ["node", "dist/server.js"] (was "server.js").

.npmrc:
  • legacy-peer-deps=true. npm 10+ hard-rejects the transitive
    peerOptional mismatch between openai@4.x (peerOptional zod@^3)
    and our direct dep zod@^4.3.6. Host, CI, and Docker now all
    share one resolution policy so the lock file stays reproducible.

server.ts:
  • `app.get('/app/*splat', …)` → `app.get('/app/*', …)`
  • `app.get('*splat', …)` → `app.get('*', …)`
    Express 5 named-splat syntax doesn't match anything on Express
    4.21 (which we ship). Hard refresh on /wellvisit, /encounter,
    /cms etc. was falling through to the static 404 instead of
    serving the React index. Verified: all 7 clinical routes now
    return HTTP 200 from the SPA fallback.

Smoke-tested against 127.0.0.1:3552 after rebuild:
  /                              HTTP 200
  /wellvisit  /encounter  /cms   HTTP 200
  /auth  /api/health              HTTP 200
  /app/assets/index-C9wKWOAN.js  HTTP 200
  /does-not-exist.jpg            HTTP 404  (static 404 preserved)
2026-04-24 06:03:02 +02:00

76 lines
3.8 KiB
Docker

# ─── OpenBao CLI, copied from upstream image (multi-arch automatic) ───
# Update the tag here to adopt a newer OpenBao. Binary is statically linked,
# safe to drop into the Node alpine image as-is.
FROM openbao/openbao:2.5.3 AS bao-src
FROM node:20-alpine
WORKDIR /app
# ffmpeg: audio conversion for AWS Transcribe (WebM → PCM)
# curl: download Whisper models for browser-based transcription
# jq: JSON parsing for the entrypoint's OpenBao secret-fetch step
RUN apk add --no-cache ffmpeg curl jq
# Pull the bao CLI out of the upstream image — matches host arch because
# buildx pulls the right manifest-list variant per build.
COPY --from=bao-src /bin/bao /usr/local/bin/bao
RUN /usr/local/bin/bao version
COPY package.json package-lock.json .npmrc ./
# argon2 compiles native code via node-gyp — needs python3/make/g++ at build time.
# npm ci installs the exact versions from package-lock.json so a transitive
# peer-dep conflict (e.g. zod v4 vs openai's peerOptional zod@^3.x) can't
# break the image build — the lock file has already resolved it once.
# Install with devDeps so tsc is available for the compile step below.
RUN apk add --no-cache --virtual .build-deps python3 make g++ \
&& npm ci \
&& apk del .build-deps
COPY . .
# Compile TypeScript → dist/, then prune devDeps from node_modules to keep
# the final image slim. `server.ts` is the entry point (renamed from
# server.js in commit d60a29f); tsconfig emits to ./dist.
#
# Also copy package.json into dist/ — server.ts does `require('./package.json')`
# to read the app version at boot, which resolves relative to dist/server.js
# at runtime rather than the repo root. Keeping a copy alongside the compiled
# entry is simpler than making the source path-aware.
RUN npm run build \
&& cp package.json dist/package.json \
&& ln -s /app/public /app/dist/public \
&& npm prune --omit=dev
# Ensure the entrypoint is executable regardless of host file permissions
RUN chmod +x /app/docker-entrypoint.sh
RUN mkdir -p /app/data/logs
# Download Browser Whisper (COMPLETE self-hosting - zero CDN dependencies)
# Library + Models all bundled and served from our server
RUN mkdir -p /app/public/models/Xenova/whisper-tiny.en/onnx && \
cd /app/public/models && \
echo "Downloading transformers.js library (worker-compatible build)..." && \
curl -sL -o transformers.min.js https://cdn.jsdelivr.net/npm/@xenova/transformers@2.0.0/dist/transformers.min.js && \
cd Xenova/whisper-tiny.en && \
echo "Downloading Whisper model files..." && \
curl -sL -o config.json https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/config.json && \
curl -sL -o tokenizer.json https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/tokenizer.json && \
curl -sL -o preprocessor_config.json https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/preprocessor_config.json && \
curl -sL -o generation_config.json https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/generation_config.json && \
curl -sL -o onnx/encoder_model_quantized.onnx https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/onnx/encoder_model_quantized.onnx && \
curl -sL -o onnx/decoder_model_merged_quantized.onnx https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/onnx/decoder_model_merged_quantized.onnx && \
echo "✅ Browser Whisper: 100% self-hosted (library: 760KB, models: 42MB)"
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
# Entrypoint wrapper handles optional OpenBao secret fetch before exec'ing CMD.
# See docker-entrypoint.sh for the logic — it is a no-op if OPENBAO_ADDR is
# unset, so legacy .env-only deployments continue to work unchanged.
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["node", "dist/server.js"]