ARG BUILDPLATFORM ARG TARGETPLATFORM # Pinned npm version used to replace the bundled npm in the upstream Node # image. Bumping requires a coordinated update in Dockerfile.web and # gitnexus/Dockerfile.test so all images bootstrap the same npm. ARG NPM_VERSION=11.14.1 # -- Builder ----------------------------------------------------------- # Native modules (tree-sitter-*, onnxruntime-node, node-gyp builds for # tree-sitter-proto / tree-sitter-swift) require python3 + a C/C++ toolchain. # node:22-bookworm-slim FROM node:22-bookworm-slim@sha256:9f6d5975c7dca860947d3915877f85607946403fc55349f39b4bc3688448bb6e AS builder ARG NPM_VERSION WORKDIR /app RUN npx --yes npm@${NPM_VERSION} install -g npm@${NPM_VERSION} # Toolchain for node-gyp / native builds. RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ git && rm -rf /var/lib/apt/lists/* # Build gitnexus-shared first - gitnexus depends on it as a workspace. COPY gitnexus-shared/package.json gitnexus-shared/package-lock.json ./gitnexus-shared/ RUN npm ci --prefix gitnexus-shared COPY gitnexus-shared ./gitnexus-shared RUN rm -f gitnexus-shared/tsconfig.tsbuildinfo RUN npm run build --prefix gitnexus-shared # Copy the full gitnexus package before installing - `npm ci` triggers # `postinstall` (patches tree-sitter-swift, builds the vendored # tree-sitter-proto) and `prepare` (compiles TypeScript via scripts/build.js), # both of which need the source tree. COPY gitnexus ./gitnexus RUN npm ci --prefix gitnexus # Drop dev dependencies for a smaller runtime layer. RUN npm prune --omit=dev --prefix gitnexus # `npm prune` removes anything not in package.json's dependency tree — which # includes the VENDORED tree-sitter grammars (materialized into node_modules/ by # postinstall, but not declared as deps) and their freshly-built native bindings. # The `serve` image analyzes/parses uploaded repos at runtime, so those grammars # must survive into the runtime layer. Re-run the grammar postinstall here in the # builder (which still has python3/make/g++ and the hoisted node-addon-api / # node-gyp-build) to re-materialize + rebuild them after the prune. This is # load-bearing for tree-sitter-c (a core, REQUIRED grammar now vendored, #2116): # as a former `dependency` it used to survive prune; vendored, it would not. RUN npm run postinstall --prefix gitnexus # -- Runtime ----------------------------------------------------------- # node:22-bookworm-slim FROM node:22-bookworm-slim@sha256:9f6d5975c7dca860947d3915877f85607946403fc55349f39b4bc3688448bb6e AS runtime # curl for the healthcheck; git for cloning; ca-certificates for TLS verification. RUN apt-get update && apt-get install -y --no-install-recommends curl git ca-certificates && rm -rf /var/lib/apt/lists/* \ && rm -rf /usr/local/lib/node_modules/npm \ && rm -rf /usr/local/lib/node_modules/corepack \ && rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack WORKDIR /app # Pre-create the data directory and hand it to the unprivileged `node` user # so the bind-mounted volume is writable without root. RUN mkdir -p /data/gitnexus && chown -R node:node /data COPY --from=builder --chown=node:node /app/gitnexus/dist ./gitnexus/dist COPY --from=builder --chown=node:node /app/gitnexus/node_modules ./gitnexus/node_modules COPY --from=builder --chown=node:node /app/gitnexus/package.json ./gitnexus/package.json COPY --from=builder --chown=node:node /app/gitnexus/scripts/install-duckdb-extension.mjs ./gitnexus/scripts/install-duckdb-extension.mjs COPY --from=builder --chown=node:node /app/gitnexus/vendor ./gitnexus/vendor # Expose the `gitnexus` binary on PATH so the documented Docker workflow # (`docker compose exec gitnexus-server gitnexus index /workspace/`) # works without users having to invoke `node /app/gitnexus/dist/cli/index.js`. # `npm prune --omit=dev` in the builder stage strips `node_modules/.bin/` # entries, so the `gitnexus` bin declared in package.json (`dist/cli/index.js`, # which already carries `#!/usr/bin/env node` and 755 perms) is otherwise # unreachable from $PATH. RUN ln -s /app/gitnexus/dist/cli/index.js /usr/local/bin/gitnexus # Bake the LadybugDB FTS extension into the image so BM25 keyword search works # at runtime. The server runs the default `load-only` extension policy (the read # pool pins `{ policy: 'load-only' }`), so a runtime `LOAD EXTENSION fts` never # INSTALLs — the extension must already exist in the runtime user's HOME # extension dir, or every keyword search silently degrades (no FTS indexes are # written and ranking falls back to vector-only with only a `warning` field). # Run the installer as the `node` user with the SAME HOME the server runs under, # so `INSTALL fts` materializes the extension under `$HOME/.lbdb/extension` where # the runtime `LOAD` resolves it offline. `ENV HOME` is pinned because Docker # does not derive HOME from `USER`, so without it build-install and runtime-load # would resolve different paths. Requires network egress for the one-time # INSTALL; the build fails loudly if it cannot fetch the extension. The DB-size # default comes from GITNEXUS_LBUG_MAX_DB_SIZE (single source of truth, matches # the runtime) — it only sizes the throwaway scratch DB used to run INSTALL. # The second `--verify-only` step re-LOADs the extension in a FRESH process # under the same HOME, so a HOME/extension-dir mismatch fails the build here # rather than silently degrading keyword search to vector-only at runtime. ENV HOME=/home/node \ GITNEXUS_LBUG_MAX_DB_SIZE=17179869184 RUN su node -s /bin/sh -c "HOME=/home/node node /app/gitnexus/scripts/install-duckdb-extension.mjs fts" \ && su node -s /bin/sh -c "HOME=/home/node node /app/gitnexus/scripts/install-duckdb-extension.mjs fts --verify-only" # Published runtime assets (in package.json `files`). Placed AFTER the DuckDB # FTS-extension RUN above so editing hook/skill content does not invalidate that # network-fetching cache layer; they have no input dependency on it. # `hooks/`: dist/cli/resolve-invocation.js does # `require('../../hooks/claude/resolve-analyze-cmd.cjs')` at module load — the # single source of truth for the npm-11 npx-crash invocation decision (#1939). # Without it, `gitnexus analyze` inside the image crashes with MODULE_NOT_FOUND # before it does any work (#2130). `skills/`: the CLI reads the bundled SKILL.md # templates from `/skills/` for `gitnexus analyze --skills` and `gitnexus # setup`/`uninstall`; absent, those degrade silently (placeholder content / zero # skills installed). (The web UI bundle `web/`, also in `files`, is deliberately # NOT shipped: this builder never builds gitnexus-web, so the image is API-only; # the UI is the separate Dockerfile.web image / hosted app.) COPY --from=builder --chown=node:node /app/gitnexus/hooks ./gitnexus/hooks COPY --from=builder --chown=node:node /app/gitnexus/skills ./gitnexus/skills USER node # The web UI defaults to http://localhost:4747 - keep that contract. ENV GITNEXUS_HOME=/data/gitnexus \ NODE_ENV=production \ PORT=4747 EXPOSE 4747 # Bind to 0.0.0.0 so the server is reachable from the host's mapped port. CMD ["node", "gitnexus/dist/cli/index.js", "serve", "--host", "0.0.0.0", "--port", "4747"]