# ─── 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. # Pinned by digest, not by tag: a tag is a moving pointer, so two builds of the # same commit could otherwise produce different images. These are manifest-list # digests, so buildx still selects the right per-architecture variant. FROM openbao/openbao:2.5.3@sha256:fdc6da21ca6963560c32336fd7feb9cf2d5e52668f1a1647205a4b41171f0806 AS bao-src FROM node:24-alpine@sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf WORKDIR /app # ffmpeg: audio conversion for AWS Transcribe (WebM → PCM) # curl: HTTP helper used by the OpenBao entrypoint and health/debug tooling # jq: JSON parsing for the entrypoint's OpenBao secret-fetch step # pandoc: Markdown → PPTX for Learning resources. It is large (~230MB), and it # is here rather than in a sidecar because a sidecar would add a # cross-stack network dependency to an export that must not fail for # reasons outside this container. It also measures images, which # pptxgenjs cannot: that library emits the target box verbatim with # , so every image in every generated deck was distorted. RUN apk add --no-cache ffmpeg curl jq pandoc-cli # python-pptx builds the slide decks. pandoc still writes Word, where its output # is good, but its pptx writer can only map markdown onto a handful of reference # layouts: no per-slide layout, no positioning, no control over where an image # lands or how large it is. That ceiling is the renderer's, not the model's — a # better-written deck still came out as bullets on a template, and slides # overflowed until autofit was injected into the emitted OOXML by hand. # # py3-lxml and py3-pillow come from apk rather than pip because both are C # extensions and Alpine has no wheels for them; installing from source here # would mean carrying a compiler in the runtime image. Adds ~58MB. RUN apk add --no-cache python3 py3-pip py3-lxml py3-pillow \ && pip install --break-system-packages --no-cache-dir python-pptx==1.0.2 \ && python3 -c 'import pptx' # 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 ./ # argon2 compiles native code via node-gyp — needs python3/make/g++ at build time RUN apk add --no-cache --virtual .build-deps python3 make g++ \ && npm ci --omit=dev \ && apk del .build-deps COPY . . # One validated source revision for both runtime cache busting and OCI provenance. # Direct development builds without an explicit revision remain visibly unversioned. ARG GIT_REVISION=unknown RUN node -e 'const r=process.argv[1]; if (r !== "unknown" && !require("./src/utils/buildId").isGitRevision(r)) throw new Error("GIT_REVISION must be a full lowercase Git SHA"); require("node:fs").writeFileSync("BUILD_ID", r + "\n");' -- "$GIT_REVISION" LABEL org.opencontainers.image.revision=$GIT_REVISION # Ensure the entrypoint is executable regardless of host file permissions RUN chmod +x /app/docker-entrypoint.sh RUN mkdir -p /app/data/logs 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", "server.js"]