-
Notifications
You must be signed in to change notification settings - Fork 656
Expand file tree
/
Copy pathContainerfile.lite
More file actions
441 lines (395 loc) · 20.6 KB
/
Containerfile.lite
File metadata and controls
441 lines (395 loc) · 20.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# syntax=docker/dockerfile:1.7
###############################################################################
# ContextForge (lite) - OCI-compliant multi-stage container build
#
# Purpose: Minimal runtime image using ubi10-minimal, supporting multiplatform
# builds (amd64, arm64, s390x, ppc64le) with optional Rust native extensions.
#
# Distinction from Containerfile (standard):
# - This is the RECOMMENDED production build (lighter, faster, ubi10-minimal).
# - Containerfile (standard) is a simpler single-stage variant without Rust.
# - Containerfile.scratch is an ultra-slim scratch-based image.
#
# Key design points:
# - Builder stage has full DNF + devel headers for wheel compilation
# - Runtime stage uses ubi10-minimal for cross-platform compatibility
# - Optional Rust builder stage for native extensions (ENABLE_RUST=true)
# - 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
ARG ENABLE_RUST_MCP_RMCP=false
# Enable profiling tools (memray, py-spy) - off by default for smaller images
# To enable: docker build --build-arg ENABLE_PROFILING=true -f Containerfile.lite .
# Usage after enabling:
# memray: docker exec -it <container> memray attach <PID> -o /tmp/profile.bin
# py-spy: sudo py-spy top --pid $(docker inspect --format '{{.State.Pid}}' <container>)
# Note: Container must have SYS_PTRACE capability for attaching to processes
# See docs/docs/development/profiling.md for detailed usage
ARG ENABLE_PROFILING=false
###############################################################################
# Rust builder stage - builds Rust MCP runtime and local native extensions
# 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 registry.access.redhat.com/ubi10/ubi:10.1-1776834862 AS rust-builder
ARG PYTHON_VERSION=3.12
ARG ENABLE_RUST
ARG ENABLE_RUST_MCP_RMCP
# 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/native-extension-wheels /build/target/release; \
printf '#!/usr/bin/env sh\n' > /build/target/release/contextforge-mcp-runtime; \
printf 'echo "Rust MCP runtime not built into this image. Rebuild with --build-arg ENABLE_RUST=true." >&2\n' >> /build/target/release/contextforge-mcp-runtime; \
printf 'exit 1\n' >> /build/target/release/contextforge-mcp-runtime; \
chmod +x /build/target/release/contextforge-mcp-runtime; \
printf '#!/usr/bin/env sh\n' > /build/target/release/contextforge-a2a-runtime; \
printf 'echo "Rust A2A runtime not built into this image. Rebuild with --build-arg ENABLE_RUST=true." >&2\n' >> /build/target/release/contextforge-a2a-runtime; \
printf 'exit 1\n' >> /build/target/release/contextforge-a2a-runtime; \
chmod +x /build/target/release/contextforge-a2a-runtime; \
exit 0; \
fi
# Install system deps + Rust toolchain in a single layer (only if ENABLE_RUST=true)
# hadolint ignore=DL3041
RUN if [ "$ENABLE_RUST" = "true" ]; then \
dnf upgrade -y && \
dnf install -y \
python${PYTHON_VERSION} \
python${PYTHON_VERSION}-devel \
python${PYTHON_VERSION}-pip \
gcc \
gcc-c++ \
openssl-devel \
postgresql-devel \
libpq-devel \
findutils \
curl && \
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python${PYTHON_VERSION} 1 && \
dnf clean all && \
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 workspace and crates (only if ENABLE_RUST=true)
COPY Cargo.toml Cargo.lock /build/
COPY crates/ /build/crates/
# Build local native extensions from maturin crates under crates/
# hadolint ignore=DL3013
RUN if [ "$ENABLE_RUST" = "true" ]; then \
mkdir -p /build/native-extension-wheels && \
python3 -m pip install --no-cache-dir --upgrade pip "maturin==1.12.6" && \
printf '%s\n' \
'import pathlib' \
'import subprocess' \
'import sys' \
'import tomllib' \
'' \
'crates_root = pathlib.Path("/build/crates")' \
'wheel_dir = pathlib.Path("/build/native-extension-wheels")' \
'for cargo_toml in sorted(crates_root.rglob("Cargo.toml")):' \
' pyproject_toml = cargo_toml.with_name("pyproject.toml")' \
' if not pyproject_toml.exists():' \
' continue' \
' pyproject = tomllib.loads(pyproject_toml.read_text(encoding="utf-8"))' \
' build_system = pyproject.get("build-system", {})' \
' backend = str(build_system.get("build-backend", ""))' \
' requires = [str(item) for item in build_system.get("requires", [])]' \
' if "maturin" not in backend and not any("maturin" in item for item in requires):' \
' continue' \
' crate_dir = cargo_toml.parent' \
' print(f"🦀 Building local native extension: {crate_dir.name}")' \
' subprocess.run([sys.executable, "-m", "maturin", "build", "--release", "--manifest-path", str(cargo_toml), "--out", str(wheel_dir)], check=True)' \
'print("✅ Local native extensions built successfully")' \
> /tmp/build_local_native_extensions.py && \
python3 /tmp/build_local_native_extensions.py && \
rm -f /tmp/build_local_native_extensions.py \
else \
echo "⏭️ Skipping local native extension build"; \
fi
# Build MCP + A2A runtime binaries (MCP + A2A in the same RUN share /build/target)
RUN if [ "$ENABLE_RUST" = "true" ]; then \
if [ "$ENABLE_RUST_MCP_RMCP" = "true" ]; then \
cargo build --release -p contextforge_mcp_runtime --features rmcp-upstream-client; \
else \
cargo build --release -p contextforge_mcp_runtime; \
fi && \
cp /build/target/release/contextforge_mcp_runtime /build/target/release/contextforge-mcp-runtime && \
cargo build --release -p contextforge_a2a_runtime && \
echo "✅ Rust MCP + A2A runtimes built successfully"; \
else \
echo "⏭️ Skipping Rust MCP + A2A runtime builds"; \
fi
###############################################################################
# Node.js builder stage - builds Tailwind CSS
###############################################################################
# Use official Red Hat UBI10 Node.js 24 image
FROM registry.access.redhat.com/ubi10/nodejs-24:10.1-1771303073 AS node-builder
USER root
RUN mkdir -p /build && chown 1001:0 /build && chmod g=u /build
USER 1001
WORKDIR /build
# Copy only files needed for CSS build (with proper ownership for non-root user)
COPY --chown=1001:1001 package.json package-lock.json* ./
COPY --chown=1001:1001 tailwind.config.js postcss.config.js ./
COPY --chown=1001:1001 mcpgateway/templates/ ./mcpgateway/templates/
COPY --chown=1001:1001 mcpgateway/static/ ./mcpgateway/static/
# Install dependencies and build CSS
RUN npm ci && \
npm run build:css && \
echo "✅ Tailwind CSS built successfully"
###########################
# Frontend builder stage
###########################
FROM registry.access.redhat.com/ubi10/nodejs-24:10.1-1777957922 AS frontend-builder
WORKDIR /opt/app-root/src
# Copy package.json and package-lock.json
COPY --chown=1001:0 package.json package-lock.json ./
# Install frontend dependencies
RUN npm ci
# Create directory structure with correct ownership before Vite build
USER root
RUN mkdir -p mcpgateway/static && chown -R 1001:0 mcpgateway
USER 1001
# Copy frontend source files
COPY --chown=1001:0 mcpgateway/admin_ui/ mcpgateway/admin_ui/
COPY --chown=1001:0 vite.config.js ./
# Run Vite build
RUN npm run vite:build
###########################
# Builder stage
###########################
FROM registry.access.redhat.com/ubi10/ubi:10.1-1776834862 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 libpq-devel \
&& 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.
# NOTE: ppc64le has the same OpenSSL requirement
# ----------------------------------------------------------------------------
RUN if [ "$(uname -m)" = "s390x" ] || [ "$(uname -m)" = "ppc64le" ]; then \
echo "Building for $(uname -m)."; \
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 \
&& chmod 644 /etc/profile.d/use-openssl.sh
# ----------------------------------------------------------------------------
# Copy only the files needed for dependency installation first.
# Order is top-to-bottom from least-frequently-changing to most:
# 1. pyproject.toml (dependency manifest — plugins come from the [plugins]
# extra, resolved from PyPI)
# 2. Native extension wheels from rust-builder (consumed by venv install)
# Everything else (rust binaries, frontend static, app code) is copied AFTER
# the heavy venv install so those changes don't invalidate the dep layer.
# ----------------------------------------------------------------------------
COPY pyproject.toml /app/
COPY --from=rust-builder /build/native-extension-wheels/ /tmp/local-native-extension-wheels/
COPY --chmod=0755 scripts/verify-native-extensions.py /tmp/verify-native-extensions.py
# ----------------------------------------------------------------------------
# Create and populate virtual environment
# - Upgrade pip, setuptools, wheel, pdm, uv
# - Install project dependencies and package
# - Include observability packages for OpenTelemetry support
# - Install plugins from PyPI (cpex-* packages)
# - Install local native extensions from pre-built wheels (if built)
# - Optionally install profiling tools (memray, py-spy) if ENABLE_PROFILING=true
# - Remove build tools but keep runtime dist-info
# - Remove build caches and build artifacts
# ----------------------------------------------------------------------------
ARG ENABLE_RUST=false
ARG ENABLE_RUST_MCP_RMCP=false
ARG ENABLE_PROFILING=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 \
&& if [ "$(uname -m)" = "s390x" ]; then \
echo "📦 Installing dependencies for s390x architecture..."; \
/app/.venv/bin/pip install --no-cache-dir ".[redis,observability,granian,plugins]" \
&& /app/.venv/bin/pip install --no-cache-dir "psycopg[c]>=3.3.3"; \
else \
/app/.venv/bin/uv pip install ".[redis,postgres,observability,granian,plugins]"; \
fi \
&& echo "✅ Plugins installed from PyPI via [plugins] extra" \
&& if [ "$ENABLE_RUST" = "true" ] && ls "/tmp/local-native-extension-wheels/"*.whl 1> /dev/null 2>&1; then \
echo "🦀 Installing local native extensions..."; \
/app/.venv/bin/pip install --no-cache-dir "/tmp/local-native-extension-wheels/"*.whl && \
/app/.venv/bin/python3 /tmp/verify-native-extensions.py && rm /tmp/verify-native-extensions.py; \
else \
echo "⏭️ No local native extensions discovered"; \
fi \
&& rm -rf /tmp/local-native-extension-wheels \
&& if [ "$ENABLE_PROFILING" = "true" ]; then \
echo "📊 Installing profiling tools (memray, py-spy)..."; \
/app/.venv/bin/pip install --no-cache-dir "memray>=1.17.0" && \
/app/.venv/bin/python3 -c "import memray; print('✓ memray installed successfully')"; \
else \
echo "⏭️ Profiling tools disabled (set --build-arg ENABLE_PROFILING=true to enable)"; \
fi \
&& /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
# ----------------------------------------------------------------------------
# Copy Rust runtime binaries and frontend static output from their builder
# stages AFTER the venv install so those changes don't invalidate the heavy
# dependency layer above.
# ----------------------------------------------------------------------------
COPY --from=rust-builder /build/target/release/contextforge-mcp-runtime /app/bin/contextforge-mcp-runtime
COPY --from=rust-builder /build/target/release/contextforge-a2a-runtime /app/bin/contextforge-a2a-runtime
COPY --from=frontend-builder /opt/app-root/src/mcpgateway/static/ /app/mcpgateway/static/
# Copy pre-built Tailwind CSS from node-builder
COPY --from=node-builder /build/mcpgateway/static/css/tailwind.min.css /app/mcpgateway/static/css/
# Optional: Copy run.sh if it's needed in production
COPY run.sh /app/
# ----------------------------------------------------------------------------
# Download CDN assets for airgapped deployment (writes into /app/mcpgateway/static/vendor/).
# Placed here so app code changes below don't invalidate this network-bound step.
# --chmod=0755 lets us skip a follow-up chmod layer.
# ----------------------------------------------------------------------------
COPY --chmod=0755 scripts/download-cdn-assets.sh /tmp/download-cdn-assets.sh
RUN /tmp/download-cdn-assets.sh \
&& rm /tmp/download-cdn-assets.sh
# ----------------------------------------------------------------------------
# Application files, ordered from least- to most-frequently-changing so that
# the more volatile COPYs at the bottom don't invalidate the stable ones above.
# - Run/entrypoint scripts (rarely change) [--chmod=0755 keeps +x]
# - gunicorn config + mcp catalog (rare/occasional)
# - mcpgateway/ + plugins/ (change most often)
# ----------------------------------------------------------------------------
COPY --chmod=0755 run-gunicorn.sh run-granian.sh docker-entrypoint.sh run.sh /app/
COPY gunicorn.config.py mcp-catalog.yml /app/
COPY mcpgateway/ /app/mcpgateway/
COPY plugins/ /app/plugins/
# ----------------------------------------------------------------------------
# Final pass: compile bytecode with -OO, strip caches, fix OpenShift-compatible
# ownership. Merged into one layer — all of these are cheap and all get
# invalidated whenever any app code changes above.
# ----------------------------------------------------------------------------
RUN python3 -OO -m compileall -x 'cpex/templates' -q /app/.venv /app/mcpgateway /app/plugins \
&& find /app -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true \
&& chown -R 1001:0 /app \
&& chmod -R g=u /app
###########################
# Final runtime stage
###########################
# Using ubi10-minimal for cross-platform compatibility
# hadolint ignore=DL3006
FROM registry.access.redhat.com/ubi10/ubi-minimal:10.1-1776834797 AS runtime
ARG PYTHON_VERSION=3.12
ARG ENABLE_RUST=false
ARG ENABLE_RUST_MCP_RMCP=false
ARG ENABLE_PROFILING=false
# ----------------------------------------------------------------------------
# OCI image metadata
# ----------------------------------------------------------------------------
LABEL maintainer="Mihai Criveti" \
org.opencontainers.image.title="mcp/mcpgateway" \
org.opencontainers.image.description="ContextForge: An enterprise-ready Model Context Protocol Gateway" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.version="1.0.0"
# ----------------------------------------------------------------------------
# Install minimal runtime dependencies
# - Python runtime (no devel packages)
# - ca-certificates for HTTPS
# - procps-ng for process management (ps command)
# - shadow-utils for useradd command
# - gdb for memray attach (only when ENABLE_PROFILING=true)
# ----------------------------------------------------------------------------
# Install runtime deps, conditionally add gdb for profiling, create the python3
# symlink and non-root user — all merged into a single layer since these are
# stable setup steps that rarely change.
# hadolint ignore=DL3041
RUN microdnf upgrade -y --nodocs --setopt=install_weak_deps=0 \
&& microdnf install -y --nodocs --setopt=install_weak_deps=0 \
python${PYTHON_VERSION} \
ca-certificates \
procps-ng \
shadow-utils \
libpq \
&& if [ "$ENABLE_PROFILING" = "true" ]; then \
microdnf install -y --nodocs --setopt=install_weak_deps=0 gdb; \
fi \
&& microdnf clean all \
&& rm -rf /var/cache/yum \
&& ln -sf /usr/bin/python${PYTHON_VERSION} /usr/bin/python3 \
&& useradd --uid 1001 --gid 0 --home-dir /app --shell /sbin/nologin --no-create-home --comment app app
# ----------------------------------------------------------------------------
# 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}" \
PYTHONPATH="/app" \
CONTEXTFORGE_ENABLE_RUST_BUILD=${ENABLE_RUST} \
CONTEXTFORGE_ENABLE_RUST_MCP_RMCP_BUILD=${ENABLE_RUST_MCP_RMCP} \
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"]