# syntax=docker/dockerfile:1 # ---- Build stage ------------------------------------------------------------- # Python 3.14 / Alpine 3.24 base. uv is copied from its official image because # there is no combined python3.14-alpine3.24 uv image tag. FROM python:3.14-alpine3.24 AS builder COPY --from=ghcr.io/astral-sh/uv:0.13.1 /uv /uvx /bin/ # setuptools-scm derives the package version from git history and tags. RUN apk add --no-cache git ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy \ UV_PYTHON_DOWNLOADS=never WORKDIR /app # Dependencies first: this layer only rebuilds when pyproject.toml/uv.lock change. COPY pyproject.toml uv.lock .python-version ./ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --no-install-project # Then install the project itself. The full checkout (including .git) is # copied so setuptools-scm sees a clean worktree and derives the version # from the closest release tag. COPY . . RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --no-editable # ---- Runtime stage ----------------------------------------------------------- FROM python:3.14-alpine3.24 AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PATH="/app/.venv/bin:$PATH" \ MCP_TRANSPORT=http \ MCP_HOST=0.0.0.0 \ MCP_PORT=8000 RUN addgroup -S -g 1001 xmcp \ && adduser -S -D -H -u 1001 -G xmcp xmcp WORKDIR /app COPY --from=builder --chown=xmcp:xmcp /app/.venv /app/.venv USER xmcp EXPOSE 8000 # In HTTP mode, probe the MCP port; stdio containers are always healthy. HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD ["python", "-c", "import os, socket, sys\nif os.environ.get('MCP_TRANSPORT', 'stdio') != 'http':\n sys.exit(0)\ns = socket.socket()\ns.settimeout(3)\nsys.exit(s.connect_ex(('127.0.0.1', int(os.environ.get('MCP_PORT', '8000')))))"] CMD ["python", "-m", "xmcp"]