# ============================================ # WeLe Agentic AI — production image # # Speech runs in this same process: ONNX on CPU via Transformers.js. No GPU, # no Python, no second service. # # node:20-slim, NOT alpine. onnxruntime-node ships glibc binaries and Alpine is # musl, so the native module fails at load with: # Error loading shared library ld-linux-x86-64.so.2 (needed by libonnxruntime.so.1) # `sharp`, pulled in by Transformers.js, has the same constraint. # ============================================ FROM node:20-slim AS deps WORKDIR /app COPY package*.json ./ # `npm ci` builds exactly the lockfile, so a deploy can never silently pick up # a different dependency tree than the one that was tested. RUN npm ci --omit=dev FROM node:20-slim WORKDIR /app # Run unprivileged. The base image already ships a `node` user. ENV NODE_ENV=production \ PORT=4000 COPY --from=deps /app/node_modules ./node_modules COPY package.json ./ COPY src/ ./src/ # Both of these get a named volume mounted over them in compose. Docker seeds a # NEW volume from the image path — including ownership — so they must exist here # owned by `node`, or the unprivileged process gets EACCES on first write. RUN mkdir -p /app/storage/artifacts /app/.transformers-cache && chown -R node:node /app/storage /app/.transformers-cache USER node EXPOSE 4000 # Compose owns restart policy; this is the in-container liveness signal. HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||4000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" CMD ["node", "src/server.js"]