# syntax=docker/dockerfile:1.7
###############################################################################
# ContextForge MCP Gateway (lite) - OCI-compliant container build
#
# This multi-stage Dockerfile produces a minimal runtime image using
# ubi10-minimal as the base, supporting multiplatform builds (amd64, arm64,
# s390x) including QEMU emulation for cross-platform builds.
#
# Key design points:
# - Builder stage has full DNF + devel headers for wheel compilation
# - Runtime stage uses ubi10-minimal for cross-platform compatibility
# - Development headers are dropped from the final image
# - Hadolint DL3041 is suppressed to allow "latest patch" RPM usage
###############################################################################
###########################
# Build-time arguments
###########################
# Python major.minor series to track
ARG PYTHON_VERSION=3.12
ARG ENABLE_RUST=false
###############################################################################
# Rust builder stage - builds Rust plugins in manylinux2014 container
# To build WITH Rust: docker build --build-arg ENABLE_RUST=true -f Containerfile.lite .
# To build WITHOUT Rust (default): docker build -f Containerfile.lite .
###############################################################################
FROM quay.io/pypa/manylinux2014:2025.12.13-1 AS rust-builder-base
ARG ENABLE_RUST
# Set shell with pipefail for safety
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Only build if ENABLE_RUST=true
RUN if [ "$ENABLE_RUST" != "true" ]; then \
echo "⏭️ Rust builds disabled (set --build-arg ENABLE_RUST=true to enable)"; \
mkdir -p /build/plugins_rust/target/wheels; \
exit 0; \
fi
# Install Rust toolchain (only if ENABLE_RUST=true)
RUN if [ "$ENABLE_RUST" = "true" ]; then \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable; \
fi
ENV PATH="/root/.cargo/bin:$PATH"
WORKDIR /build
# Copy only Rust plugin files (only if ENABLE_RUST=true)
COPY plugins_rust/ /build/plugins_rust/
# Switch to Rust plugin directory
WORKDIR /build/plugins_rust
# Build Rust plugins using Python 3.12 from manylinux image (only if ENABLE_RUST=true)
RUN if [ "$ENABLE_RUST" = "true" ]; then \
rm -rf target/wheels && \
/opt/python/cp312-cp312/bin/python -m pip install --upgrade pip maturin && \
/opt/python/cp312-cp312/bin/maturin build --release --compatibility manylinux2014 && \
echo "✅ Rust plugins built successfully"; \
else \
echo "⏭️ Skipping Rust plugin build"; \
fi
FROM rust-builder-base AS rust-builder
###########################
# Builder stage
###########################
FROM registry.access.redhat.com/ubi10/ubi:10.1-1767602397 AS builder
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
ARG PYTHON_VERSION
ARG GRPC_PYTHON_BUILD_SYSTEM_OPENSSL='False'
# ----------------------------------------------------------------------------
# 1) Patch the OS
# 2) Install Python + headers for building wheels
# 3) Install binutils for strip command and curl for downloading CDN assets
# 4) Register python3 alternative
# 5) Clean caches to reduce layer size
# ----------------------------------------------------------------------------
# hadolint ignore=DL3041
RUN set -euo pipefail \
&& dnf upgrade -y \
&& dnf install -y \
python${PYTHON_VERSION} \
python${PYTHON_VERSION}-devel \
binutils openssl-devel gcc postgresql-devel gcc-c++ curl \
&& update-alternatives --install /usr/bin/python3 python3 /usr/bin/python${PYTHON_VERSION} 1 \
&& dnf clean all
WORKDIR /app
# ----------------------------------------------------------------------------
# s390x architecture does not support BoringSSL when building wheel grpcio.
# Force Python whl to use OpenSSL.
# ----------------------------------------------------------------------------
RUN if [ `uname -m` = "s390x" ]; then \
echo "Building for s390x."; \
echo "export GRPC_PYTHON_BUILD_SYSTEM_OPENSSL='True'" > /etc/profile.d/use-openssl.sh; \
else \
echo "export GRPC_PYTHON_BUILD_SYSTEM_OPENSSL='False'" > /etc/profile.d/use-openssl.sh; \
fi
RUN chmod 644 /etc/profile.d/use-openssl.sh
# ----------------------------------------------------------------------------
# Copy only the files needed for dependency installation first
# This maximizes Docker layer caching - dependencies change less often
# ----------------------------------------------------------------------------
COPY pyproject.toml /app/
# ----------------------------------------------------------------------------
# Copy Rust plugin wheels from rust-builder stage (if any exist)
# ----------------------------------------------------------------------------
COPY --from=rust-builder /build/plugins_rust/target/wheels/ /tmp/rust-wheels/
# ----------------------------------------------------------------------------
# Create and populate virtual environment
# - Upgrade pip, setuptools, wheel, pdm, uv
# - Install project dependencies and package
# - Include observability packages for OpenTelemetry support
# - Install Rust plugins from pre-built wheels (if built)
# - Remove build tools but keep runtime dist-info
# - Remove build caches and build artifacts
# ----------------------------------------------------------------------------
ARG ENABLE_RUST=false
RUN set -euo pipefail \
&& . /etc/profile.d/use-openssl.sh \
&& python3 -m venv /app/.venv \
&& /app/.venv/bin/pip install --no-cache-dir --upgrade pip setuptools wheel pdm uv \
&& /app/.venv/bin/uv pip install ".[redis,postgres,mysql,observability,granian]" \
&& if [ "$ENABLE_RUST" = "true" ] && ls /tmp/rust-wheels/*.whl 1> /dev/null 2>&1; then \
echo "🦀 Installing Rust plugins..."; \
/app/.venv/bin/pip install --no-cache-dir /tmp/rust-wheels/mcpgateway_rust-*-manylinux*.whl && \
/app/.venv/bin/python3 -c "from plugins_rust import PIIDetectorRust; print('✓ Rust PII filter installed successfully')"; \
else \
echo "⏭️ Rust plugins not available - using Python implementations"; \
fi \
&& rm -rf /tmp/rust-wheels \
&& /app/.venv/bin/pip uninstall --yes uv pip setuptools wheel pdm \
&& rm -rf /root/.cache /var/cache/dnf \
&& find /app/.venv -name "*.dist-info" -type d \
\( -name "pip-*" -o -name "setuptools-*" -o -name "wheel-*" -o -name "pdm-*" -o -name "uv-*" \) \
-exec rm -rf {} + 2>/dev/null || true \
&& rm -rf /app/.venv/share/python-wheels \
&& rm -rf /app/*.egg-info /app/build /app/dist /app/.eggs
# ----------------------------------------------------------------------------
# Now copy only the application files needed for runtime
# This ensures code changes don't invalidate the dependency layer
# ----------------------------------------------------------------------------
COPY run-gunicorn.sh run-granian.sh docker-entrypoint.sh /app/
COPY mcpgateway/ /app/mcpgateway/
COPY gunicorn.config.py /app/
COPY plugins/ /app/plugins/
COPY mcp-catalog.yml /app/
# Optional: Copy run.sh if it's needed in production
COPY run.sh /app/
# ----------------------------------------------------------------------------
# Download CDN assets for airgapped deployment
# This downloads external CSS/JS dependencies during build
# ----------------------------------------------------------------------------
COPY scripts/download-cdn-assets.sh /tmp/download-cdn-assets.sh
RUN chmod +x /tmp/download-cdn-assets.sh \
&& /tmp/download-cdn-assets.sh \
&& rm /tmp/download-cdn-assets.sh
# ----------------------------------------------------------------------------
# Ensure executable permissions for scripts
# ----------------------------------------------------------------------------
RUN chmod +x /app/run-gunicorn.sh /app/run-granian.sh /app/run.sh
# ----------------------------------------------------------------------------
# Pre-compile Python bytecode with -OO optimization
# - Strips docstrings and assertions
# - Improves startup performance
# - Remove __pycache__ directories after compilation
# ----------------------------------------------------------------------------
RUN python3 -OO -m compileall -q /app/.venv /app/mcpgateway /app/plugins \
&& find /app -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
# ----------------------------------------------------------------------------
# Set ownership for non-root user (1001)
# - OpenShift compatible (accepts any UID in group 0)
# ----------------------------------------------------------------------------
RUN chown -R 1001:0 /app \
&& chmod -R g=u /app
###########################
# Final runtime stage
###########################
# Using ubi10-minimal for cross-platform compatibility (works with QEMU emulation)
# hadolint ignore=DL3006
FROM registry.access.redhat.com/ubi10/ubi-minimal:10.1-1766033715 AS runtime
ARG PYTHON_VERSION=3.12
# ----------------------------------------------------------------------------
# OCI image metadata
# ----------------------------------------------------------------------------
LABEL maintainer="Mihai Criveti" \
org.opencontainers.image.title="mcp/mcpgateway" \
org.opencontainers.image.description="ContextForge MCP Gateway: An enterprise-ready Model Context Protocol Gateway" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.version="1.0.0-BETA-1"
# ----------------------------------------------------------------------------
# Install minimal runtime dependencies
# - Python runtime (no devel packages)
# - ca-certificates for HTTPS
# - procps-ng for process management (ps command)
# ----------------------------------------------------------------------------
# hadolint ignore=DL3041
RUN microdnf install -y --nodocs --setopt=install_weak_deps=0 \
python${PYTHON_VERSION} \
ca-certificates \
procps-ng \
&& microdnf clean all \
&& rm -rf /var/cache/yum
# ----------------------------------------------------------------------------
# Create python3 symlink
# ----------------------------------------------------------------------------
RUN ln -sf /usr/bin/python${PYTHON_VERSION} /usr/bin/python3
# ----------------------------------------------------------------------------
# Create non-root user
# ----------------------------------------------------------------------------
RUN echo 'app:x:1001:0:app:/app:/sbin/nologin' >> /etc/passwd
# ----------------------------------------------------------------------------
# Copy the application from the builder stage
# ----------------------------------------------------------------------------
COPY --from=builder --chown=1001:0 /app /app
# ----------------------------------------------------------------------------
# Ensure our virtual environment binaries have priority in PATH
# - Don't write bytecode files (we pre-compiled with -OO)
# - Unbuffered output for better logging
# - Random hash seed for security
# - Disable pip cache to save space
# - Disable pip version check to reduce startup time
# ----------------------------------------------------------------------------
ENV PATH="/app/.venv/bin:${PATH}" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# ----------------------------------------------------------------------------
# Application working directory
# ----------------------------------------------------------------------------
WORKDIR /app
# ----------------------------------------------------------------------------
# Expose application port
# ----------------------------------------------------------------------------
EXPOSE 4444
# ----------------------------------------------------------------------------
# Run as non-root user (1001)
# ----------------------------------------------------------------------------
USER 1001
# ----------------------------------------------------------------------------
# Health check
# ----------------------------------------------------------------------------
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD ["python3", "-c", "import httpx,sys;sys.exit(0 if httpx.get('http://localhost:4444/health',timeout=5).status_code==200 else 1)"]
# ----------------------------------------------------------------------------
# Entrypoint
# ----------------------------------------------------------------------------
# HTTP server selection via HTTP_SERVER environment variable:
# - gunicorn : Python-based with Uvicorn workers (default)
# - granian : Rust-based HTTP server (alternative)
#
# Examples:
# docker run -e HTTP_SERVER=gunicorn mcpgateway # Default
# docker run -e HTTP_SERVER=granian mcpgateway # Alternative
ENV HTTP_SERVER=gunicorn
CMD ["./docker-entrypoint.sh"]