|
| 1 | +# LeanMCP — Linux container image |
| 2 | +# |
| 3 | +# Default behavior: starts the LeanMCP proxy on 0.0.0.0:8799 (MCP Streamable HTTP) |
| 4 | +# and 0.0.0.0:9464 (Prometheus). It fans out to five upstream MCP servers via |
| 5 | +# `npx`, so we warm the npm cache for those packages during build to make the |
| 6 | +# first call fast and self-contained. |
| 7 | +# |
| 8 | +# Override CMD to run the bench harness instead, e.g.: |
| 9 | +# docker run --rm leanmcp:dev pnpm bench --skip-agent |
| 10 | +# |
| 11 | +# Multi-stage layout: |
| 12 | +# 1. deps — install ALL dependencies (incl. dev) for build + bench |
| 13 | +# 2. build — compile TypeScript -> dist |
| 14 | +# 3. runtime — slim final image with deps + dist + bench harness |
| 15 | + |
| 16 | +ARG NODE_VERSION=20.18.0 |
| 17 | +ARG PNPM_VERSION=10.26.0 |
| 18 | + |
| 19 | +# ----------------------------------------------------------------------------- |
| 20 | +# Stage 1: deps — pnpm install with frozen lockfile |
| 21 | +# ----------------------------------------------------------------------------- |
| 22 | +FROM node:${NODE_VERSION}-bookworm-slim AS deps |
| 23 | +ARG PNPM_VERSION |
| 24 | + |
| 25 | +ENV PNPM_HOME=/pnpm \ |
| 26 | + PATH="/pnpm:$PATH" \ |
| 27 | + CI=true |
| 28 | + |
| 29 | +# Install a pinned pnpm without corepack (corepack hits npmjs.org for "latest" |
| 30 | +# even when packageManager isn't pinned, which fails on flaky networks). |
| 31 | +RUN npm install -g pnpm@${PNPM_VERSION} \ |
| 32 | + || (sleep 5 && npm install -g pnpm@${PNPM_VERSION}) |
| 33 | + |
| 34 | +WORKDIR /app |
| 35 | + |
| 36 | +COPY package.json pnpm-lock.yaml ./ |
| 37 | + |
| 38 | +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ |
| 39 | + pnpm install --frozen-lockfile |
| 40 | + |
| 41 | +# ----------------------------------------------------------------------------- |
| 42 | +# Stage 2: build — produce dist/ |
| 43 | +# ----------------------------------------------------------------------------- |
| 44 | +FROM node:${NODE_VERSION}-bookworm-slim AS build |
| 45 | +ARG PNPM_VERSION |
| 46 | + |
| 47 | +ENV PNPM_HOME=/pnpm \ |
| 48 | + PATH="/pnpm:$PATH" |
| 49 | + |
| 50 | +RUN npm install -g pnpm@${PNPM_VERSION} \ |
| 51 | + || (sleep 5 && npm install -g pnpm@${PNPM_VERSION}) |
| 52 | + |
| 53 | +WORKDIR /app |
| 54 | + |
| 55 | +COPY --from=deps /app/node_modules ./node_modules |
| 56 | +COPY . . |
| 57 | + |
| 58 | +RUN pnpm build |
| 59 | + |
| 60 | +# ----------------------------------------------------------------------------- |
| 61 | +# Stage 3: runtime — slim image we actually ship/run |
| 62 | +# ----------------------------------------------------------------------------- |
| 63 | +FROM node:${NODE_VERSION}-bookworm-slim AS runtime |
| 64 | +ARG PNPM_VERSION |
| 65 | + |
| 66 | +ENV PNPM_HOME=/pnpm \ |
| 67 | + PATH="/pnpm:$PATH" \ |
| 68 | + NODE_ENV=production \ |
| 69 | + NPM_CONFIG_UPDATE_NOTIFIER=false \ |
| 70 | + NPM_CONFIG_FUND=false |
| 71 | + |
| 72 | +# Native deps that some npx-resolved MCP servers may pull in (sqlite3, sharp, |
| 73 | +# etc.). Cheap and rare, but keeps `npx -y <server>` from breaking on first call. |
| 74 | +RUN apt-get update \ |
| 75 | + && apt-get install -y --no-install-recommends \ |
| 76 | + ca-certificates \ |
| 77 | + curl \ |
| 78 | + dumb-init \ |
| 79 | + git \ |
| 80 | + && rm -rf /var/lib/apt/lists/* |
| 81 | + |
| 82 | +RUN npm install -g pnpm@${PNPM_VERSION} \ |
| 83 | + || (sleep 5 && npm install -g pnpm@${PNPM_VERSION}) |
| 84 | + |
| 85 | +WORKDIR /app |
| 86 | + |
| 87 | +# Copy built artifacts and the dependency tree from earlier stages. |
| 88 | +COPY --from=deps /app/node_modules ./node_modules |
| 89 | +COPY --from=build /app/dist ./dist |
| 90 | + |
| 91 | +# Source files the bench harness still needs at runtime (it imports from src/ |
| 92 | +# via tsx and uses bench/*.ts directly). |
| 93 | +COPY package.json pnpm-lock.yaml tsconfig.json ./ |
| 94 | +COPY bin ./bin |
| 95 | +COPY src ./src |
| 96 | +COPY bench ./bench |
| 97 | +COPY examples ./examples |
| 98 | +COPY README.md LICENSE ./ |
| 99 | + |
| 100 | +# Warm the npm cache so the five upstream MCP servers don't pay a fresh |
| 101 | +# download on the first proxy/bench run inside a container. |
| 102 | +RUN set -eux; \ |
| 103 | + for pkg in \ |
| 104 | + @modelcontextprotocol/server-everything \ |
| 105 | + @modelcontextprotocol/server-filesystem \ |
| 106 | + @modelcontextprotocol/server-memory \ |
| 107 | + @modelcontextprotocol/server-sequential-thinking \ |
| 108 | + @modelcontextprotocol/server-github ; do \ |
| 109 | + npm pack --silent "$pkg" --pack-destination=/tmp >/dev/null 2>&1 || true; \ |
| 110 | + done; \ |
| 111 | + rm -f /tmp/*.tgz |
| 112 | + |
| 113 | +# Filesystem upstream needs a sandbox dir; the bench config defaults to |
| 114 | +# /app/bench/sandbox via BENCH_FS_ROOT. |
| 115 | +RUN mkdir -p /app/bench/sandbox /app/bench/results /app/.leanmcp |
| 116 | + |
| 117 | +EXPOSE 8799 9464 |
| 118 | + |
| 119 | +# dumb-init reaps zombie children spawned by the upstream stdio servers. |
| 120 | +ENTRYPOINT ["/usr/bin/dumb-init", "--"] |
| 121 | + |
| 122 | +# Default: start the proxy. Override with `pnpm bench [...]` etc. |
| 123 | +CMD ["node", "bin/leanmcp", "start", "--config", "examples/benchmark.docker.config.yaml"] |
0 commit comments