|
1 | | -# Placeholder. Snyk's PR check only tests files already imported as projects from main, so |
2 | | -# a Dockerfile has to exist here before the real one can be gated. Replaced by the image in |
3 | | -# the follow-up PR for OPS-4767, which uses this same base. |
4 | | -FROM python:3.13-slim-bookworm |
| 1 | +# syntax=docker/dockerfile:1 |
| 2 | +# |
| 3 | +# The MCP server as its own container, serving the http transport for external clients. |
| 4 | +# |
| 5 | +# The stdio transport is not what this image is for: the API image vendors this repository |
| 6 | +# and spawns it per chat request. This image runs alongside the API as a separate service, |
| 7 | +# reached by agents at ${OPENOPS_MCP_RESOURCE_URL} and reaching the API at ${OPENOPS_API_URL}. |
| 8 | +# |
| 9 | +# Two stages so the runtime never carries uv, the lockfile, or a package cache. The |
| 10 | +# environment is resolved from uv.lock exactly as CI does (--frozen), so the image cannot |
| 11 | +# quietly drift from what was tested. |
| 12 | + |
| 13 | +ARG PYTHON_VERSION=3.13 |
| 14 | + |
| 15 | +# ---- Builder stage: resolve the environment from the lockfile ---- |
| 16 | +FROM python:${PYTHON_VERSION}-alpine AS builder |
| 17 | + |
| 18 | +COPY --from=ghcr.io/astral-sh/uv:0.9.0 /uv /usr/local/bin/uv |
| 19 | + |
| 20 | +ENV UV_COMPILE_BYTECODE=1 \ |
| 21 | + UV_LINK_MODE=copy \ |
| 22 | + UV_PYTHON_DOWNLOADS=never |
| 23 | + |
| 24 | +WORKDIR /app |
| 25 | + |
| 26 | +# Only the manifests: the environment layer stays cached across source-only changes. |
| 27 | +COPY pyproject.toml uv.lock ./ |
| 28 | + |
| 29 | +# --no-install-project: the server runs from source via main.py, the same way the API |
| 30 | +# image runs it, so the package itself is not installed into the environment. |
| 31 | +RUN --mount=type=cache,target=/root/.cache/uv \ |
| 32 | + uv sync --frozen --no-dev --no-install-project |
| 33 | + |
| 34 | +# ---- Final stage: runtime only ---- |
| 35 | +FROM python:${PYTHON_VERSION}-alpine |
| 36 | + |
| 37 | +ENV LANG=C.UTF-8 \ |
| 38 | + LC_ALL=C.UTF-8 \ |
| 39 | + PYTHONUNBUFFERED=1 \ |
| 40 | + PYTHONDONTWRITEBYTECODE=1 \ |
| 41 | + PATH="/app/.venv/bin:$PATH" \ |
| 42 | + # The container is the http deployment. stdio is the API's business. |
| 43 | + MCP_TRANSPORT=http \ |
| 44 | + MCP_HTTP_HOST=0.0.0.0 \ |
| 45 | + MCP_HTTP_PORT=3020 \ |
| 46 | + # No outbound call to pypi.org at startup, and no ASCII banner in collected logs. |
| 47 | + FASTMCP_CHECK_FOR_UPDATES=off \ |
| 48 | + FASTMCP_SHOW_SERVER_BANNER=false |
| 49 | + |
| 50 | +# No shell login, no home directory: the process needs neither. |
| 51 | +RUN addgroup -S -g 10001 openops \ |
| 52 | + && adduser -S -u 10001 -G openops -H -s /sbin/nologin openops |
| 53 | + |
| 54 | +WORKDIR /app |
| 55 | + |
| 56 | +COPY --from=builder --chown=openops:openops /app/.venv ./.venv |
| 57 | +COPY --chown=openops:openops main.py ./ |
| 58 | +COPY --chown=openops:openops openops_mcp ./openops_mcp |
| 59 | + |
| 60 | +ARG VERSION=unknown |
| 61 | +ENV OPENOPS_MCP_VERSION=$VERSION |
| 62 | + |
| 63 | +LABEL service=openops-mcp \ |
| 64 | + org.opencontainers.image.title="OpenOps MCP" \ |
| 65 | + org.opencontainers.image.description="MCP server exposing OpenOps API operations as tools" \ |
| 66 | + org.opencontainers.image.source="https://github.com/openops-cloud/openops-mcp" \ |
| 67 | + org.opencontainers.image.version=$VERSION |
| 68 | + |
| 69 | +USER openops |
| 70 | + |
| 71 | +EXPOSE 3020 |
| 72 | + |
| 73 | +ENTRYPOINT ["python", "main.py"] |
0 commit comments