diff --git a/Dockerfile.gpu b/Dockerfile.gpu index 2c59a74..5c8358d 100644 --- a/Dockerfile.gpu +++ b/Dockerfile.gpu @@ -2,11 +2,14 @@ # MemPalace — GPU (NVIDIA CUDA) image. # -# The `gpu` extra pulls onnxruntime-gpu, which needs CUDA + cuDNN shared -# libraries at runtime, so this variant builds on an nvidia/cuda base instead -# of python:slim. It is single-stage on purpose: the venv is created with a -# uv-managed interpreter, and copying that interpreter cleanly across build -# stages is more trouble than the size saving is worth for a GPU image. +# Multi-stage build using uv (the project ships a uv.lock, so we install from +# the frozen lockfile for reproducible images). The `gpu` extra pulls +# onnxruntime-gpu, which needs CUDA + cuDNN shared libraries at runtime, so +# this variant builds on an nvidia/cuda base instead of python:slim. +# +# The builder stage is dropped from the final image, so build tools +# (compilers, apt cache) never reach production — only the uv-managed +# interpreter and the resolved virtualenv do. # # Build: # docker build -f Dockerfile.gpu -t mempalace:gpu . @@ -22,51 +25,83 @@ # rebuild. ARG CUDA_IMAGE=nvidia/cuda:12.6.3-cudnn-runtime-ubuntu22.04 -FROM ${CUDA_IMAGE} + +# --- builder ---------------------------------------------------------------- +FROM ${CUDA_IMAGE} AS builder + +COPY --from=ghcr.io/astral-sh/uv:0.5 /uv /uvx /bin/ + +# Minimal toolchain for any source-built wheels; dropped before the runtime +# stage so compilers never ship in the production image. +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/* + +ARG PYTHON_VERSION=3.12 +ARG EXTRAS="extract,spellcheck,gpu" + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_INSTALL_DIR=/opt/uv/python \ + UV_PYTHON_PREFERENCE=only-managed + +WORKDIR /app + +# Layer 1: dependencies only (no project source). Bind-mounted files keep the +# project tree out of this layer, so changing source code does not bust the +# deps cache. The uv-managed interpreter is also installed here, into a +# stable path that the runtime stage can copy verbatim. +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=README.md,target=README.md \ + set -e; \ + uv python install ${PYTHON_VERSION}; \ + flags=""; \ + for e in $(echo "${EXTRAS}" | tr ',' ' '); do flags="${flags} --extra ${e}"; done; \ + uv sync --frozen --no-install-project --no-dev --python ${PYTHON_VERSION} ${flags} + +# Layer 2: the project itself. --no-editable installs mempalace into the +# venv's site-packages (instead of an .pth pointing at /app), so the runtime +# stage can copy only /app/.venv and drop the source tree. +COPY . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + set -e; \ + flags=""; \ + for e in $(echo "${EXTRAS}" | tr ',' ' '); do flags="${flags} --extra ${e}"; done; \ + uv sync --frozen --no-dev --no-editable ${flags} + +# --- runtime ---------------------------------------------------------------- +FROM ${CUDA_IMAGE} AS runtime LABEL org.opencontainers.image.title="MemPalace (GPU)" \ org.opencontainers.image.description="Local-first AI memory with CUDA-accelerated embeddings." \ org.opencontainers.image.source="https://github.com/MemPalace/mempalace" \ org.opencontainers.image.licenses="MIT" -COPY --from=ghcr.io/astral-sh/uv:0.5 /uv /uvx /bin/ - -# Minimal toolchain for any source-built wheels; ca-certificates for the -# lazy HuggingFace model download on first use. +# ca-certificates only — needed for the lazy HuggingFace model download on +# first use. No build toolchain in this stage. RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential ca-certificates \ + && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* -ARG PYTHON_VERSION=3.12 -ARG EXTRAS="extract,spellcheck,gpu" - ENV HOME=/data \ PATH="/app/.venv/bin:${PATH}" \ - UV_COMPILE_BYTECODE=1 \ - UV_LINK_MODE=copy \ - UV_PYTHON_INSTALL_DIR=/opt/uv/python \ - UV_PYTHON_PREFERENCE=only-managed \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ MEMPALACE_EMBEDDING_DEVICE=cuda +# Non-root user owning the data volume. +RUN groupadd --gid 1000 mempalace \ + && useradd --uid 1000 --gid 1000 --home-dir /data --create-home mempalace + WORKDIR /app -# uv-managed interpreter pinned to PYTHON_VERSION, installed to a stable path -# so the venv stays valid at runtime. -RUN uv python install ${PYTHON_VERSION} - -COPY . /app -RUN --mount=type=cache,target=/root/.cache/uv \ - set -e; \ - flags=""; \ - for e in $(echo "${EXTRAS}" | tr ',' ' '); do flags="${flags} --extra ${e}"; done; \ - uv sync --frozen --no-dev --no-editable --python ${PYTHON_VERSION} ${flags} - -RUN groupadd --gid 1000 mempalace \ - && useradd --uid 1000 --gid 1000 --home-dir /data --create-home mempalace \ - && chown -R mempalace:mempalace /app /data - +# Bring the uv-managed interpreter and the resolved venv across the stage +# boundary. /opt/uv/python must be copied alongside .venv: the venv's +# shebangs and binary launcher reference it. +COPY --from=builder /opt/uv/python /opt/uv/python +COPY --from=builder --chown=mempalace:mempalace /app/.venv /app/.venv COPY --chown=mempalace:mempalace docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh diff --git a/README.md b/README.md index ad8efbb..5259b11 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,8 @@ docker build -t mempalace . # MCP server over stdio — note the `-i` flag (JSON-RPC needs stdin) docker run -i --rm -v mempalace-data:/data mempalace -# Run any CLI command instead -docker run --rm -v mempalace-data:/data mempalace mine /work +# Run any CLI command instead (mount the host directory you want to mine) +docker run --rm -v mempalace-data:/data -v /path/to/project:/work mempalace mine /work docker run --rm -v mempalace-data:/data mempalace search "why GraphQL" ``` diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 79102fd..7c4601f 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -13,11 +13,15 @@ set -e case "${1:-mcp}" in mcp) - shift 2>/dev/null || true + if [ "$#" -gt 0 ]; then + shift + fi exec mempalace-mcp "$@" ;; cli) - shift + if [ "$#" -gt 0 ]; then + shift + fi exec mempalace "$@" ;; *)