6.9 KiB
Secure MCP stack
The Docker Compose deployment of the MCP servers: the compose files,
the gateway’s Caddyfiles, the egress proxy’s configuration and the
example .env. The servers themselves live in the
mcps repository, which builds their images and publishes
them to Docker Hub; this repository only pulls them, so a server running
it needs neither the sources nor a build toolchain.
| Image | What it is |
|---|---|
luciolelii/coding-agent-mcp |
Scoped workspace editing. Task execution is disabled in this stack. |
luciolelii/dev-server-mcp |
Authenticated facade for a private, allowlisted development-server worker. One instance per execution, each in a throwaway copy of that execution’s tree and on its own port. The same image runs the facade and the worker. |
luciolelii/browser-mcp |
Playwright automation restricted to exact allowed origins. |
luciolelii/minio-mcp |
Bounded list/read/stat/write access to object storage, with no
delete tool. In internal mode (catalog entry
minio-mcp-internal, header x-minio-scope) it
uses the platform’s MinIO whose endpoint and keys the catalog entry
sends as headers, and one bucket per execution,
<MINIO_MCP_INTERNAL_BUCKET_PREFIX><execution id>,
created when the session opens; in external mode the model opens
session-scoped connections to MinIO deployments itself. |
luciolelii/postgres-mcp |
SQL access to PostgreSQL: postgres_query (read-only,
bounded rows), postgres_execute,
postgres_list_tables and
postgres_describe_table. In internal mode (catalog entry
postgres-mcp-internal, header
x-postgres-scope) each execution gets a database and a user
of its own,
<POSTGRES_MCP_INTERNAL_DATABASE_PREFIX><execution id>,
created when the session opens, which nobody else can connect to; the
model may do anything inside it and nothing outside. In external mode
the model opens a connection to a host the server allows, and the mode
is off until POSTGRES_MCP_ALLOWED_HOSTS names one. |
egress-proxy is the development worker’s only route to
package registries: it allows their CONNECT requests and refuses
everything else.
The MCP client orchestrates the servers; they do not share MCP sessions or credentials.
Start
Copy
mcp-stack.env.exampleto.envand replace every token with an independent random value. Every variable, required or optional, is documented in ENVIRONMENT.md.Set
MCP_WORKSPACE_HOST_PATHto one dedicated project directory, owned by10001:10001(sudo chown -R 10001:10001 <dir>): that is the user the images run as.Copy and edit
dev-server-mcp/services.example.json, and pointDEV_SERVER_SERVICES_CONFIGat the copy. It ships four worked service definitions: a shared Vite tree, and per-execution ones for Node, Java (through the project’s own./mvnw) and Python. KeepBROWSER_MCP_ALLOWED_ORIGINSaligned withDEV_SERVER_PORT_RANGE: the browser reaches a preview only on a port that range covers.Optionally set
MINIO_MCP_ALLOWED_ENDPOINTSto the comma-separated MinIO S3 origins clients may use.If the Docker Hub repositories are private, run
docker loginon this host first.Pull and run:
docker compose --env-file .env -f mcp-stack.compose.yml pull docker compose --env-file .env -f mcp-stack.compose.yml up -dOn a dedicated VM add the overlay to both commands, or the gateway loses ports 80 and 443:
-f mcp-stack.compose.yml -f mcp-stack.vm.compose.yml.
Updating
A deploy is a new image tag. Images are published from
mcps and tagged with its commit hash and
latest. Set MCP_IMAGE_TAG in .env
(or edit the default in mcp-stack.compose.yml) to the new
hash, then pull and up -d as above. To restart
only some services without touching the others, name them and add
--no-deps:
up -d --no-deps minio-mcp mcp-gateway.
Local MCP endpoints:
http://127.0.0.1:3101/mcp— coding agenthttp://127.0.0.1:3102/mcp— development serverhttp://127.0.0.1:3103/mcp— browserhttp://127.0.0.1:3104/mcp— MinIO
The same endpoints are also available through the unified gateway at
/coding-agent/mcp, /dev-server/mcp,
/browser/mcp and /minio/mcp on port 3100 (or
the configured HTTPS host on the VM deployment).
Example Codex client configuration (.codex/config.toml
in a trusted project):
[mcp_servers.coding_agent]
url = "http://127.0.0.1:3101/mcp"
bearer_token_env_var = "CODING_AGENT_MCP_TOKEN"
[mcp_servers.dev_server]
url = "http://127.0.0.1:3102/mcp"
bearer_token_env_var = "DEV_SERVER_MCP_TOKEN"
[mcp_servers.browser]
url = "http://127.0.0.1:3103/mcp"
bearer_token_env_var = "BROWSER_MCP_TOKEN"
[mcp_servers.minio]
url = "http://127.0.0.1:3104/mcp"
bearer_token_env_var = "MINIO_MCP_TOKEN"
[mcp_servers.postgres]
url = "http://127.0.0.1:3105/mcp"
bearer_token_env_var = "POSTGRES_MCP_TOKEN"Set those variables in the MCP client’s environment to the token portions configured for the corresponding servers. Do not reuse a token between servers.
Only the minimal Caddy gateway publishes loopback ports. The coding,
development and browser services stay exclusively on internal networks.
minio-mcp has outbound connectivity because each MCP
session can open connections to remote S3 endpoints. Set
MINIO_MCP_ALLOWED_ENDPOINTS whenever those endpoints are
known; leaving it empty gives authenticated MCP clients an intentional
network-request capability. postgres-mcp is the same, and
stricter by default: it has outbound connectivity because the internal
mode reaches the platform’s PostgreSQL and the external mode a host the
model names, but the external mode is off until
POSTGRES_MCP_ALLOWED_HOSTS lists the hosts it may use. For
remote use, use the included VM overlay so Caddy terminates TLS, and add
rate limiting at the network edge when appropriate.
The worker and browser networks are marked internal, no service uses host networking or the Docker socket, and the development-server workspace mount is read-only. Container isolation reduces the blast radius but is not a substitute for an ephemeral VM/microVM when executing hostile multi-tenant code.
Installing dependencies means fetching and running other people’s
code, so three things stand between it and the rest of the stack: the
tree runs in a copy of itself and never in the workspace mount, the
install runs with package scripts disabled, and the only way out of the
worker is a proxy that allows the registries and nothing else. Each
execution also gets a HOME of its own, which is what keeps
Maven’s ~/.m2 from becoming a channel between executions —
npm’s shared cache is safe because npm ci verifies every
package against the lockfile, and Maven has no lockfile to verify
against.
coding-agent-mcp intentionally retains write access to
the selected project because editing is its purpose. Point
MCP_WORKSPACE_HOST_PATH only at a dedicated, backed-up
directory on storage with a disk quota; never point it at a home
directory, repository collection, or filesystem root.