-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
168 lines (149 loc) · 7.52 KB
/
Copy pathDockerfile
File metadata and controls
168 lines (149 loc) · 7.52 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
# syntax=docker/dockerfile:1.6
#
# Couchbase Admin MCP Server — container image
#
# Build:
# docker build -t couchbase-ecosystem/couchbase-admin-mcp:0.1.0 -t couchbase-ecosystem/couchbase-admin-mcp:latest .
#
# Run (default: stdio — for Claude Desktop and local MCP clients that launch the
# container per session and speak over stdin/stdout):
# docker run -i --rm \
# --network your_docker_network \
# -e CB_CONNECTION_STRING="couchbase://couchbase-server" \
# -e CB_USERNAME="user" -e CB_PASSWORD="pass" \
# couchbase-ecosystem/couchbase-admin-mcp:latest
# # (--network lets the container resolve the couchbase service name)
#
# Run (HTTP transport — opt in for a long-running networked service that other
# containers / agents connect to; stdio cannot cross a container boundary):
# docker run -d --rm --name couchbase-admin-mcp \
# -p 8000:8000 \
# -e CB_ADMIN_TRANSPORT=http \
# -e CB_ADMIN_HOST=0.0.0.0 \
# -e CB_CONNECTION_STRING="couchbase://couchbase-server" \
# -e CB_USERNAME="user" -e CB_PASSWORD="pass" \
# couchbase-ecosystem/couchbase-admin-mcp:latest
# # MCP client connects to http://<host>:8000/mcp
#
# Connecting to a Couchbase cluster in ANOTHER container: put both on the same
# Docker network and point CB_CONNECTION_STRING at the cluster's service name,
# e.g. CB_CONNECTION_STRING="couchbase://couchbase-server". See README
# ("Connecting from Claude Desktop" and "Running in Docker").
#
# Note: CB_BUCKET is optional for the admin server — admin operations act at the
# cluster level. It only affects the SDK warm-up used by a few diagnostics tools.
# ── Stage 1: build dependencies ───────────────────────────────────────────────
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /build
# Build deps for couchbase SDK C extension
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
cmake \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
COPY pyproject.toml ./
# Install runtime deps + the HTTP transport extras.
# NOTE: the HTTP transport uses StreamableHTTPServerTransport, whose API has
# varied across mcp releases. This code was validated against mcp 1.28.1. If the
# HTTP transport fails to start after a rebuild, pin mcp to a known-good version
# here (e.g. "mcp==1.28.1") rather than the open ">=1.0.0" range.
RUN pip install --prefix=/install \
# Same bound as pyproject.toml. The Tool-model differences are handled by
# mcp_compat.py, but 2.x removed the Server decorator registry and request_ctx, on
# which the HTTP authorization path depends. See pyproject.toml for the detail.
"mcp>=1.10,<2.0" \
"couchbase>=4.4.0,<5.0.0" \
"uvicorn>=0.27" \
"starlette>=0.35" \
"PyJWT[crypto]>=2.8.0" \
"cryptography>=44.0.1" \
"requests>=2.32.4" \
# The image COPIES gui/ but did not install its dependencies, so the admin
# console could not start there — the packaging test passed on the COPY line
# alone. Shipping the code without the runtime is worse than shipping neither.
"flask>=3.0" \
"flask-cors>=6.0.0"
# ── Stage 2: runtime image ────────────────────────────────────────────────────
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/usr/local/bin:$PATH"
# Runtime-only OS dependencies (TLS, libc) — no compilers
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# Non-root user for security
RUN groupadd --system --gid 1000 mcp \
&& useradd --system --uid 1000 --gid mcp --home /app --shell /sbin/nologin mcp
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /install /usr/local
# Copy application code.
#
# EVERY top-level module server.py imports must be here. This list omitted
# audit.py, authz.py and profile_config.py, so the built image could not start at
# all — `import server` raised ModuleNotFoundError on the first one. Which means
# every control added in the security review was absent from the artifact that would
# actually be deployed, and the natural fix under time pressure (`COPY . .`) would
# pull .env and credentials into the image, defeating .dockerignore.
#
# Kept as an explicit list rather than `COPY . .` so what ships is a decision. The
# test at tests/test_packaging.py fails if a module server.py imports is missing here.
# Apache 2.0 section 4(d) requires the NOTICE file to travel with redistributions,
# and a container image is a redistribution.
COPY --chown=mcp:mcp LICENSE NOTICE /app/
COPY --chown=mcp:mcp server.py /app/server.py
COPY --chown=mcp:mcp audit.py /app/audit.py
COPY --chown=mcp:mcp authz.py /app/authz.py
COPY --chown=mcp:mcp deployment.py /app/deployment.py
COPY --chown=mcp:mcp dryrun.py /app/dryrun.py
COPY --chown=mcp:mcp logging_config.py /app/logging_config.py
COPY --chown=mcp:mcp mcp_compat.py /app/mcp_compat.py
COPY --chown=mcp:mcp profile_config.py /app/profile_config.py
COPY --chown=mcp:mcp tls_config.py /app/tls_config.py
COPY --chown=mcp:mcp handlers /app/handlers
COPY --chown=mcp:mcp auth /app/auth
COPY --chown=mcp:mcp gui /app/gui
# The enterprise profile's default audit sink lives here, and an unwritable audit
# path is now fatal at startup (audit.py) — so the directory has to exist and belong
# to the runtime user, or every enterprise container would refuse to boot.
RUN mkdir -p /var/log/couchbase-admin-mcp \
&& chown mcp:mcp /var/log/couchbase-admin-mcp \
&& chmod 0700 /var/log/couchbase-admin-mcp
USER mcp
# Container defaults:
# * Transport defaults to STDIO — the frictionless path for Claude Desktop and
# other local MCP clients, which launch the server with `docker run -i` and
# speak over stdin/stdout. stdio cannot cross a container boundary, so for a
# long-running networked service that other containers/agents connect to,
# opt in with -e CB_ADMIN_TRANSPORT=http (and CB_ADMIN_HOST=0.0.0.0).
# * Host defaults to 127.0.0.1 (only relevant once http is enabled). Set
# CB_ADMIN_HOST=0.0.0.0 to accept connections from other containers/the host.
# HTTP without OAuth performs no request auth — keep it on a trusted network,
# publish the port only when you mean to, and enable OAuth (OAUTH_ISSUER +
# CB_ADMIN_HTTP_REQUIRE_AUTH=true) for authenticated scope enforcement.
# Read-only is ON by default in every mode — writes require CB_ADMIN_READ_ONLY_MODE=false.
ENV CB_ADMIN_READ_ONLY_MODE=true \
CB_ADMIN_TRANSPORT=stdio \
CB_ADMIN_HOST=127.0.0.1 \
CB_ADMIN_PORT=8000
# Document the HTTP transport port (no-op for stdio mode)
EXPOSE 8000
# Healthcheck — only meaningful in HTTP transport mode. When CB_ADMIN_TRANSPORT
# is anything other than 'http', the check exits 0 (skip), since stdio mode
# has no HTTP listener to probe.
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD python -c "import os, sys; \
sys.exit(0) if os.environ.get('CB_ADMIN_TRANSPORT', 'stdio').lower() != 'http' else None; \
import urllib.request; \
host = os.environ.get('CB_ADMIN_HOST', '127.0.0.1'); \
port = os.environ.get('CB_ADMIN_PORT', '8000'); \
urllib.request.urlopen(f'http://{host}:{port}/mcp', timeout=5)" || exit 1
ENTRYPOINT ["python", "/app/server.py"]