ARG BASE_IMAGE=python:3.13-alpine
ARG BUILDER_IMAGE=${BASE_IMAGE}
ARG PRODUCTION_IMAGE=${BASE_IMAGE}
# --- Stage 1: Build viewer-mcp frontend (Svelte/Vite) ---
FROM node:22-alpine AS frontend-builder
WORKDIR /app
COPY packages/viewer-mcp/package*.json ./
RUN npm ci --omit=dev
COPY packages/viewer-mcp/tsconfig.json packages/viewer-mcp/vite.config.ts packages/viewer-mcp/mcp-app.html ./
COPY packages/viewer-mcp/ui ./ui
RUN npm run build
# --- Stage 2: Build Python workspace with uv ---
FROM ${BUILDER_IMAGE} AS builder
COPY --from=ghcr.io/astral-sh/uv:0.10.4-alpine@sha256:40b4b624e6f8e674a038507efbbaa97f7535536808e75c8e3161602dc2ac8024 /uv /uvx /usr/local/bin/
ENV UV_COMPILE_BYTECODE=1
WORKDIR /app
# Copy workspace configuration and all packages
COPY pyproject.toml uv.lock ./
COPY packages/ ./packages/
COPY src/ ./src/
COPY README.md LICENSE ./
# Copy built frontend into viewer-mcp package before uv sync
# vite outputs to src/ra_mcp_viewer_mcp/dist/, so --no-editable will include it in the wheel
COPY --from=frontend-builder /app/src/ra_mcp_viewer_mcp/dist/ ./packages/viewer-mcp/src/ra_mcp_viewer_mcp/dist/
# Sync workspace packages (--no-editable makes .venv self-contained)
RUN uv sync --frozen --no-cache --no-dev --no-editable
# --- Stage 3: Production runtime ---
FROM ${PRODUCTION_IMAGE} AS production
# Install runtime dependencies based on base image
# Alpine uses apk, Wolfi/Chainguard use apk, Debian uses apt
RUN if command -v apk >/dev/null 2>&1; then \
apk upgrade --no-cache && \
apk add --no-cache ca-certificates libgcc; \
elif command -v apt-get >/dev/null 2>&1; then \
apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*; \
fi
# Remove pip and setuptools to eliminate CVE-2025-8869 and CVE-2026-1703
# We use uv for all package management, so pip is not needed at runtime
# Use rm -rf directly instead of invoking pip to avoid executing a potentially vulnerable binary
RUN rm -rf /usr/local/lib/python3.13/site-packages/pip* \
/usr/local/lib/python3.13/site-packages/setuptools* \
/usr/local/lib/python3.13/site-packages/wheel* \
/usr/local/bin/pip* 2>/dev/null || true
# Create non-root user for security
RUN addgroup -g 1000 ra-mcp && \
adduser -u 1000 -G ra-mcp -s /bin/sh -D ra-mcp
WORKDIR /app
# Copy only what's needed at runtime (--chown avoids a separate chown layer)
# With --no-editable, .venv is self-contained — no need to copy src/ or packages/
COPY --from=builder --chown=ra-mcp:ra-mcp /app/.venv /app/.venv
COPY --chown=ra-mcp:ra-mcp docs/assets/index.html ./assets/index.html
COPY --chown=ra-mcp:ra-mcp packages/guide-mcp/resources/ ./resources/
COPY --chown=ra-mcp:ra-mcp plugins/ ./plugins/
RUN mkdir -p /app/data && chown ra-mcp:ra-mcp /app /app/data
USER ra-mcp
ENV PATH="/app/.venv/bin:$PATH"
ENV GRADIO_SERVER_NAME="0.0.0.0"
# Health check via /health endpoint
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "from urllib.request import urlopen; urlopen('http://localhost:7860/health')" || exit 1
EXPOSE 7860
CMD ["ra-serve", "--http", "--host", "0.0.0.0", "--port", "7860"]